Cross-Site Request Forgery

A user logs into their bank, then browses a malicious forum post in another tab. Their savings quietly move. This is the essence of Cross-Site Request Forgery (CSRF): a browser faithfully attaches a user's credentials to a request the user never intended to send. The attack exploits a mismatch between two things the web was built to separate. It never fully did — the browser's automatic handling of ambient authority like cookies, and a server's assumption that any request bearing valid credentials reflects genuine user intent. CSRF matters because it turns the browser's convenience features into a weapon. Unlike Cross-Site Scripting, which injects attacker code into a victim's browsing session, CSRF needs no code execution on the target site at all. It only needs a browser that will send cookies alongside a cross-origin request. It also needs a server that trusts cookies as proof of intent. For over a decade this made CSRF one of the most common vulnerabilities in deployed web applications. It sat for years in the OWASP Top Ten before browser and framework defenses pushed it down. This article builds the mental model of the attack from first principles inside the The Web Security Model. It then walks through the classical and modern defenses: synchronizer tokens, double-submit cookies, and the `SameSite` cookie attribute. Each defense is explained not just as a recipe but as a response to a precise gap in the trust model. Specifically, what information a forged request is missing that a legitimate request has. By the end you should be able to explain, in one paragraph, why a plain cookie is not proof of user intent. You should be able to design a token scheme that closes that gap. You should also be able to identify which real-world defenses are necessary versus merely helpful. The chapter foreshadows a broader lesson explored under Security Engineering and the Human Factor: technical fixes only work if they survive contact with careless implementation.

Key takeaways
  • CSRF forces a victim's browser to submit an authenticated request the victim never intended, exploiting automatic credential attachment (cookies).
  • The core flaw: servers treat "request carries valid session cookie" as equivalent to "user intended this action" — those are not the same thing.
  • Classic defenses add a secret the attacker cannot know or predict: the synchronizer token pattern and the double-submit cookie pattern.
  • Modern browsers add a structural defense: the `SameSite` cookie attribute, which restricts when cookies travel cross-site.
  • CSRF requires no script execution on the victim site, unlike Cross-Site Scripting, making it exploitable from any page the victim merely visits.
  • Real defense in depth combines `SameSite` cookies, anti-CSRF tokens, and checking the `Origin`/`Referer` header — no single layer is fully sufficient alone.
The core insight

A forged request looks identical to a legitimate one at the server, unless the server demands proof of intent that the attacker's page cannot forge or predict.

The idea

A Cross-Site Request Forgery attack tricks a victim's browser into issuing a request to a target site, using credentials the browser automatically attaches, without the victim's knowledge or consent. The attacker does not need to read the response — many CSRF exploits are "blind," succeeding purely because the request executes a state-changing action (transfer funds, change an email address, delete an account) on the server. The vulnerability is structural, not a coding bug in the traditional sense. Browsers attach cookies to every request sent to a domain, regardless of which page or origin initiated that request. This is precisely the behavior that keeps users logged in across page loads, and it is precisely what CSRF abuses: a form on `evil.example` can auto-submit to `bank.example`, and the browser will happily include the victim's `bank.example` session cookie. The server sees a request with a valid session, and — if it does nothing further — cannot distinguish "the legitimate user clicked a button on my site" from "the user's browser was tricked into firing a request from somewhere else." The key terminology: the origin of a request is the scheme, host, and port of the page that initiated it; a request is cross-site (or cross-origin, depending on granularity) when the initiating page's origin differs from the target's. Ambient authority describes credentials, like cookies, that travel automatically without the requester having to actively supply them — this is the property CSRF exploits. A CSRF token (or anti-CSRF token) is a secret value the server requires in the request body or a custom header, which an attacker's page cannot read or predict, thereby proving the request originated from a page the server itself served, with the user's active participation.

How it works

The anatomy of the attack has three participants: the victim (an authenticated user), the target site (which trusts cookies), and the attacker's site (which the victim visits while still logged into the target). The attack does not require the attacker to compromise the target server or run script in the target's origin — that is XSS's job. CSRF is purely a request-forgery problem exploiting trust boundaries.

