Skip to content
pwnsy
web-securityintermediate#csrf#web-security#owasp#session-security#appsec

CSRF Explained: SameSite Cookies & Anti-CSRF Tokens

How cross-site request forgery rides a logged-in user's cookies to force state-changing requests, and how SameSite, tokens, and double-submit stop it.

Log into your bank in one browser tab, then open a malicious page in another, and that second page may be able to move your money without ever touching your password. It does not need to. Your browser is already authenticated to the bank, and it will happily attach your session cookie to a request the attacker's page tells it to make. That is cross-site request forgery, CSRF, and for years it was one of the most common serious flaws on the web.

The vulnerability comes from a design decision baked into how browsers handle cookies. A cookie set by a site is sent back to that site on every request, automatically, regardless of which page triggered the request. That behavior is convenient for keeping you logged in. It is also exactly what CSRF abuses.

The core idea

CSRF exploits the gap between authentication and intent. When your browser sends a request to a site, the site checks that you are authenticated by reading your session cookie. What the site cannot see, without extra effort, is whether you actually meant to send that request or whether some other page made your browser send it.

Consider an application that changes a user's email address with a simple form submission:

POST /account/email HTTP/1.1
Host: bank.example
Cookie: session=abc123

[email protected]

An attacker cannot read your session cookie. They do not need to. They build a page that submits this exact request and get you to visit it. The moment you load their page, your browser fires the request at bank.example, attaches your valid session cookie because that is what browsers do, and the bank processes the email change as if you asked for it. The attacker has hijacked your identity for one action without ever stealing a credential.

The classic delivery is an auto-submitting form hidden on any page the attacker controls:

<form action="https://bank.example/account/email" method="POST">
  <input type="hidden" name="email" value="[email protected]">
</form>
<script>document.forms[0].submit()</script>

No interaction beyond visiting the page. For requests that use GET, it can be even simpler: an <img> tag whose src points at the state-changing URL is enough.

What CSRF can and cannot do

CSRF forces the victim's browser to send a request. It does not let the attacker read the response. The same-origin policy still stops the attacker's page from reading what bank.example sends back. This shapes what CSRF is good for.

PropertyCSRF
Reads sensitive dataNo, the response is invisible to the attacker
Performs state-changing actionsYes, this is the whole point
Needs the victim's passwordNo, it rides an existing session
Needs the victim to be logged inYes, at the target site
Works against bearer-token APIsOnly if the token is sent automatically, like a cookie

Because CSRF is blind, its value lives entirely in side effects: changing an account email so a password reset can be captured later, transferring funds, adding an attacker's key to an account, changing a delivery address, or toggling a security setting. On MITRE ATT&CK the broader pattern of abusing valid browser sessions relates to techniques around session and web abuse, but CSRF itself is a client-trust problem specific to how cookies are sent.

Why cookies are the root cause

The reason CSRF exists at all is that cookie authentication is ambient. The browser decides to send the session cookie based on the destination of the request, not on which code initiated it. Any page, anywhere, can cause your browser to make a request to the target, and the cookie tags along.

Bearer tokens sent in an Authorization header behave differently. The browser does not attach them automatically. An attacker's page cannot set that header on a cross-site request to another origin, so a pure header-token API is not vulnerable to classic CSRF. The catch is that many single-page apps store tokens in cookies for convenience, which quietly reintroduces the problem.

CSRF and XSS are not the same bug

CSRF makes a victim's browser send a request without reading the reply. Cross-site scripting runs attacker code inside your origin and can read anything the user can. If an attacker has XSS on your site, no CSRF token saves you, because their script runs in your context and can read the token. Fix XSS first, then treat CSRF defense as protection for everything else.

The defenses that work

Every effective CSRF defense proves one thing: that the state-changing request genuinely came from your own application and not from a third-party page. There are two mainstream ways to prove it.

Anti-CSRF tokens (synchronizer token pattern)

The server generates a random, unpredictable token, ties it to the user's session, and embeds it in every form:

