Choose your stack

Node.js · Express

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 · Flask

Fix 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 Boot

Fix 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 Core

Fix CSRF in C# and ASP.NET Core

Call AddAntiforgery() and, after UseAuthentication() and UseAuthorization(), UseAntiforgery().

Open the fix card
Python · Django

Fix 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 · Rails

Fix 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 · Laravel

Fix 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 card

Stacks without a card

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

  1. OWASP Cross-Site Request Forgery Prevention Cheat Sheet
  2. OWASP Top 10:2025 A01 Broken Access Control
  3. CWE-352: Cross-Site Request Forgery (CSRF)
  4. CAPEC-62: Cross Site Request Forgery
  5. OWASP ASVS 5.0.0, V3 Web Frontend Security (3.3 Cookie Setup, 3.5 Browser Origin Separation)
  6. W3C Fetch Metadata Request Headers
  7. MDN: Sec-Fetch-Site
  8. MDN: Origin (when browsers send it, and when it is null)
  9. MDN: Set-Cookie (SameSite)
  10. Filippo Valsorda: Cross-Site Request Forgery

Attack patterns:

Practice in the browser

Guides on this weakness