Why is the double-submit CSRF pattern a defence at all, given the server stores no token and only compares a cookie against the request?
answer
- no server memory involved
- two halves must match
- the user agent attaches one half
- hostile page cannot read your cookie
- an author-set header needs permission
basics
~20 sThe defence rests on what a document served by another site cannot do: read your cookie, or attach an author-defined header to a request it emits. Matching the two halves is evidence your own page authored the request.
solid answer
~50 sThe server puts an unpredictable value in a cookie and requires the same value back on every unsafe request, carried in a hidden form field or in a request header the page's own code sets. It compares the two and keeps nothing. The comparison means something because a document served by another site cannot read the cookie, so it cannot guess the value to submit; and it cannot put an author-defined header on a form submission at all, because a form sends only the headers the user agent chooses. The cookie half proves nothing on its own — the user agent attaches it to any request bound for your site, which is the whole reason forgery works. The *submitted* half is the evidence. Statelessness is the appeal: any instance in a pool can judge the request from the request.
code
http · 8 linesPOST /points/transfer HTTP/1.1
Host: flights.brand.example
Cookie: session=8f41c0...; csrf=9f2c7a41b0d3
X-CSRF-Token: 9f2c7a41b0d3
Content-Type: application/x-www-form-urlencoded
Content-Length: 34
to=partner-cafe&points=2500go deeper
Remember the shape: one value goes out in a cookie, the same value must come back in the request, and the server compares them without storing anything.
Be able to say which party puts each half on the request, and therefore why the cookie half is not evidence. Name the two things a document from another site cannot do.
Show what the check must do on absence and mismatch, and be honest that the comparison proves equality rather than ownership by this session.
Frame it as a state-versus-trust trade: you removed a shared store and took on an assumption about who can write into your cookie namespace.
## What the pattern actually does The server generates an unpredictable value, returns it to the browser in a `Set-Cookie` response header, and then requires the **same value** to come back on every state-changing request. The returning copy travels one of two ways: as a hidden field inside an `application/x-www-form-urlencoded` body, or as a request header whose name the application chooses and whose value the page's own code sets. On arrival the server reads both copies, compares them, and accepts or rejects. Nothing is written down. There is no session entry holding the issued value, no shared cache, no sticky routing. That is the entire appeal of the pattern and the reason teams reach for it: **any instance behind a load balancer can judge the request from the request alone.** ## Why comparing a value against itself means anything It looks circular — the attacker supplies both halves, so why can he not supply two matching ones? Because the two halves are placed on the request by different parties: | Half of the pair | Who puts it on the request | What it proves on its own | |---|---|---| | The copy in the `Cookie` request header | The user agent, automatically, for any request bound for your site | Nothing at all | | The copy in the body field or the author-set header | The markup or script of whichever document composed the request | That its author knew the value | So the question collapses to: **can a document the victim's site never served learn the value?** Two facts say no: - **It cannot read the cookie.** Cookie values are readable only to script running in a document belonging to that site. A hostile page can cause a request to be sent to your site; it cannot look at what the browser will attach. - **It cannot forge an author-defined header on a form submission.** An HTML form lets its author pick the target, the method and the body encoding — never an arbitrary header field. A scripted cross-origin request *can* ask for an extra header, but that request only proceeds after the target origin grants permission for it, and a site defending itself does not grant that to strangers. Note what is *not* removed: ambient authority. The session cookie still rides along on the forged request exactly as before. The double-submit pattern does not stop the request being sent — it gives the server evidence about **who composed** it. ## What the pattern requires to hold 1. **Unpredictability.** A counter, a user identifier or a timestamp is guessable, and a guessable value converts the whole scheme into decoration. The value must come from a cryptographically strong source. 2. **Readability by your own page.** The page has to read the value to echo it, so this cookie cannot be hidden from script the way a session cookie usually is. The two cookies have different jobs and different attributes; do not copy one's settings onto the other. 3. **Failing closed.** Absent submitted copy, absent cookie, or a mismatch must all reject. A check that accepts when the submitted copy is missing accepts every forgery, because a forgery is exactly the request that has no submitted copy. 4. **Coverage of unsafe methods.** `GET`, `HEAD`, `OPTIONS` and `TRACE` are the safe methods in the RFC 9110 sense and are conventionally exempt — which is only sound if your service really does not change state on them. ## Where it sits relative to keeping the value on the server Holding the issued value in server-side state binds it to a particular session by construction: the server looks the session up and compares. The double-submit pattern buys away that lookup, and the price is that the pair is **self-referential**. The comparison proves "these two strings match", not "this value belongs to this session". As long as only your own servers can write into your cookie namespace, the difference is invisible. The moment something else can write a cookie your host will accept, the difference is the entire security of the scheme — which is why the pattern is normally deployed in a session-bound, cryptographically signed form rather than the plain one described here. ## The reject, and what a client should make of it A failed comparison is a statement about provenance, not about credentials: the user is logged in, the session is valid, and the request is refused anyway. `403 Forbidden` is the honest answer; sending the user to log in again teaches both the user and the client code the wrong lesson, because logging in again changes nothing about where the request came from.
- Why does the copy in the `Cookie` request header prove nothing by itself?Because the user agent attaches it to every request bound for your site, whoever caused that request to be sent. A hostile document's forged submission arrives carrying the cookie exactly as your own page's submission would. Only the copy placed on the request by its author distinguishes the two.
- Does the plain form of the pattern tie the value to a particular user's session?No. It checks only that the two copies are equal, so any pair of matching strings passes, whoever minted them. The value is bound to itself rather than to the session, and every weakness of the plain pattern follows from that one property.
- Does it matter whether the submitted copy travels as a form field or as an author-set header?It changes what an attacker must do. A field in an `application/x-www-form-urlencoded` body is trivially settable by any document once the value is known. An author-set header cannot be produced by a form submission at all, so a hostile page needs script plus the target's own permission for that header field.
saying these in an interview costs you the question
- Says the cookie arriving proves the request came from our own page.
- Believes a hostile page cannot submit a hidden field it knows the value of.
- Thinks the server must remember the issued value for the comparison to work.
- Says the value only needs to be unique per user, not unpredictable.
- Hides the double-submit cookie from script and still expects the page to echo it.
- Treats a failed comparison as a login failure rather than a provenance failure.