<input type="hidden" name="csrf_token" value="9f2a...c71">

When the request comes back, the server checks that the submitted token matches the one bound to the session. An attacker's page cannot read the token (same-origin policy blocks it) and cannot guess it (it is random), so it cannot forge a valid request. This is the strongest and most widely used control. Most web frameworks ship it built in and enabled.

When you do not want to store per-session tokens server-side, the double-submit pattern offers a stateless option. The server sets a random value in a cookie and the application also sends that same value as a request header or form field. On each request, the server checks the two match. An attacker cannot read or set the cookie value for your origin, so they cannot make the two sides agree. It is weaker than a synchronizer token in some edge cases (subdomain and cookie-scoping issues), so pair it with other controls.

SameSite cookies

The SameSite cookie attribute tells the browser when to send a cookie on cross-site requests. It is the most important modern layer:

ValueBehavior
StrictCookie is never sent on any cross-site request, including top-level navigation
LaxCookie is sent on top-level navigation (a link click) but not on cross-site POST, images, or iframes
NoneCookie is always sent cross-site, and must be paired with Secure

Modern browsers default to Lax when no attribute is set, which alone blocks a large share of cross-site POST CSRF. Set it explicitly. Use Strict for the most sensitive session cookies, accepting that a user following a link from an external site starts logged out.

Other useful checks

  • Verify the Origin and Referer headers for state-changing requests and reject those from unexpected sources. This is a solid secondary check because these headers are set by the browser and cannot be forged by an attacker's page.
  • Require re-authentication or a step-up (password, or a fresh factor) for the highest-value actions, so a single forged request is not enough. See our two-factor authentication guide for how step-up factors fit in.
  • Avoid using GET for anything that changes state. A state-changing GET can be triggered by an image tag and is far easier to smuggle.
Layer tokens under SameSite

Treat SameSite cookies and anti-CSRF tokens as two layers, not a choice. SameSite is a browser-enforced default that covers a lot of ground but depends on the user's browser behaving and on your cookies being scoped correctly. Tokens are enforced by your own server on every request. Ship both, and a gap in one is covered by the other.

How to defend against CSRF

  1. Enable your framework's built-in CSRF protection and confirm it covers every state-changing route. Most modern frameworks include the synchronizer token pattern; the failure mode is a custom endpoint that bypasses it.
  2. Set SameSite explicitly on session cookies, preferring Lax as a baseline and Strict for high-value sessions.
  3. Never change state on GET. Reserve GET for reads and require POST, PUT, PATCH, or DELETE for anything with side effects.
  4. Validate Origin and Referer on state-changing requests as a secondary control and reject mismatches.
  5. Require step-up authentication for critical actions like changing email, password, or payment details, so no single forged request completes them.
  6. Do not store bearer tokens in cookies unless you also apply full CSRF protection to them, because that reintroduces the ambient-credential problem the token model was meant to avoid.

Attackers rarely stop at one bug. A forged email change today can seed an account takeover tomorrow, and the delivery often arrives inside a phishing page or a malicious document. Our Exploit Intelligence dashboard tracks which classes of web flaw are actively being weaponized, so you can see where CSRF-adjacent account-takeover chains are showing up in the wild rather than guessing.

A forged request, step by step

Tracing one attack from setup to effect shows exactly where the trust breaks down.

The attacker starts by studying the target application to learn which request performs a valuable state change. Say the application changes an account's recovery email with a POST to a known path, carrying the new address as a form field, and authenticating the user purely by the session cookie. That last property, cookie-only authentication on a state-changing endpoint, is the precondition the whole attack depends on.

Next the attacker builds a page that reproduces that request. A hidden form on the page targets the application's email-change path, carries the attacker's address in a hidden field, and submits itself the moment the page loads. Nothing on the page looks unusual to a casual visitor, and the form is invisible.

