CSRF and SameSite: why a cookie is not consent
A cookie proves who is signed in; only a token or the browser's own headers prove your page sent the request.
Predict first
Before you read: predict
ExamHall's session cookie is SameSite=None; Secure, because universities open the exam inside their own learning portals. POST /attempts/123/submit submits Mia's attempt; a leftover route also answers GET on that path and submits it too. Today the handler checks only that the session cookie is valid. While signed in, Mia opens a page on another site that makes her browser send requests to that path: a form it submits by itself, a link and an image.
Which is the simplest submit handler that refuses every request that page can make her browser send, and still accepts her own Submit button?
The 30-second version
Browsers attach cookies to requests that other sites' pages start, so a cookie proves who is signed in, never that they meant the request. Guard each state-changing request with something another site cannot supply: a session-bound token it cannot read, or an Origin or Sec-Fetch-Site check. CORS and SameSite alone are not enough, and GET must never change state.
A cookie proves who is signed in; only a token or the browser's own headers prove your page sent the request.
How it works
Step 1 of 13
net-cookies-samesite).In the kitchen
- the seal pressed on every letter on the desk → the session cookie the browser attaches (step 1)
Analogy: A king's secretary and the royal seal (royal court)
A king's secretary presses the royal seal on every letter left on the king's desk, whoever left it there. The treasury pays out on any letter that bears the seal.
| At court | In the browser and on the server |
|---|---|
| the seal pressed on every letter on the desk | the session cookie the browser attaches (step 1) |
| a visitor leaving a letter on the desk | another site's page starting a request (step 2) |
| the treasury paying any sealed letter | a handler that checks only the session (step 3) |
| real orders written on numbered vellum kept in the king's locked cabinet | the synchronizer token in your own pages (steps 5–6) |
| the gate book, where the guard notes which room each letter left from; no writer can alter it | the Origin and Sec-Fetch-Site headers the browser sets (steps 9–10) |
| a minister's own warrant, shown only when she chooses | a bearer header the page adds itself (step 12) |
In real terms: the server must ask "did my own page send this?", and only a value or header another site cannot supply answers it.
Breaks down when: a secretary could notice a stranger at the desk. A browser never knows who wrote the request it sends, so the server alone must tell.
Examples, run for real
4.1 ExamHall EXH-1410: another site's form submits an attempt
ExamHall's submit handler checked only the session cookie. While Mia was signed in, a quiz-tips page on another site submitted a form to POST /attempts/123/submit by itself. MR !1610 adds a session-bound token to the exam page's Submit form and refuses unsafe requests that arrive marked Sec-Fetch-Site: cross-site:
4.1 before MR !1610 (session only): the other site's form -> 200, attempt submitted · the server's log: Sec-Fetch-Site cross-site, Origin https://quiz-tips.localhost, session cookie present
4.1 after MR !1610 (token + Sec-Fetch-Site): the other site's form -> 403 (cross-site write refused), not submitted · Mia's own Submit with the token -> 200, submitted
Her cookie arrived both times (steps 1–3). After the MR, the request also needs what only ExamHall's own page holds, and Chromium's own header names the site that started it (steps 5, 10).
4.2 ExamHall: the admin's delete link under Lax
The admin console's session cookie is SameSite=Lax, and its emails link to GET /exams/5/delete. A link on another site's page sends the admin's browser there:
4.2 before (SameSite=Lax session, GET deletes): a link on the other site -> 200, exam 5 deleted · the session cookie rode the top-level GET (Sec-Fetch-Site cross-site)
4.2 after (GET shows a confirm page; delete is a POST with the token): the same link -> 200, a confirm page, exam 5 kept · the admin's own Delete -> 200, deleted
Lax withheld nothing, because a link is a top-level GET (net-cookies-samesite step 7). The token check, like most, skips GET, so it could not help either. Only moving the delete to POST, with the token, closed it (step 11).
Try it
Answer first, then check. Nothing is saved.
Question 1 of 7
The box office's session cookie is SameSite=None; Secure, because partner sites embed its checkout. An MR adds one CSRF middleware: on every POST, PUT, PATCH and DELETE request, it compares the csrf field of the body with the session's token. The MR changes three more files.
Which change should the review block?
Question 2 of 7
ExamHall's admin console opens inside the university's portal, so its session cookie is SameSite=None; Secure. Its delete route answers both GET and POST /exams/5/delete, and checks the Origin header: a request whose Origin names another origin is refused, and one with no Origin is accepted, for old clients. While the admin is signed in, a page on another site makes her browser send three requests to that route: (1) a form it submits by itself, (2) a link, (3) a fetch with method: 'POST', mode: 'no-cors' and credentials: 'include'. Each runs on a fresh copy of exam 5.
Which of the three delete exam 5?
Question 3 of 7
In a design round you said ExamHall's results API needs no CSRF token: the candidate app keeps its access token in memory and sends it as Authorization: Bearer. The interviewer follows up: "To survive a reload, we'll move that token into an HttpOnly, SameSite=None; Secure cookie, since universities embed the app, and the API will read the cookie instead." You give one answer.
Which answer survives the interviewer's next question?
Question 4 of 7
ExamHall's answers API keeps no CSRF state on its servers, so the team picks a double-submit cookie: a token sent both in a cookie and in a request header, which the server compares. A forum vendor runs forum.examhall.test, a sibling subdomain of the exam app.
Which token does the OWASP CSRF Prevention Cheat Sheet recommend?
Question 5 of 7
The interviewer asks: "ExamHall's proctor API has 40 routes, and the console signs in with a cookie. Write your answer in at most 150 words, as a short list.
How do you make sure none of them can be triggered from another site's page, and how would you test it?
Question 6 of 7
A box office runs two transfer endpoints, neither with a CSRF check. The web API's POST /tickets/4471/transfer reads the fan's session cookie (SameSite=None; Secure, so that partner sites can embed the checkout). The partner API's POST /v1/tickets/4471/transfer, on another host, reads only Authorization: Bearer <token>, which the partner app keeps in memory and adds to its own requests. While Ola is signed in to both, a page on another site submits a transfer form to each endpoint by itself.
Which transfers happen?
Question 7 of 7
ExamHall's exam page sets a csrf cookie and puts the same token in a hidden csrf field of its Submit form. Three versions of the submit handler's token check follow, each run after the session check:
While Mia is signed in, a page on another site submits a form to POST /attempts/123/submit by itself. Her browser sends her cookies, and the form has no csrf field.
Which of the three checks refuse it?
// (1)if (body.csrf !== session.csrfToken) return reply(403);// (2)if (body.csrf && body.csrf !== session.csrfToken) return reply(403);// (3)if (!cookies.csrf) return reply(403);Go deeper
Pro tip
- In a review, read the route table before the handlers: every unsafe method should pass one CSRF middleware, and no GET route should write. When: an MR adds or changes a route (
mr-security-lens). Status: SELF (this lesson's practice target; not run). - Sign-in CSRF and the OAuth
statecheck are not taught here: they belong toauth-oauth-oidc-code-pkce, step 13. When: a review touches a sign-in redirect. Status: SELF (a pointer; not run).
Source
Valid as of 2026-10-19.
- OWASP, Cross-Site Request Forgery Prevention Cheat Sheet (the row's first source), https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html (
owasp-csrf.txt): "then it can be transmitted back to the server as a hidden field on a form submission" and "CSRF tokens should be generated on the server-side and they should be generated only once per user session or each request." (step 5); "they cannot be altered programmatically as they fall under forbidden headers list, meaning that only the browser can set them" (steps 9–10); "Treat cross-site as untrusted for state-changing actions." (step 10); "SameSite is useful as a defense-in-depth control but it does not replace a proper CSRF defense in most deployments." (step 13); "Remember that Cross-Site Scripting (XSS) can defeat all CSRF mitigation techniques!". As of 2026-10-19. - web.dev, SameSite cookies explained (the row's second source; the page reads "Last updated 2019-05-07"), https://web.dev/articles/samesite-cookies-explained (
webdev-samesite.txt): "SameSite=Lax allows the browser to send the cookie with these top-level navigations." (step 11); "use None to ensure your intent is clear" and "Cookies with SameSite=None must also specify Secure" (why an embedded app's cookie isSameSite=None; Secure). - MDN, Sec-Fetch-Site (the row's third source), https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Sec-Fetch-Site (
mdn-sec-fetch-site.txt): "The HTTP Sec-Fetch-Site fetch metadata request header indicates the relationship between a request initiator's origin and the origin of the requested resource." and "By default, requests that are not accepted should be rejected with a 403 response code." (step 10); "This request is a user-originated operation." (step 10, the valuenone); "The header is only included in requests to potentially trustworthy URLs" (why the lab serves https). As of 2026-10-19. - MDN, Origin (added for step 9; a standard vendor reference), https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Origin (
mdn-origin.txt): "The HTTP Origin request header indicates the origin" and "Broadly speaking, user agents add the Origin request header to:" (step 9); "There are some exceptions to the above rules" (step 9: a missingOriginproves nothing). As of 2026-10-19. - IETF, RFC 9110 HTTP Semantics, https://www.rfc-editor.org/rfc/rfc9110.txt (
rfc9110.txt): "Of the request methods defined by this specification, the GET, HEAD, OPTIONS, and TRACE methods are defined to be safe." (step 11).
Related lessons
Checked against the tools and docs as of 2026-10-19.