Cross-site request forgery¶
CSRF exploits the browser’s automatic inclusion of cookies
in every request to a site. A page on attacker.com posts a form to bank.com, and the victim’s session cookie goes
along for the ride. The server sees a valid authenticated request; it has no way to distinguish it from one the user
initiated.
SameSite cookies¶
The most effective general mitigation is the SameSite cookie attribute. It instructs browsers not to attach the cookie
to cross-site requests:
Set-Cookie: session=abc123; SameSite=Strict; Secure; HttpOnly
Strict prevents the cookie from being sent on any cross-origin request, including navigations. Lax (the current
browser default) allows the cookie on top-level navigations (clicking a link) but not on form submissions or embedded
requests. Lax is sufficient for most CSRF protection; Strict breaks some legitimate workflows (e.g., users arriving
at a page via email link and expecting to be logged in).
Synchroniser tokens¶
For environments where SameSite cannot be relied on (older browsers, APIs accessed by non-browser clients), a
per-session token added to every state-changing form provides a second layer:
import secrets
from flask import session
def generate_csrf_token() -> str:
if "csrf_token" not in session:
session["csrf_token"] = secrets.token_hex(32)
return session["csrf_token"]
def validate_csrf_token(submitted_token: str) -> None:
expected = session.get("csrf_token")
if not expected or not secrets.compare_digest(expected, submitted_token):
raise ValueError("CSRF token invalid")
secrets.compare_digest() avoids timing attacks; a simple == comparison on strings reveals timing information that
can be used to brute-force the token one character at a time.
The token appears as a hidden field in every form:
<form method="POST" action="/settings/email">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
...
</form>
The server extracts the submitted token, compares it to the session value, and rejects the request if they do not match.
Framework-provided CSRF protection¶
Django includes CSRF middleware that handles token generation and validation automatically. It is enabled by default;
disabling it (@csrf_exempt) is intentional and removes the protection entirely. Flask-WTF provides equivalent
protection for Flask applications via the CSRFProtect extension.
For JSON APIs where the client is a single-page application on the same origin, CSRF is substantially mitigated by
requiring a custom request header (X-Requested-With, X-CSRFToken). Custom headers cannot be set by cross-origin
forms, only by XMLHttpRequest or fetch calls from the same origin or an explicitly allowed origin.
Controls that fall short¶
The Referer header is inconsistently present (some proxies strip it, some users configure their browsers to suppress
it) and can sometimes be manipulated. Checking only for Referer is insufficient as a sole protection.
Checking the request method (accepting only POST) does not prevent CSRF; a cross-origin form can submit a POST
request. It makes exploitation marginally harder but is not a control.
HTTPS alone does not prevent CSRF. The browser still attaches cookies to HTTPS requests from other origins.