-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
🐛 [Bug]: CSRF Cookie is removed when using Proxy Middleware #3387
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
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. |
According to the documentation at https://docs.gofiber.io/next/middleware/cors this middleware does not have an API that would allow you to get a key that you can then add to the response headers yourself. |
That’s the wrong middleware CORS is not CSRF. The docs you want are https://docs.gofiber.io/next/middleware/csrf CSRF does have a method for getting the token |
Sorry, my mistake |
Yes, this is how you can manually add a cookie to the response and get the desired behavior. But I like the |
Well, the documentation for version 3 does not correspond to reality. func handler(c fiber.Ctx) error {
handler := csrf.HandlerFromContext(c)
token := csrf.TokenFromContext(c)
if handler == nil {
panic("csrf middleware handler not registered")
}
cfg := handler.Config
if cfg == nil {
panic("csrf middleware handler has no config")
}
if !strings.Contains(cfg.KeyLookup, ":") {
panic("invalid KeyLookup format")
}
formKey := strings.Split(cfg.KeyLookup, ":")[1]
tmpl := fmt.Sprintf(`<form action="/post" method="POST">
<input type="hidden" name="%s" value="%s">
<input type="text" name="message">
<input type="submit" value="Submit">
</form>`, formKey, token)
c.Set("Content-Type", "text/html")
return c.SendString(tmpl)
}
// Handler for CSRF middleware
type Handler struct {
sessionManager *sessionManager
storageManager *storageManager
config Config
} This is also inconvenient, because you need to additionally duplicate the csrf settings to correctly set the cookies in handler. |
@nexcode From your original example. Is your server on port 7000 also using CSRF? |
Good find. Will fix. |
No, there is just a proxy that distributes static html, as if we just served a directory with html files. These are limitations of the infrastructure in which SPA applications are built into containers that are available on the local network on their port. This containers is no additional logic for generating a response, there is simply a web server inside the container that distributes own static files. CSRF is needed for the API that this SPA application make requests and which is served from Fiber. |
Thanks for clarifying. In that case, I would recommend either:
or
Trying to layer CSRF middleware on a proxy that just forwards to a static file server is inherently fragile. Fiber's middleware model isn't designed to interact with a response that will be overwritten by a proxied backend. A cleaner architecture avoids that. |
If you don't think this is a problem then this issue can be closed, I'll just do this: app.Get("/", func(c fiber.Ctx) error {
if err := proxy.Do(c, "https://localhost:7000"); err != nil {
return err
}
c.Cookie(&fiber.Cookie{
Name: "csrf_",
Value: csrf.TokenFromContext(c),
Path: "/",
SameSite: fiber.CookieSameSiteLaxMode,
MaxAge: int((30 * time.Minute).Seconds()),
})
return nil
}) I just thought it might be better to have it handled in Fiber itself :) |
Most middleware — including CSRF — sets cookies before the route handler runs. The exception is session middleware when using the new handler-based approach, since it defers saving until after the route completes, to capture any changes made to the session during the request. In this case, the confusion comes from trying to mix middleware like session or CSRF with So this isn’t a bug in Fiber, CSRF, or proxy — it’s just a misunderstanding of how and when middleware runs in the chain relative to a handler like If we did want to support this use case, we’d need to standardize and redesign how middleware runs relative to So yes, we can close this issue and also the related PR #3390. |
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: