Technical Publication

2. CSRF

When Browsers Become Deputies

A browser that automatically attaches your credentials to any request, regardless of who asked for it, isn't malfunctioning. It's doing exactly what it was built to do — the trust decision it's enforcing was just never checked against what that would mean once anyone could ask.

Cross-site request forgery exploits a specific, narrow gap: a browser sending a request will automatically attach whatever ambient credentials it holds for the destination — session cookies, in the typical case — regardless of which page, script, or site actually triggered that request. If a victim is logged into bank.example and an attacker's page, hosted anywhere else, causes the victim's browser to submit a form or fire a request to bank.example/transfer, the browser attaches the victim's session cookie exactly as it would for a request the victim intentionally initiated from the bank's own site. The server receives a request that is, from its point of view, indistinguishable from a legitimate one: valid session, valid cookie, a well-formed request to a real endpoint. What the server has no way to see is that the intent behind the request didn't come from the user at all — it came from a page the user never meant to authorize anything through.

That's the mechanism in full. No credential was stolen. No session was hijacked. The attacker never learns the cookie's value and doesn't need to. They only need the victim's browser, already authenticated, to be present in the same tab or window where the attacker's page can quietly issue a request — an auto-submitting form, an <img> tag pointed at a state-changing GET endpoint, a fetch call from a malicious script — and let the browser's own credential-attachment behavior do the rest.

The Browser as an Unwitting Agent

The framing that makes CSRF legible as one coherent thing, rather than a grab-bag of form-submission and image-tag tricks, is that the browser has been deputized. It holds the user's authority — the session cookie is, functionally, a bearer token standing in for "this request comes from someone we've already authenticated" — and it exercises that authority on behalf of whoever asks, with no mechanism of its own for verifying that the asker had any right to. A deputy who executes any order handed to them, without checking whether the order came from someone entitled to give it, isn't broken; they're doing exactly what a deputy does. The failure is that nobody built in the step where the deputy checks the order's provenance before acting on it.