Attack construction (GET-based, simplest form): ```pseudocode // Victim is logged into bank.example (has a valid session cookie) // Attacker crafts a page hosted anywhere, e.g. evil.example attacker_page: // When victim's browser loads evil.example: // 1. Browser parses the tag // 2. Browser issues GET https://bank.example/transfer?to=attacker&amount=1000 // 3. Browser AUTOMATICALLY attaches bank.example's session cookie // 4. bank.example sees a valid session -> executes the transfer // 5. Victim never clicked anything related to a transfer ``` State-changing actions should never be reachable via GET, but many legacy applications violate this, making the `` vector devastating: the victim need not even click — page load alone triggers the request.

Attack construction (POST-based, auto-submitting form): ```pseudocode attacker_page:

// Browser sends a real cross-site POST with body: // to=attacker&amount=1000 // and attaches bank.example cookies automatically, exactly as // it would for a legitimate form submission from bank.example itself. ``` The server-side logic that is vulnerable looks deceptively correct: ```pseudocode function handle_transfer(request): session = lookup_session(request.cookies["session_id"]) if session is None or not session.authenticated: return 401 Unauthorized // BUG: only checks who is logged in, never whether this // specific browser tab, with user intent, submitted this form execute_transfer(session.user, request.form["to"], request.form["amount"]) return 200 OK ```

Defense 1 — Synchronizer Token Pattern. The server embeds a per-session (or per-request) secret token in every state-changing form it serves, and rejects submissions that do not echo it back correctly. ```pseudocode function render_transfer_form(session): token = generate_random_token() // cryptographically random, e.g. 128+ bits session.pending_csrf_token = token // stored server-side, tied to session return html_form_with_hidden_field("csrf_token", token) function handle_transfer(request): session = lookup_session(request.cookies["session_id"]) if session is None or not session.authenticated: return 401 Unauthorized submitted_token = request.form["csrf_token"] if submitted_token != session.pending_csrf_token: return 403 Forbidden // attacker cannot know this value execute_transfer(session.user, request.form["to"], request.form["amount"]) invalidate_or_rotate(session.pending_csrf_token) return 200 OK ``` The attacker's forged form cannot include a valid `csrf_token`, because it can only be read from a page rendered by the target server — and cross-origin script cannot read another origin's response body, by the The Web Security Model's same-origin policy.

Defense 2 — Double-Submit Cookie Pattern. Useful for stateless servers that avoid server-side session storage. The server sets a random token both as a cookie and expects it echoed in the request body or a custom header; since the attacker cannot read the cookie's value (again, blocked by same-origin policy), it cannot replicate it in the forged form. ```pseudocode function issue_csrf_cookie(response): token = generate_random_token() response.set_cookie("csrf_token", token, secure=true, samesite="Strict") return response // client-side JS on the legitimate site reads this // cookie and copies it into a custom header on requests function handle_transfer(request): cookie_token = request.cookies["csrf_token"] header_token = request.headers["X-CSRF-Token"] if cookie_token is None or header_token is None: return 403 Forbidden if cookie_token != header_token: return 403 Forbidden execute_transfer(...) return 200 OK ``` The double-submit pattern relies on the attacker being unable to set an arbitrary custom header on a cross-site request without triggering a CORS preflight that the target server can reject, and unable to read the cookie value to replicate it, since JavaScript on `evil.example` cannot access `bank.example`'s cookies.

Defense 3 — `SameSite` Cookie Attribute. A structural browser-level fix: cookies marked `SameSite=Strict` are never sent on cross-site requests at all; `SameSite=Lax` (the modern browser default) sends cookies on top-level navigations via safe methods (GET) but not on cross-site POSTs, embedded images, or fetches. ```pseudocode Set-Cookie: session_id=abc123; SameSite=Lax; Secure; HttpOnly ``` With `Lax`, the POST-based auto-submitting form attack above fails outright: the browser refuses to attach the session cookie to a cross-site POST, so the server never even reaches the authentication check with a valid session.

Figure
Anatomy of a CSRF attack and where each defense intervenes
Victim Browser cookie jar: bank.example → session=XYZ evil.example <form action= "bank.example/xfer"> auto-submit on load bank.example server checks session cookie executes transfer if session valid victim visits POST /xfer + cookie session=XYZ (attached automatically) Where defenses intervene Synchronizer token: server rejects — no valid csrf_token in forged form SameSite=Lax/Strict: browser never attaches cookie to cross-site POST Double-submit cookie: attacker can't read cookie to echo in custom header Origin/Referer check: server sees Origin: evil.example and rejects the request

