The Confused Deputy Problem

Cross-Site Request Forgery (CSRF) is an attack that forces an end user to execute unwanted actions on a web application in which they are currently authenticated.

Unlike XSS, which steals data from the user, CSRF abuses the trust that a web application has in the user's browser.

How CSRF Works

Browsers automatically send cookies associated with a domain whenever a request is made to that domain.

Imagine you are logged into your bank at bank.com. Your session cookie is active. You then open a new tab and visit evil.com. The attacker's site contains a hidden form:

<form action="https://bank.com/transfer" method="POST">
    <input type="hidden" name="amount" value="10000">
    <input type="hidden" name="to_account" value="HackerAccount">
</form>
<script>
    document.forms[0].submit();
</script>

When the page loads, the form automatically submits a POST request to bank.com. Because your browser sees the request going to bank.com, it automatically attaches your active bank session cookie. The bank's server receives the request, sees your valid cookie, and processes the $10,000 transfer, believing you authorized it.

Defenses Against CSRF

1. Anti-CSRF Tokens (The Traditional Defense) The server generates a unique, cryptographically strong, and unpredictable token for the user's session. This token is embedded into every HTML form as a hidden field. When the form is submitted, the server verifies that the token in the request matches the token in the session. Because the attacker on evil.com cannot read the HTML on bank.com (due to the Same-Origin Policy), they cannot extract the token and their forged request will fail validation.

2. SameSite Cookie Attribute (The Modern Defense) Modern browsers support the SameSite attribute on cookies, which explicitly tells the browser whether to send cookies on cross-site requests. - SameSite=Strict: The cookie is never sent on cross-site requests. Complete CSRF immunity. - SameSite=Lax: The default in modern browsers. Cookies are withheld on cross-site POST requests, but allowed on safe top-level navigations (like clicking a regular link).

Set-Cookie: session_id=abc123xyz; Secure; HttpOnly; SameSite=Strict

3. The Double-Submit Cookie Pattern Useful for Single Page Apps (SPAs). The server sets a random value in a cookie (not HttpOnly). The SPA's JavaScript reads this cookie and attaches it as a custom HTTP header (e.g., X-CSRF-Token) on every API request. The server verifies the header matches the cookie. An attacker cannot read the cookie from another domain to populate the header, blocking the attack.

Why Browsers Are the Weapon

The root cause of CSRF is the browser's auto-attach behavior: cookies ride along with any request to their domain, whether that request was typed in the address bar, submitted by a form, or triggered by an <img> tag. Attackers exploit this by planting requests the victim never initiates: an <img src="https://bank.com/logout">, a <link rel="stylesheet" href="https://api.example.com/delete?account=...">, or an auto-submitting form. Any object-loading tag works, which is why a CSRF defense must never rely on "users won't execute GET-style actions" — modern frameworks either require a token on every state-changing request or refuse to act on GET at all.

The Expanding Surface: JSON APIs

Traditional CSRF targets form-POST requests, but JSON APIs are equally at risk. An attacker can submit a cross-site request with Content-Type: text/plain carrying an embedded JSON body, and some pre-CORS-era backends happily parse it. The defense beats the attack: read the actual Content-Type and reject anything that does not match; verify the Origin header for state-changing JSON endpoints; and keep SameSite on. Origin checks are especially effective because browsers reliably send Origin on cross-origin requests, so a simple allowlist comparison blocks the forged call before your logic even runs:

from urllib.parse import urlparse

origin = request.headers.get("Origin")
expected = {"https://bank.com", "https://www.bank.com"}
if origin and urlparse(origin).netloc not in expected:
    raise Forbidden("Cross-origin request rejected")

Defense-in-Depth Checklist

Ship all four layers rather than choosing one: SameSite=Lax (default hardening), a per-session anti-CSRF token on every state-changing form and header, Content-Type + Origin validation on JSON APIs, and (for maximum security on sensitive actions) a secondary confirmation prompt for destructive operations like transfers or deletions. Layer tokens exist because any single control can fail — a token can leak in a referer, SameSite can be bypassed on old browsers, and an origin check can be skipped by a same-site page.