Choose your stack
Fix CSRF in Node.js and Express
Express has no built-in CSRF protection, so add one middleware before every route: let POST, PUT, PATCH and DELETE through only when Sec-Fetch-Site is same-origin (or none), fall back to an exact Origin match when the browser sends no Sec-Fetch-Site, and refuse the rest with 403.
Open the fix card Python · FlaskFix CSRF in Python and Flask
Register Flask-WTF's CSRFProtect(app). It requires the session's token on every POST, PUT, PATCH and DELETE: render {{ csrf_token() }} in a hidden csrf_token field, or send it from scripts in an X-CSRFToken header.
Open the fix card Java · Spring BootFix CSRF in Java and Spring Boot
Keep Spring Security's CSRF protection on (it is the default) and never call csrf.disable() for browser-facing routes.
Open the fix card C# · ASP.NET CoreFix CSRF in C# and ASP.NET Core
Call AddAntiforgery() and, after UseAuthentication() and UseAuthorization(), UseAntiforgery().
Open the fix card Python · DjangoFix CSRF in Python and Django
Keep django.middleware.csrf.CsrfViewMiddleware in MIDDLEWARE and do not exempt views: it checks the Origin (or, over HTTPS, the Referer) against the site and CSRF_TRUSTED_ORIGINS, and the token.
Open the fix card Ruby · RailsFix CSRF in Ruby on Rails
Keep the protection ActionController::Base already has (load_defaults 5.2 adds protect_from_forgery with: :exception): forms from form_with carry the token, fetch() sends csrf_meta_tags' token in X-CSRF-Token, and Rails also checks Origin.
Open the fix card PHP · LaravelFix CSRF in PHP and Laravel
Keep PreventRequestForgery, which Laravel puts in the web middleware group, on every state-changing route: it accepts a request that the browser marks Sec-Fetch-Site: same-origin, and otherwise requires the session's token in _token or X-CSRF-TOKEN.
Open the fix cardStacks without a card
- TypeScript and Next.js: no card yet. The nearest is Fix CSRF in Node.js and Express, in the same language.
- Python and FastAPI: no card yet. The nearest is Fix CSRF in Python and Flask, in the same language.
- Go and net/http: no card yet. The nearest is Fix CSRF in Node.js and Express.
How it happens
- Browsers attach a site's cookies to requests for that site whichever page started them. A hidden form on evil.example, submitted by script, can POST email=mallory@evil.example to portal.example.test/account/email, and the server sees Alice's valid session.
- The attacker never reads the response and does not need to: the change has already happened. A CORS error in the browser console does not mean the request was blocked.
- SameSite=Lax narrows the attack but does not close it. Browsers still send a Lax cookie on top-level GET navigations from other sites and on every request from a sibling subdomain (forum.example.test is the same site as portal.example.test), and older browsers ignore the attribute.
- Handlers that change state on GET, through broad mappings such as app.all(), @RequestMapping without a method or methods=["GET", "POST"], can be triggered by a link the user follows or a script that navigates the page, and the automatic CSRF checks in Spring Security, ASP.NET Core and Flask-WTF skip GET by design.
- Hand-made origin checks fail in predictable ways. A test that the Referer contains the host name accepts https://portal.example.test.evil.example, and letting requests without a Referer through admits any page that sets Referrer-Policy: no-referrer, whose form posts also carry Origin: null.
Fix principles
- Use the framework's CSRF protection and keep it on: Spring Security's CsrfFilter, ASP.NET Core antiforgery and Flask-WTF's CSRFProtect tie a token to the session or user and check it on POST and other unsafe methods (each card lists exactly which). Find and remove csrf.disable(), .DisableAntiforgery() and blanket exemptions.
- Where the framework has none, as with Express, refuse unsafe requests unless Sec-Fetch-Site is same-origin (or none), fall back to an exact Origin match for browsers that do not send Sec-Fetch-Site, and refuse requests that carry neither; or use a synchronizer token library.
- Change state only with POST, PUT, PATCH or DELETE, and map each handler to its method so that a GET cannot reach it.
- Bind tokens to the session or user and send them in a form field or a custom header, never only in a cookie; a token from another session must fail.
- Compare origins exactly, as scheme, host and port, against a configured value. Never test whether a header contains your host name, and treat a missing Origin or Origin: null as untrusted.
- Set SameSite=Lax or Strict, HttpOnly and Secure on the session cookie as defence in depth, and protect sign-in and sign-out as well.
How the cards prove it
- Run each card's tests in three modes: the fixed implementation must pass, and the vulnerable and the plausible-but-wrong implementations must fail. run-all.mjs does this for every card.
- The exploit tests attach the victim's session cookie to each forged request and send the headers a browser would: a cross-site form post without a token, a foreign Origin without Fetch Metadata, a sibling subdomain (Sec-Fetch-Site: same-site), another user's token, a look-alike host in the Referer, a no-referrer page (Origin: null) and a cross-site GET navigation. Each must leave the email unchanged. Functional tests keep same-origin form posts, fetch with a header token, browsers without Fetch Metadata, a second tab and links from other sites working.
- Static analysis is uneven. Semgrep flags Spring's @RequestMapping without a method, but its spring-csrf-disabled rule only matches the http.csrf().disable() form that Spring Security 7 removed, its Express rule only fires on CommonJS apps and cannot tell a Fetch Metadata check from no check, and nothing checked here covers ASP.NET Core minimal APIs or a Flask app without Flask-WTF. Each card's scannerRules records what was run and what it found.
References
- OWASP Cross-Site Request Forgery Prevention Cheat Sheet
- OWASP Top 10:2025 A01 Broken Access Control
- CWE-352: Cross-Site Request Forgery (CSRF)
- CAPEC-62: Cross Site Request Forgery
- OWASP ASVS 5.0.0, V3 Web Frontend Security (3.3 Cookie Setup, 3.5 Browser Origin Separation)
- W3C Fetch Metadata Request Headers
- MDN: Sec-Fetch-Site
- MDN: Origin (when browsers send it, and when it is null)
- MDN: Set-Cookie (SameSite)
- Filippo Valsorda: Cross-Site Request Forgery
Attack patterns:
Practice in the browser
- CSRF lab: Protect an account change from a forged request · Foundation, JavaScript
Guides on this weakness
Protect cookie-authenticated actions against CSRF
Make the server distinguish an intended action from a request another site caused the browser to send.
Configure CORS and CSP around the browser capabilities you need
Control who can read API responses and which resources a page may execute without confusing either control with authorization.
Preserve framework security controls across the request lifecycle
Trace middleware, persistence and rendering so custom code does not bypass the protections supplied by the framework.
OAuth 2.0 and OpenID Connect login: authorization code with PKCE
Implement OAuth 2.0 and OpenID Connect login with the authorization code flow and PKCE, keep each transaction bound to its browser, and block redirect, mix-up, code-injection and token-theft attacks.
Defend OAuth and OIDC against redirect and token attacks
Apply RFC 9700 and RFC 10017: exact redirect matching, PKCE with downgrade protection, state and iss binding, refresh-token rotation, sender-constrained tokens, and a backend-for-frontend so browser tokens cannot be stolen.