Skip to content
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

Open
3 tasks done
nexcode opened this issue Apr 1, 2025 · 10 comments · May be fixed by #3390
Open
3 tasks done

🐛 [Bug]: CSRF Cookie is removed when using Proxy Middleware #3387

nexcode opened this issue Apr 1, 2025 · 10 comments · May be fixed by #3390
Milestone

Comments

@nexcode
Copy link

nexcode commented Apr 1, 2025

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 receive csrf_ cookie.

Fiber Version

v3.0.0-beta.4

Code Snippet (optional)

package main

import (
	"github.com/gofiber/fiber/v3"
	"github.com/gofiber/fiber/v3/middleware/csrf"
	"github.com/gofiber/fiber/v3/middleware/proxy"
	"github.com/gofiber/fiber/v3/middleware/session"
)

func main() {
	app := fiber.New()

	sessionMiddleware, sessionStore := session.NewWithStore()

	app.Use(sessionMiddleware)
	app.Use(csrf.New(csrf.Config{
		Session: sessionStore,
	}))

	app.Get("/", func(c fiber.Ctx) error {
		return proxy.Do(c, "https://localhost:7000")
	})

	app.Listen(":8000")
}

Checklist:

  • I agree to follow Fiber's Code of Conduct.
  • I have checked for existing issues that describe my problem prior to opening this one.
  • I understand that improperly formatted bug reports may be closed without explanation.
Copy link

welcome bot commented Apr 1, 2025

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

@gaby
Copy link
Member

gaby commented Apr 1, 2025

@nexcode The cookie is not set if using the proxy middleware? Is that the issue?

@gaby gaby added this to v3 Apr 1, 2025
@gaby gaby added this to the v3 milestone Apr 1, 2025
@JIeJaitt
Copy link
Contributor

JIeJaitt commented Apr 1, 2025

Hi @nexcode ,

After investigating the interaction between the proxy middleware and other middleware like csrf, i've identified the cause.

The problem stems from how the Fiber proxy middleware utilizes the underlying fasthttp library to perform the request forwarding. Here's a breakdown:

  1. The proxy.Do function (and related proxy functions) obtains the underlying fasthttp.Response object (resp) from the current Fiber context (the one handling the request to your proxy server, e.g., localhost:8000).
  2. It then calls fasthttp.Client.Do (or a similar method like fasthttp.HostClient.Do) passing this resp object.
  3. Crucially, the standard behavior of fasthttp's client methods when filling a response object involves resetting the provided resp object first (using resp.Reset()). This clears out any existing status code, body, and headers that might have been set previously on the localhost:8000 response context.
  4. fasthttp then reads the entire response (status, headers, body) from the target server (e.g., localhost:7000) and populates the now-empty resp object with this new data.
  5. Therefore, the headers received from the target server (localhost:7000) completely replace the original headers of the response object belonging to the proxy server (localhost:8000).

@nexcode
Copy link
Author

nexcode commented Apr 1, 2025

@nexcode The cookie is not set if using the proxy middleware? Is that the issue?

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 session_id cookie header from session middleware.
I think middleware should work the same way with other middlewares.

@JIeJaitt
Copy link
Contributor

JIeJaitt commented Apr 1, 2025

@nexcode

Since the saveSession function saves the session cookie information, the session cookie is retained in subsequent requests. the point is that fasthttp's proxy.Do clears the response header from the client.

func NewWithStore(config ...Config) (fiber.Handler, *Store) {
cfg := configDefault(config...)
if cfg.Store == nil {
cfg.Store = NewStore(cfg)
}
handler := func(c fiber.Ctx) error {
if cfg.Next != nil && cfg.Next(c) {
return c.Next()
}
// Acquire session middleware
m := acquireMiddleware()
m.initialize(c, cfg)
stackErr := c.Next()
m.mu.RLock()
destroyed := m.destroyed
m.mu.RUnlock()
if !destroyed {
m.saveSession()
}
releaseMiddleware(m)
return stackErr
}
return handler, cfg.Store
}