Complexity & correctness

Cost of the attack. Constructing a CSRF attack is O(1) work for the attacker per victim: one HTML page with an auto-submitting form or a single `` tag. No brute force, no code execution on the target, no interaction beyond a page visit. This asymmetry — trivial attacker cost versus potentially severe victim damage (fund transfer, account takeover, data deletion) — is why CSRF was historically rated as high-impact despite its conceptual simplicity.

Cost of the synchronizer token defense. Per request: generate one random token (O(1) using a cryptographically secure random number generator), store or bind it to the session (O(1) with a hash map keyed by session ID), and compare on submission (O(n) in token length, effectively O(1) for fixed-length tokens, ideally using constant-time comparison to avoid timing side channels). Space cost is O(1) per active session — a single extra token field.

Correctness argument for the token defense. The scheme is correct if and only if two properties hold: 1. Unpredictability — the token is drawn from a space large enough, and randomly enough, that an attacker cannot guess it. A 128-bit token drawn from a cryptographically secure random source has guessing probability `2^-128` per attempt, which is negligible under any realistic attempt budget. 2. Unreadability by the attacker — the attacker cannot obtain the token through any channel. This depends entirely on the same-origin policy from The Web Security Model: script on `evil.example` cannot read the DOM or response body of a page served by `bank.example`, so it cannot extract a hidden form field's value. If the target site has an Cross-Site Scripting vulnerability, this guarantee collapses — XSS lets the attacker's script run inside the target's origin and read the token directly, which is why CSRF tokens are not a substitute for fixing XSS. Given both properties, any request lacking the correct token is provably not a replay of a legitimate form submission from the site itself (up to the negligible guessing probability), which is exactly the notion of "intent" the defense formalizes.

Correctness argument for `SameSite`. This defense is enforced entirely by the browser and requires no server-side secret. Its guarantee: under `Strict`, the cookie is included if and only if the top-level document's site matches the cookie's registered domain, for every request. Under `Lax`, the cookie is additionally excluded from cross-site requests unless the request is a top-level navigation using a "safe" HTTP method (effectively GET). This closes the ambient authority gap directly at its source — no cookie, no forged authentication — rather than requiring the server to prove intent after the fact.

Honest tradeoffs.

  • Synchronizer tokens require server-side session state (or careful stateless design), add a hidden field to every form, and must be regenerated per session or per request depending on the desired strength versus usability tradeoff (per-request tokens defeat some replay attacks but complicate back-button navigation and multi-tab use).
  • Double-submit cookies avoid server state but are weaker if an attacker can set cookies on the target domain — for instance, via a subdomain takeover or a cookie injection bug, so-called "session fixation"-style attacks — since the attacker could set both the cookie and a matching header value.
  • `SameSite=Lax` is not airtight: it still allows cookies on top-level GET navigations, so GET-based state-changing endpoints remain exposed; this is one more reason state-changing actions must never be idempotent-unsafe GET requests.
  • Relying solely on `Referer`/`Origin` header checks is fragile because some privacy tools strip these headers, and a permissive fallback ("if header missing, allow") reopens the hole.

Worked example

Consider a simplified social media API endpoint that lets a logged-in user change their account email, tracked through three defense configurations.

StepNo defenseSynchronizer tokenSameSite=Lax cookie
1. Victim logs into `social.example`Session cookie `sid=9f8a...` setSame, plus server generates `csrf=71bd...` bound to sessionCookie set with `SameSite=Lax`
2. Victim visits `evil.example` in another tabBrowser loads attacker's auto-submit formSameSame
3. Attacker's form targets `social.example/change-email` with `POST`Form has fields `email=attacker@evil.example`Form lacks a valid `csrf` field (attacker cannot read it)Browser refuses to attach `sid` cookie to this cross-site POST
4. Browser fires the requestIncludes `Cookie: sid=9f8a...`Includes `Cookie: sid=9f8a...` and attacker's guessed/blank `csrf` fieldRequest sent with no `Cookie` header for `social.example`
5. Server processesSees valid session → executes email changeSees valid session, but `csrf` mismatch → `403 Forbidden`Sees no session cookie → treated as anonymous → `401 Unauthorized`
6. OutcomeAccount compromised: attacker now controls password reset via new emailAttack blockedAttack blocked