Then the attacker delivers the page to a victim who is logged into the target. The delivery is ordinary: a link in an email, a post on a forum, an advertisement, or a compromised page the victim already trusts. The victim does not need to interact beyond arriving, because the form submits on load.

When the page loads in the victim's browser, the browser fires the request at the target application. Because the request goes to the target's origin, the browser attaches the victim's session cookie automatically, exactly as it does for every legitimate request to that site. The cookie is valid, the session is real, and the application has no built-in way to tell that the victim did not intend this request.

The application processes the email change as though the victim asked for it. The attacker never saw the response and never learned the session cookie, and none of that was necessary. With the recovery email now pointing at the attacker, a password reset can be triggered and captured later, converting a single forged request into a full account takeover. The chain shows why CSRF is dangerous despite being blind: the value is in the side effect, and the side effect can seed the next step.

Detection signals

CSRF is a design gap rather than a noisy exploit, so much of the defensive value is in reviewing endpoints and watching for the patterns that indicate exposure or attempts.

  • State-changing endpoints without a token check. The clearest audit signal is any route that performs a side effect while accepting a request that carries no anti-CSRF token and relies only on the session cookie. These are the endpoints an attacker looks for.
  • State changes reachable over GET. A side effect triggered by a GET request can be fired by an image tag and is the easiest kind of CSRF to deliver. Any state change on GET is a finding on its own.
  • Cross-origin state requests without a matching Origin. For state-changing requests, an Origin or Referer header pointing at a site other than your own is a strong sign of a forged request, since a legitimate request originates from your application.
  • Bearer tokens stored in cookies. An API that authenticates with a token but keeps that token in a cookie has quietly reintroduced ambient credentials, which returns the CSRF exposure the token model was meant to remove.
  • Missing or permissive SameSite attributes. Session cookies set without SameSite, or set to None without a strong reason, widen the window for cross-site delivery and are worth flagging in review.
  • Spikes of identical state-changing requests with mismatched Origins. In logs, a burst of the same email-change or setting-toggle request arriving with foreign or absent Origin headers can indicate an active campaign.

The defenses compared

DefenseHow it proves intentServer state neededMain limitation
Synchronizer tokenSecret per-session token embedded in the formYes, token bound to sessionMust cover every state-changing route
Double-submit cookieCookie value must match a request fieldNo, statelessSubdomain and cookie-scoping edge cases
SameSite cookiesBrowser withholds cookie on cross-site requestsNo, browser-enforcedDepends on browser and correct cookie scope
Origin and Referer checkHeader must match the application's originNoSome clients strip or omit the header
Step-up re-authenticationFresh credential required for the actionDepends on methodAdds user friction, reserve for high-value actions

The table shows why these are layers rather than substitutes. The synchronizer token is the strongest single control and is enforced by your own server, while SameSite is a broad browser-enforced default that depends on the client behaving and on correct cookie scoping. Shipping a token check under a SameSite default means a gap in one is covered by the other, which is the posture to aim for.

Common misconceptions

"SameSite=Lax by default means CSRF is solved." Lax blocks a large share of cross-site POST CSRF, and it is genuine progress. It still relies on the user's browser enforcing it and on your cookies being scoped correctly, and it does not cover every case. Treat it as a strong default under an explicit token check, not as a complete replacement.

"An HTTPS site is safe from CSRF." HTTPS encrypts the connection and does nothing about who caused the request. A forged request travels over HTTPS just as a legitimate one does, carrying the session cookie all the same. Transport security and CSRF defense are unrelated properties.

"CSRF lets an attacker read my data." It does not. The same-origin policy still stops the attacker's page from reading the response, so CSRF is blind. Its value is entirely in the side effect it forces, such as changing a setting or moving money, which is why blind does not mean harmless.

"Only POST requests are vulnerable." Any request that changes state is a candidate, and state-changing GET requests are the easiest to exploit because an image tag can trigger them. Keeping GET strictly read-only removes that entire, easy delivery path.