This is worth sitting with because it explains why CSRF defenses all take the same shape, even though they look different on the surface: every real defense is a way of making the server (or the browser, on the server's behalf) check something about where the request came from, not just what credential it carries.

• Synchronizer tokens. The server embeds a random, unpredictable value in each legitimate form it renders and requires that value to come back with the submission. An attacker's page, unable to read the victim's authenticated session, can't obtain a valid token to include — so a forged request arrives without one, or with the wrong one, and gets rejected. This works because it adds a second, unspooled piece of state that ambient cookies alone can't reproduce.
• Custom request headers. Requiring a header like X-Requested-With on state-changing requests works as a CSRF defense specifically because setting arbitrary headers on a cross-origin request is something only JavaScript can do, and cross-origin JavaScript is itself gated by CORS — which means this defense is quietly leaning on a different trust boundary (the one the next essay covers) to do work CSRF protection was never designed to do on its own.
• Origin and Referer checks. The server inspects the Origin or Referer header and rejects requests that didn't originate from its own domain. This is a real signal — it's much harder for an attacker's page to spoof — but it degrades in specific documented cases: some legitimate cross-origin flows can suppress the Referer header entirely (via a Referrer-Policy set upstream, or a browser configured for privacy), and a server that fails open when the header is simply absent reopens exactly the hole the check was meant to close.
• SameSite cookies. Telling the browser itself not to attach a cookie on requests it can identify as cross-site removes the ambient-credential problem at its source, rather than trying to detect forged requests after the fact. This is powerful enough, and central enough to the series, that it gets its own essay next — for now, the relevant point is that it's a fix implemented at the browser layer, decades after the cookie mechanism it's patching was designed, because nobody who designed cookies originally was solving for this.

A Pattern Worth Naming Separately: Double-Submit Cookies

One variant of the synchronizer-token idea is worth pulling out on its own, because it shows up constantly in stateless API designs where keeping a server-side record of every issued token is inconvenient. In the double-submit cookie pattern, the server sets a random value as a cookie and requires the client's JavaScript to read that same value and echo it back in a request header or form field. The check on the server side is simply "does the header value match the cookie value" — no server-side token storage needed. This works as a CSRF defense specifically because of a same-origin constraint: an attacker's page, running on a different origin, cannot read the victim's cookies for the target site, so it has no way to obtain the value it would need to echo back correctly, even though the browser will still attach the cookie itself to the forged request automatically. The defense doesn't rely on the cookie being secret from the network or the server — it relies on the cookie being unreadable to a script on another origin, which is Same-Origin Policy doing quiet, load-bearing work for a mechanism that doesn't advertise its dependency on it.

The failure mode worth flagging is that double-submit cookies stop working the moment an attacker can write a cookie into the victim's browser for the target's domain — which is possible under certain subdomain configurations (a compromised or attacker-controlled subdomain can often set cookies scoped to the parent domain) or in the presence of an unrelated cookie-injection bug elsewhere on the same site. The pattern is a genuine, useful simplification over server-stored tokens; it is not a stronger guarantee than the synchronizer-token pattern, and treating it as interchangeable without checking the domain and subdomain assumptions it depends on is a common way this defense quietly stops applying without anyone noticing.

GET Requests Were Never Supposed to Change State, and CSRF Is Part of Why That Matters

A meaningful fraction of real-world CSRF findings, historically, targeted state-changing actions exposed behind GET requests — a "delete this item" link, a "confirm this action" endpoint reachable by simply visiting a URL. This matters for CSRF specifically because a plain <img src="..."> tag, an <iframe>, or a redirect can trigger a GET request with zero JavaScript and zero user interaction beyond the page loading — no click required, no form submission, nothing that even looks like the user did anything. A well-formed CSRF defense assumes the attacker needs to construct a cross-site POST, which takes a small amount of active machinery (an auto-submitting form, at minimum). An endpoint that changes state on GET removes even that minimal bar. This is one of the reasons HTTP semantics — GET as safe and idempotent, state changes reserved for POST, PUT, PATCH, DELETE — were never purely a REST-style stylistic preference; violating them measurably widens the CSRF attack surface, independent of whatever token or SameSite protection sits on top.

Why "Just Check the Cookie" Was Never Going to Hold

It's worth being precise about what actually broke down, because it isn't that cookie-based session management was designed badly. When cookies were introduced as a way to give the otherwise stateless HTTP protocol a notion of an ongoing session, the web those cookies operated in was overwhelmingly a collection of independent sites that didn't reference each other's resources in ways that mattered for authorization. A request carrying a valid session cookie really was, for practical purposes, a request the user intended — because the paths by which another site could cause your browser to issue a request to a first site, on your behalf, without your awareness, either didn't exist yet or weren't understood as an attack surface. "A request with valid cookies represents the user's intent" was a reasonable inference at the time it became the web's default behavior, not a mistake.

What changed wasn't the cookie mechanism — it was everything built on top of it. Third-party content embedding became routine. Cross-site linking, cross-site forms, and eventually cross-site scripting via ordinary JavaScript became the fabric of how the web worked, not an edge case. The assumption baked into "cookies get sent automatically" scaled fine when sites mostly didn't interact; it became a live vulnerability once they did, constantly, by design. Nobody made a decision that broke — the conditions the original decision depended on changed underneath it, silently, over roughly a decade, while the decision itself never got revisited because it wasn't framed as a decision in the first place. It was framed as how cookies work.

That's the organizational point worth landing here, once: the trust decision — "a request carrying valid session credentials reflects the user's actual intent" — was made implicitly by the design of stateful HTTP sessions, inherited by every application that used cookies for auth afterward, and never assigned to anyone as a thing to keep checking against how the web actually worked. Browser vendors eventually stepped in with SameSite defaults; application developers eventually adopted synchronizer tokens as a near-universal default in serious frameworks. Both of those are real fixes. Neither of them happened because someone was assigned to own "is ambient credential attachment still safe" as an ongoing question — they happened years after the gap between the assumption and reality had already produced a long, well-documented string of exploited cases across the industry.

The trust boundary here is the line between a browser holding a user's credentials and a browser knowing whose purpose it's currently serving — and for most of the web's history, and on a meaningful fraction of it still today, the answer to who owns keeping those two things distinct is the application developer who remembered to add a token, not any structural guarantee that someone would.