// saveSession encodes session data to saves it to storage.
func (s *Session) saveSession() error {
if s.data == nil {
return nil
}
s.mu.Lock()
defer s.mu.Unlock()
// Set idleTimeout if not already set
if s.idleTimeout <= 0 {
s.idleTimeout = s.config.IdleTimeout
}
// Update client cookie
s.setSession()

@JIeJaitt
Copy link
Contributor

JIeJaitt commented Apr 1, 2025

I'm not sure if we need to keep the csrf like a session. what do you think? @gaby @ReneWerner87

@nexcode
Copy link
Author

nexcode commented Apr 1, 2025

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, /get-csrf), a request to which the SPA must make only to get csrf_ cookies. It would be very convenient if the SPA was ready to work with POST server API right away.

@gaby gaby changed the title 🐛 [Bug]: csrf middleware dont set cookie if use proxy middleware 🐛 [Bug]: CSRF Cookie is removed when using Proxy Middleware Apr 1, 2025
@JIeJaitt
Copy link
Contributor

JIeJaitt commented Apr 2, 2025

I tried the same scenario on echo, and interestingly enough, they only save the csrf and not the session, the opposite of fiber.

Verification Code

package 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

@sixcolors
Copy link
Member

Hi @nexcode ,

After investigating the interaction between the proxy middleware and other middleware like csrf, i've identified the cause.

The problem stems from how the Fiber proxy middleware utilizes the underlying fasthttp library to perform the request forwarding. Here's a breakdown:

  1. The proxy.Do function (and related proxy functions) obtains the underlying fasthttp.Response object (resp) from the current Fiber context (the one handling the request to your proxy server, e.g., localhost:8000).
  2. It then calls fasthttp.Client.Do (or a similar method like fasthttp.HostClient.Do) passing this resp object.
  3. Crucially, the standard behavior of fasthttp's client methods when filling a response object involves resetting the provided resp object first (using resp.Reset()). This clears out any existing status code, body, and headers that might have been set previously on the localhost:8000 response context.
  4. fasthttp then reads the entire response (status, headers, body) from the target server (e.g., localhost:7000) and populates the now-empty resp object with this new data.
  5. Therefore, the headers received from the target server (localhost:7000) completely replace the original headers of the response object belonging to the proxy server (localhost:8000).

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.

@sixcolors
Copy link
Member

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 receive csrf_ cookie.

Fiber Version

v3.0.0-beta.4

Code Snippet (optional)

package main

import (
"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/middleware/csrf"
"github.com/gofiber/fiber/v3/middleware/proxy"
"github.com/gofiber/fiber/v3/middleware/session"
)

func main() {
app := fiber.New()

sessionMiddleware, sessionStore := session.NewWithStore()

app.Use(sessionMiddleware)
app.Use(csrf.New(csrf.Config{
Session: sessionStore,
}))

app.Get("/", func(c fiber.Ctx) error {
return proxy.Do(c, "https://localhost:7000")
})

app.Listen(":8000")
}

Checklist:

  • I agree to follow Fiber's Code of Conduct.[x] I have checked for existing issues that describe my problem prior to opening this one.[x] I understand that improperly formatted bug reports may be closed without explanation.

To clarify:

The issue you're seeing with the missing Set-Cookie: csrf_= header is due to how the proxy middleware works internally — and more specifically, how fasthttp.Client.Do() behaves. When proxying a request, it completely replaces the original response object (c.Response()) with the response received from the upstream server. That includes status code, body, and all headers. This is expected behavior for a proxy — it's supposed to transparently forward the request and return the exact response from the target service.

In other words:

A proxy is not a partial passthrough — it’s a full relay of the upstream response.

As for the session middleware appearing to retain the cookie: that’s just an accidental implementation detail, not a guarantee. It happens due to the order and timing of how session.Save() writes headers — but this is fragile and shouldn’t be relied on. It could easily break if the middleware ordering changes or internals are refactored.

More broadly — and this is key — we shouldn't attempt to bolt on middleware like csrf, session, etc. to proxy routes. These middlewares are designed to operate on app-level routes, where Fiber handles the response directly. In a proxied request, Fiber hands off control to an upstream server, and the response belongs to that upstream, not to Fiber.

So if you need to inject headers like Set-Cookie into a proxied response, you'd need to explicitly do it after the proxy call, e.g.:

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: No status
Development

Successfully merging a pull request may close this issue.

4 participants