"A token in a cookie is a CSRF defense." A token that the browser sends automatically, as it does a cookie, offers no protection, because the attacker's forged request carries it too. The double-submit pattern works only because the application also copies the value into a header or field that a cross-site page cannot set.

How the defenses developed

CSRF drew serious attention as applications moved real actions, payments, account changes, administrative controls, onto the web while still authenticating every request with an ambient session cookie. The root behaviour, a browser attaching a site's cookie to any request bound for that site regardless of what caused the request, was convenient for keeping users logged in and was exactly what the attack abused.

The first durable answer was the synchronizer token pattern, a secret unpredictable value tied to the session and embedded in every form, checked on each state-changing request. Because a cross-site page cannot read the token or guess it, it cannot forge a valid request. Frameworks absorbed this control and began shipping it enabled by default, which turned CSRF from a common finding into a mostly solved problem for teams that used their framework's protection.

The double-submit cookie followed for cases where server-side token storage was undesirable, trading a little robustness for statelessness. The larger shift came from the browsers themselves with the SameSite cookie attribute, which lets a site tell the browser to withhold a cookie on cross-site requests, and with modern browsers defaulting to Lax when no attribute is set. That default quietly removed a large share of classic CSRF across the web. The current posture combines a browser-enforced SameSite default with a server-enforced token check, so neither the browser nor the server is the single point of failure.

Frequently asked questions

Is CSRF still relevant with modern browsers defaulting to SameSite Lax? Yes. The Lax default removed a large share of classic cross-site POST CSRF, which is real progress, but it depends on the user's browser enforcing it and on your cookies being scoped correctly, and it does not cover every case. Endpoints that change state still need an explicit anti-CSRF token, with SameSite as a strong default underneath rather than the only control.

What is the difference between CSRF and XSS? CSRF makes a victim's browser send a request without reading the reply, riding an existing session. Cross-site scripting runs attacker code inside your origin and can read anything the user can, including a CSRF token. If an attacker has XSS on your site, no CSRF token protects you, because their script runs in your context. Fix XSS first, then rely on CSRF defense for everything else.

Are single-page applications and APIs vulnerable to CSRF? A pure API that authenticates with a bearer token in an Authorization header is not vulnerable to classic CSRF, because the browser does not attach that header automatically to cross-site requests. The exposure returns the moment the app stores its token in a cookie for convenience, which reintroduces the ambient credential. If you use cookies, apply full CSRF protection.

Does CSRF work if the victim is not logged in? No. CSRF rides an existing authenticated session at the target site. If the victim has no active session, there is no cookie for the browser to attach, and the forged request arrives unauthenticated. The attack depends on the victim being logged in at the moment they load the attacker's page.

Can I rely on the Referer header alone to stop CSRF? It is a useful secondary control, since the browser sets Origin and Referer and a cross-site page cannot forge them, but it is not sufficient alone. Some clients and privacy settings strip or omit the header, which would either weaken the check or break legitimate requests if you reject every missing header. Use it alongside tokens and SameSite, not instead of them.

Why is changing state on a GET request dangerous? A GET request can be triggered simply by loading a resource, for example an image tag whose source points at the state-changing URL. That makes a state-changing GET the easiest possible CSRF to deliver, since it needs no form and no script. Reserving GET for reads and requiring POST, PUT, PATCH, or DELETE for side effects removes that path entirely.

What is the single most important CSRF defense to get right? Enabling your framework's built-in token protection and confirming it covers every state-changing route. Most modern frameworks ship the synchronizer token pattern enabled, and the common failure is a custom endpoint that bypasses it. Pair that with an explicit SameSite setting and read-only GET, and the attack has nothing to grab.

CSRF is old, well understood, and completely preventable. The controls are cheap, they ship in every serious framework, and the only real failure is forgetting to apply them to one endpoint. Turn them on everywhere state changes, set SameSite deliberately, and keep GET read-only, and the attack simply has nothing to grab.

Sources & further reading

Sharetwitterlinkedin

Related guides