Now suppose the site also has a reflected Cross-Site Scripting bug on a search page. Trace what changes:

StepSynchronizer token, with XSS present
1Attacker lures victim to `social.example/search?q=`
2Injected script runs inside `social.example`'s origin, in the victim's authenticated session
3Script performs `fetch("/account")`, reads the rendered page, extracts the hidden `csrf` token from the DOM
4Script then performs `fetch("/change-email", {method:"POST", body: "email=attacker@evil.example&csrf=" + stolenToken})`
5Server sees a matching, valid token → request succeeds
6Attack succeeds — CSRF defense is bypassed entirely because XSS broke the same-origin assumption the token relied on

This second trace is the crucial lesson: CSRF tokens are only as strong as the same-origin guarantee they depend on. This is why the chapter ordering matters — CSRF defenses must be understood as sitting on top of the guarantees established by The Web Security Model and can be undermined by any Cross-Site Scripting flaw on the same origin.

Common pitfalls & traps

PitfallWhy it fails
Using GET for state-changing actions``, ``, or prefetch tags trigger GET automatically; no user interaction needed, and `SameSite=Lax` still allows top-level GET navigation
Checking `Referer`/`Origin` only, with a permissive fallback when the header is absentSome proxies, extensions, and privacy modes strip these headers; "fail open" on missing header reopens the vulnerability
Storing the CSRF token in a cookie only, without a matching body/header checkDouble-submit relies on the comparison, not the cookie alone — a cookie-only check is just the same broken model with an extra name
Using the same static CSRF token forever, never tied to sessionA token leaked once (via logs, screenshots, referer leakage) remains valid indefinitely
Believing `SameSite=Lax` fully replaces tokens`Lax` still permits cookies on top-level cross-site GET navigations; state-changing GET endpoints remain exposed
Assuming CSRF tokens protect against XSS-enabled attackersA same-origin script can simply read the token from the DOM; tokens defend against forged cross-origin requests, not code running in-origin
Comparing tokens with a non-constant-time string comparisonOpens a timing side channel that can, in principle, leak the token byte by byte

Pioneers & history

CSRF was not invented by a single named researcher in the way public-key cryptography has a Diffie or an RSA; it emerged from the broader community documenting web application vulnerabilities as the web moved from static pages to stateful, cookie-authenticated applications. The term itself was coined and popularized through mailing-list discussions and early 2000s security research, well before formal standardization of defenses.

Origins · 1 minPeter Watkins and how this was discovered
PW
Peter Watkins
active early 2000s · early documented CSRF discussions on Bugtraq and security mailing lists, helping coin the term and catalog the "confused deputy" framing borrowed from Norm Hardy's earlier operating-systems work

The "confused deputy" concept, foundational to understanding CSRF, predates the web entirely — it comes from classical The Web Security Model-adjacent operating systems research on capability confusion, where a privileged program is tricked into misusing its authority on behalf of an unprivileged caller. CSRF is the confused-deputy problem instantiated with the browser as the deputy and cookies as the misused authority.

Origins · 1 minhow this was discovered
History
2001
— early public documentation of "session riding" and cross-site request forgery on security mailing lists.
2007
— synchronizer token pattern formalized and widely recommended by OWASP guidance for framework builders.
2008
— double-submit cookie pattern documented as a stateless alternative for large-scale services.
2016
— the `SameSite` cookie attribute proposed as an IETF draft to give browsers a structural defense.
2020
— major browsers ship `SameSite=Lax` as the default for cookies lacking an explicit attribute, closing the gap for a large class of legacy applications.

Where this matters

Every mainstream web framework — Django, Rails, Spring, ASP.NET, Express with middleware — ships built-in CSRF token generation and verification, because the vulnerability class is common enough that leaving it to individual developers proved unworkable. Payment processors and banking applications layer synchronizer tokens, `SameSite` cookies, and re-authentication ("step-up" confirmation) for high-value actions like adding a payee, precisely because a single defense layer failing should not mean total compromise. Browser vendors treat `SameSite` as infrastructure-level security, not an opt-in feature, which is why the 2020 default change affected the entire web simultaneously rather than requiring individual sites to update. Content management systems, forum software, and single-page application backends all bake CSRF protection into their default request-handling pipeline, often via a middleware layer that intercepts every state-changing request before it reaches application logic. The broader lesson — that a security check protecting one boundary (server trusting a cookie) can be silently undermined by a failure at another boundary (XSS breaking the same-origin assumption) — is a recurring theme that reappears throughout Security Engineering and the Human Factor, where technical correctness of a single mechanism is never sufficient without considering how mechanisms interact, fail, and get misconfigured by real engineers under real deadlines.

