-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
🐛 [Bug]: CSRF Cookie is removed when using Proxy Middleware #3387
Comments
Thanks for opening your first issue here! 🎉 Be sure to follow the issue template! If you need help or want to chat with us, join us on Discord https://gofiber.io/discord |
@nexcode The cookie is not set if using the proxy middleware? Is that the issue? |
Hi @nexcode , After investigating the interaction between the The problem stems from how the Fiber
|
I figured yes, because Session middleware sets its cookie header correctly, so I figured CSRF middleware should too. In my code snippet, even through a proxy, I am given a |
Since the fiber/middleware/session/middleware.go Lines 77 to 108 in c5c7f86
fiber/middleware/session/session.go Lines 296 to 311 in c5c7f86
|
I'm not sure if we need to keep the csrf like a session. what do you think? @gaby @ReneWerner87 |
Maybe you are right, my case is that when using a proxy middleware to serve a static SPA, you need to make an additional empty route (for example, |
I tried the same scenario on Verification Codepackage main
import (
"log"
"net/http"
"net/url"
"os"
"github.com/gorilla/sessions"
"github.com/labstack/echo-contrib/session"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
const (
targetPort = ":7000"
mainPort = ":8000"
sessionSecret = "super-secret-key-change-me" // CHANGE THIS IN PRODUCTION!
sessionCookieName = "my_session_id" // Custom session cookie name
csrfCookieName = "_csrf" // Default Echo CSRF cookie name
)
// --- Target Server ---
func startTargetServer() {
e := echo.New()
e.HideBanner = true
e.GET("/", func(c echo.Context) error {
log.Println("[TargetServer:7000] Received request")
c.Response().Header().Set("X-Target-Server", "true")
return c.String(http.StatusOK, "Hello from target server!")
})
log.Printf("Starting target server on %s\n", targetPort)
if err := e.Start(targetPort); err != nil && err != http.ErrServerClosed {
log.Fatalf("Failed to start target server: %v", err)
os.Exit(1)
}
}
// --- Main Server ---
func main() {
// Start target server in a goroutine
go startTargetServer()
// Main Echo instance
e := echo.New()
e.HideBanner = true
// --- Middleware Setup ---
// 1. Session Middleware (using gorilla/sessions)
store := sessions.NewCookieStore([]byte(sessionSecret))
store.Options = &sessions.Options{ // Use store.Options to configure cookie basics
Path: "/",
MaxAge: 1800, // 30 minutes
HttpOnly: true, // Recommended for security
SameSite: http.SameSiteLaxMode,
// NOTE: The actual cookie name is controlled by session.Config below
}
// Apply session middleware using the specific config struct from echo-contrib/session
e.Use(session.MiddlewareWithConfig(session.Config{
Store: store,
}))
log.Println("[MainServer:8000] Using Session middleware")
// 2. CSRF Middleware
e.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{
TokenLookup: "header:X-CSRF-Token,form:_csrf",
CookieName: csrfCookieName,
CookiePath: "/",
CookieSameSite: http.SameSiteLaxMode,
}))
log.Println("[MainServer:8000] Using CSRF middleware")
// 3. Custom Logging Middleware
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
log.Println("--- Request Headers ---")
for k, v := range c.Request().Header {
log.Printf(" %s: %v\n", k, v)
}
err := next(c)
log.Println("--- Response Headers (Before Final Middleware Unwind) ---")
for k, v := range c.Response().Header() {
log.Printf(" %s: %v\n", k, v)
}
return err
}
})
// --- Routes ---
// Proxy Route Setup
targetURL, err := url.Parse("http://localhost" + targetPort)
if err != nil {
e.Logger.Fatal(err)
}
proxyBalancer := middleware.NewRoundRobinBalancer([]*middleware.ProxyTarget{{URL: targetURL}})
// Configure the proxy using ProxyConfig
proxyConfig := middleware.ProxyConfig{
Balancer: proxyBalancer,
// Add other proxy configurations here if needed
}
// Create the proxy *middleware* function
proxyMiddleware := middleware.ProxyWithConfig(proxyConfig)
// Apply the proxy middleware specifically to the "/" route
// It needs a handler function to wrap, even if the proxy normally handles the response.
e.GET("/", func(c echo.Context) error {
// This handler should ideally not be executed if the proxy successfully handles the request.
// It's a necessary placeholder when applying proxy as middleware to a specific route.
log.Println("[MainServer:8000] Fallback handler for / reached (should not happen if proxy works)")
return echo.NewHTTPError(http.StatusInternalServerError, "Proxy fallback")
}, proxyMiddleware) // Apply the proxy middleware here
log.Println("[MainServer:8000] Registered Proxy middleware for /")
// Direct Route (for comparison)
e.GET("/direct", func(c echo.Context) error {
log.Println("[MainServer:8000] Executing Direct Handler for /direct")
sess, _ := session.Get(sessionCookieName, c)
sess.Options = store.Options // Ensure session uses the same options
sess.Values["foo"] = "bar_direct"
if err := sess.Save(c.Request(), c.Response()); err != nil {
log.Printf("[MainServer:8000] Error saving session in /direct: %v", err)
}
csrfToken, ok := c.Get(middleware.DefaultCSRFConfig.ContextKey).(string)
if ok {
log.Printf("[MainServer:8000] CSRF Token in /direct: %s", csrfToken)
} else {
log.Println("[MainServer:8000] CSRF Token not found in context for /direct")
}
return c.String(http.StatusOK, "Hello directly from main server!")
})
// --- Start Server ---
log.Printf("Starting main server on %s\n", mainPort)
if err := e.Start(mainPort); err != nil && err != http.ErrServerClosed {
log.Fatalf("Failed to start main server: %v", err)
}
} Verification results$ curl -v http://localhost:8000/
* Host localhost:8000 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
* Trying [::1]:8000...
* Connected to localhost (::1) port 8000
> GET / HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/8.6.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Length: 25
< Content-Type: text/plain; charset=UTF-8
< Date: Wed, 02 Apr 2025 08:01:54 GMT
< Set-Cookie: _csrf=amfFMQrrKrNHptjqiVSfOBHlDtcmUvZr; Path=/; Expires=Thu, 03 Apr 2025 08:01:54 GMT; SameSite=Lax
< Vary: Cookie
< X-Target-Server: true
<
Hello from target server!* Connection #0 to host localhost left intact $ curl -v http://localhost:8000/direct
* Host localhost:8000 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
* Trying [::1]:8000...
* Connected to localhost (::1) port 8000
> GET /direct HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/8.6.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: text/plain; charset=UTF-8
< Set-Cookie: _csrf=MXsibhbjtAKdhjFffGPnmgnFdCDkMeXl; Path=/; Expires=Thu, 03 Apr 2025 08:02:10 GMT; SameSite=Lax
< Set-Cookie: my_session_id=MTc0MzU4MDkzMHxEWDhFQVFMX2dBQUJFQUVRQUFBbl80QUFBUVp6ZEhKcGJtY01CUUFEWm05dkJuTjBjbWx1Wnd3TUFBcGlZWEpmWkdseVpXTjB8VN3YG_oAn9Vs98c84izjry_XpAvDyh36qx8EcJuVANY=; Path=/; Expires=Wed, 02 Apr 2025 08:32:10 GMT; Max-Age=1800; HttpOnly; SameSite=Lax
< Vary: Cookie
< Date: Wed, 02 Apr 2025 08:02:10 GMT
< Content-Length: 32
<
Hello directly from main server!* Connection #0 to host localhost left intact |
Thanks for the detailed explanation — I appreciate you breaking it down. That said, I still don’t think this behavior warrants a change on the CSRF side. The proxy is working as intended: it's forwarding the request and returning the response from the upstream server, including its headers. Overwriting the response is expected behavior. CSRF protection is designed to mitigate attacks in the browser context, where a malicious site tricks a logged-in user into submitting a request. That concern doesn’t apply when the proxy is just relaying server-to-server calls or browser-initiated requests — especially if the CSRF logic has already run before the proxying happens. From a design perspective, it feels like we’re trying to bolt CSRF protection onto a layer where it doesn't belong. If users need to inject headers post-proxy, that feels like a different problem — and maybe a custom handler is the better fit. |
To clarify: The issue you're seeing with the missing In other words:
As for the More broadly — and this is key — we shouldn't attempt to bolt on middleware like So if you need to inject headers like app.Get("/", func(c fiber.Ctx) error {
err := proxy.Do(c, "https://localhost:7000")
if err != nil {
return err
}
// Optionally inject something into the response
c.Set("Set-Cookie", "csrf_=abc; Path=/; Secure; HttpOnly")
return nil
}) But again, that's not typical for a proxy. If the proxied backend is your own service, it should handle CSRF and session logic itself — the proxy should just forward requests/responses as-is. |
Bug Description
When using the CSRF middleware the cookie is lost if the route uses a Proxy.
When using the Session middleware](https://docs.gofiber.io/next/middleware/session) cookies are kept when using a Proxy.
How to Reproduce
Specified in a small snippet of the code.
Expected Behavior
Response must contains
Set-Cookie: csrf_=
header, because if I use a proxy to serve a static SPA application, then this application cannot make POST requests to the API, since the browser does not receivecsrf_
cookie.Fiber Version
v3.0.0-beta.4
Code Snippet (optional)
Checklist:
The text was updated successfully, but these errors were encountered: