What A Get Request Taught Me About Csrf
A single hidden CSRF token stops most forged requests. A GET endpoint that never got one taught me why the request method matters as much as the token itself.
The last post closed Meridian Bank's transfer hole with a single session-scoped token. There's a sharper version of that pattern worth knowing, and an edge case where a token alone still isn't enough.
Two Secrets, Not One Secret in Two Places
The token pattern from the last post works by storing one token and checking it against one submitted value. A stronger variant splits that into two separate random identifiers entirely.
Think of it like a coat check at a busy restaurant. You get handed a stub, and a matching stub stays pinned to your coat. Neither one is a copy of the other, they're two separate pieces of paper that happened to be generated as a pair. Lose your stub, and having someone else's coat doesn't help you claim it, because the two were never the same secret split in half.
That's the double-token pattern. One random identifier lives in the cookie, a completely separate random identifier lives in the hidden form field, and neither is derived from the other.
If an attacker somehow gets hold of the cookie value, they still don't automatically get the form token, and the reverse holds too. A single leak compromises one secret, not both, because there was never a way to compute one from the other in the first place. Deriving the form token from the cookie would quietly collapse this back into a single secret wearing two different outfits.
Where Even Tokens Stop Protecting You
Fictional app, this time a small social platform called Ripple. Denver signs up, and posting a status update goes through a POST request, protected exactly the way Meridian's transfer endpoint is now.
Ripple also had a GET endpoint that liked a post, added early, back when the team assumed GET requests were harmless by definition. It never got the same token treatment as the POST routes, because nobody thought a "like" needed protecting.
That cookie happened to be set with SameSite=None, left over from an embeddable Ripple widget the product team had shipped for partner sites. The exact reason None still exists at all, covered two posts back: something needed the session to follow the user across a boundary Lax would otherwise block.
Denver later visits a completely unrelated recipe blog. Static page, no login, nothing about it looks remotely connected to Ripple.
Buried in the page's CSS was a background image reference pointing straight at Ripple's like endpoint. The image never renders, because the response isn't actual image data. The request still fires, Denver's Ripple cookie still attaches, and somewhere on Ripple, a post Denver never saw gets a like he never gave it.
Why a Token Can't Save a GET Request Here
The instinct after seeing this is to ask why the token pattern didn't just apply to this endpoint too. It could have, and should have. But it's worth understanding exactly why GET makes this so much easier to get wrong in the first place.
A GET request has no body. Anything passed along with it becomes a query parameter, tacked onto the URL itself. And query parameters cannot be encrypted, even over HTTPS, because the browser has to read the URL in cleartext before it can even figure out where to send the request.
ExpandGET requests have no body, so anything passed becomes a query parameter, and query parameters land in browser history, server logs, and referrer headers, unencrypted, regardless of HTTPS.
So a CSRF token tacked onto a GET request as ?csrf=abc123 isn't hidden by HTTPS the way a POST body's token is. It sits in browser history. It sits in server access logs. It gets forwarded in the referrer header to whatever site the user clicks through to next. A token that leaks that easily barely qualifies as a secret anymore.
The Rule This Teaches: No PII in a Query Param, Ever
That same root cause reaches past CSRF tokens into a much broader rule worth internalizing: never put personally identifying information in a query parameter, for any request, GET or otherwise.
Email addresses, phone numbers, anything resembling a social security number, all of it travels in cleartext through logs, browser history, and referrer headers, regardless of whether the connection itself is HTTPS. HTTPS protects the body of a request. It was never built to protect the URL sitting in front of it.
Even something as ordinary as a search filter deserves this same caution the moment the value is personal rather than generic. A product category in a query string is fine. An email address never is.
Weaker Defenses Worth Knowing About
A couple of alternative approaches come up often enough to be worth naming, mostly so you recognize their limits rather than lean on them as a primary defense.
- Referrer-based validation checks where a request claims to have come from. It's spoofable, and plenty of legitimate requests arrive with no referrer at all, since a page can suppress it entirely. It's a nice-to-have signal, never a primary defense.
- Double-signed cookies combine the cookie value and the CSRF token, then sign the combination, for cases where rendering a token into a form or header genuinely isn't feasible. It's more operational hassle than it's usually worth, and worth knowing exists rather than reaching for by default.
The Defense That Works Even When Everything Else Fails
There's one more layer worth knowing, and it works precisely because CSRF's fire-and-forget nature can't defeat it.
Multi-step confirmation for destructive, high-stakes actions. GitHub's repository deletion flow is the clearest example most developers have actually experienced: a modal warning, a second confirmation step, and finally typing the exact repository name before anything actually happens.
A single forged request cannot complete that flow. CSRF works by firing one request and never seeing the response, but a multi-step confirmation requires reading what came back at each stage and acting on it accordingly. That's exactly the one thing a CSRF attack can never do, by its own fire-and-forget nature.
This isn't purely a security control either. It protects you from yourself as much as from an attacker, the same friction that stops a forged request also stops a fat-fingered click before it deletes something irreplaceable.
What This Sets Up
Everything in this chapter rested on one assumption: the attacker could never run code on the target site itself, only get a victim's browser to visit a page they controlled elsewhere.
That assumption is exactly what the next part of this course takes away.
The Essentials
- The double-token pattern uses two separate random identifiers, one in the cookie and one in the form field, neither derivable from the other.
- A leaked cookie or a leaked form token compromises only one secret, not both, because they were never the same value split in two.
- A
GETrequest has no body, so anything passed becomes a query parameter, and query parameters land in cleartext logs, browser history, and referrer headers regardless of HTTPS. - Never put PII, email addresses, phone numbers, anything like a social security number, in a query parameter for any request.
- Referrer-based validation is spoofable and often just missing. Double-signed cookies are a fallback for when a token can't be rendered, not a default choice.
- Multi-step confirmation for destructive actions works even when every other layer fails, because CSRF's fire-and-forget nature can't read a response and act on it.
Next up: cross-origin resource sharing and cross-site scripting, the point where this course drops the one assumption CSRF depended on entirely, that the attacker could never get code running on the target site itself.
Further Reading and Watching
- Cross Site Request Forgery (CSRF or XSRF): CyberShaolin's overview covering GET-based CSRF risk alongside the standard token defense.
- Never Put Secrets in URLs and Query Parameters: a practical breakdown of exactly how query parameters leak through logs, history, and referrer headers.
Keep reading