Ask the tutor: How would you design a CSRF defense for a single-page application that authenticates using a JWT stored in local storage instead of cookies?

Is CSRF the same vulnerability as Cross-Site Scripting?

No — they are distinct, though related, vulnerability classes. Cross-Site Scripting lets an attacker inject and execute code inside the victim's browsing session on the target origin, breaking the same-origin policy directly. CSRF requires no code execution on the target at all; it only exploits the browser's automatic attachment of credentials to cross-site requests. Crucially, XSS can be used to bypass CSRF defenses, since script running in-origin can read tokens the CSRF protection relies on being secret. They require different fixes: input sanitization and output encoding for XSS, and intent-proving tokens or `SameSite` cookies for CSRF.

Does using HTTPS prevent CSRF?

No. HTTPS protects the confidentiality and integrity of data in transit between the browser and server, preventing eavesdropping and tampering by network attackers. CSRF is not a network-layer attack — the forged request travels over a perfectly legitimate, correctly encrypted HTTPS connection from the victim's own browser. The problem is that the request itself was never intended by the user, which HTTPS has no mechanism to detect. CSRF defenses must operate at the application layer, verifying intent rather than transport security.

Are CSRF tokens necessary if I only use JSON APIs with fetch() instead of HTML forms?

Often less necessary but not automatically safe. If your API requires a custom header like `X-Requested-With` or `X-CSRF-Token` for every request, an attacker's page cannot set arbitrary custom headers on a simple cross-site request without triggering a CORS preflight, which your server can then reject based on the `Origin` header. However, if the API accepts `Content-Type: text/plain` or form-encoded bodies without custom headers, a forged form or fetch can still succeed. The safest posture combines `SameSite` cookies, explicit `Origin` checks, and avoiding "simple request" content types for state-changing endpoints.

Why doesn't checking the Referer header alone solve CSRF?

It mostly works but has known gaps. Some browser configurations, corporate proxies, and privacy extensions strip the `Referer` header for legitimate cross-origin navigations, and a naive implementation that "allows the request if the header is missing" reopens the vulnerability entirely. The `Origin` header is generally more reliable for POST requests since browsers send it consistently for cross-origin submissions, but relying on any single header as the sole defense violates the defense-in-depth principle — headers can, in rare misconfigurations, be spoofed by non-browser clients if the server doesn't also require a token.

Can CSRF happen through mobile apps or only in browsers?

CSRF as classically defined depends on the browser's automatic cookie attachment and same-origin policy semantics, so a pure native mobile app making its own HTTP requests with explicitly managed tokens is not vulnerable in the same way. However, mobile apps that embed web views, or that rely on shared cookie stores with an in-app browser, can reintroduce CSRF-like conditions. Any client that automatically attaches ambient credentials to requests triggered by untrusted content faces an analogous risk, which is why the underlying principle — never trust ambient credentials as proof of intent — generalizes beyond browsers.

What's the difference between the synchronizer token pattern and the double-submit cookie pattern?

The synchronizer token pattern requires server-side state: the server generates a token, stores it against the session, and checks the submitted value against that stored copy. The double-submit cookie pattern avoids server-side storage by placing the token in both a cookie and a request field, relying on the attacker's inability to read the cookie's value to forge a matching field. Double-submit is easier to scale across stateless or load-balanced servers, but is slightly weaker if an attacker can inject cookies onto the target domain, for example through a subdomain vulnerability.

Does SameSite=Strict eliminate the need for any other CSRF defense?

Not entirely. `SameSite=Strict` prevents the cookie from being sent on essentially any cross-site request, including top-level navigations, which can break legitimate use cases like clicking an email link that should land the user in a logged-in state. It offers very strong protection but requires careful UX tradeoffs. Additionally, browsers that predate full `SameSite` support, misconfigured subdomains, and non-cookie authentication schemes (like tokens manually attached via JavaScript) fall outside its protection, so token-based defenses remain the recommended baseline for high-value actions.

Before this

Where this leads