Skip to content

Wrap router with http.CrossOriginProtection for CSRF defence#119

Open
paskal wants to merge 1 commit intoumputun:masterfrom
paskal:feat/csrf-protection
Open

Wrap router with http.CrossOriginProtection for CSRF defence#119
paskal wants to merge 1 commit intoumputun:masterfrom
paskal:feat/csrf-protection

Conversation

@paskal
Copy link
Copy Markdown
Contributor

@paskal paskal commented Apr 17, 2026

Adds Go 1.25's http.NewCrossOriginProtection().Handler to the global middleware chain in app/server/server.go. One line of code, ~50 lines of test.

Why

Today the only CSRF defence on the cookie session is SameSite=Strict, which has known gaps:

  • Firefox does not default to SameSite=Lax on its release channel and has no plans to change in 2025 -- explicit Strict is honoured but doesn't cover sites that only set Lax
  • subdomain attacks bypass SameSite entirely -- it operates on site (registrable domain), not origin, so an attacker controlling any *.safesecret.info subdomain can issue same-site POSTs with the session cookie attached
  • Chrome's "Lax+POST" two-minute window keeps cookies without an explicit SameSite available across sites for 120s after navigation

http.CrossOriginProtection checks the browser-set Sec-Fetch-Site header (a forbidden header that JavaScript cannot forge, shipped in all major browsers since 2023) with an Origin vs Host fallback for older clients. Unlike SameSite, it distinguishes same-origin from same-site -- subdomain attacks are blocked.

OWASP elevated this algorithm from defence-in-depth to a primary defence in its CSRF cheatsheet in December 2025.

What changed

  • app/server/server.go -- one line in the router.Use(...) block
  • app/server/server_test.go -- new TestServer_crossOriginProtection covering same-origin / cross-site / origin-host-mismatch / non-browser cases for both web routes (/theme) and the API (/api/v1/message)

Behaviour notes

  • HTMX-driven POSTs from the web UI run as same-origin requests, so Sec-Fetch-Site: same-origin is sent and the middleware passes them through. Verified end-to-end via the new test.
  • API consumers (curl, scripts, server-to-server) do not send Sec-Fetch-Site -- the middleware treats this as a non-browser request and lets them through. The API was never cookie-authenticated so this is the correct behaviour.
  • A cross-origin browser POST to /api/v1/message (e.g. an attacker page issuing fetch() without CORS) is now rejected before reaching the handler -- not a regression since CORS is not configured.

References

Add Go 1.25's http.NewCrossOriginProtection().Handler to the global
middleware chain. Previously the only CSRF defence was SameSite=Strict
on the session cookie, which Firefox does not enforce by default and
which subdomain attacks can bypass.

The middleware checks Sec-Fetch-Site (forbidden header, set by all
major browsers since 2023) with an Origin/Host fallback. Safe methods
and non-browser POSTs (no Sec-Fetch-Site header, e.g. curl/scripts
hitting /api/v1/) pass through unchanged.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant