In the Browser
Chapter 1 named the mechanism — ambient authority. This chapter looks at the browser machinery in detail: how cookies get attached, what "credentialed request" really means, and crucially which ways of making a request will carry the victim's cookies cross-site. Knowing exactly which elements are dangerous (and which aren't) is what lets you reason about both attacks and defences precisely.
How Cookies Get Attached — by Destination, Not Origin
The single rule behind all of CSRF: the browser attaches a cookie based on where the request is going, not where it came from. When a request heads to yourbank.com, the browser looks up the cookies stored for yourbank.com and includes them — and it does not care which page initiated that request. A script on evil.com triggering a request to the bank gets the bank's cookies attached just the same.
SameSite cookie attribute (Chapter 7) exists precisely to let a cookie say "don't attach me on cross-site requests," but to understand why that's a fix you first have to see the default behaviour it changes.
"Credentialed" Requests
A credentialed request is one the browser sends with the user's ambient credentials (cookies, HTTP auth, client certs) attached. Whether a request is credentialed depends on how it's made:
- Navigations & form submissions — always credentialed. A
<form>POST or following a link always carries cookies for the destination. This is the CSRF workhorse. - Resource loads (
<img>,<script>,<iframe>) — credentialed. They fetch their target with cookies attached, which is why a GET that changes state is forgeable via an image tag. - fetch / XMLHttpRequest — credentialed only if asked. By default cross-origin
fetchsends no cookies; you must opt in withcredentials: 'include'— and even then the response is gated by CORS.
The Critical Asymmetry: Sending vs Reading
Here's the distinction that explains why classic CSRF is "write-only," and it trips up almost everyone at first:
| Action | Allowed cross-site by default? | Governed by |
|---|---|---|
| Browser sends a credentialed request (form/img/script) | YES — always has been | cookie attach rules (the CSRF gap) |
| Attacker page reads the cross-site response | NO — blocked | Same-Origin Policy / CORS |
The Same-Origin Policy (SOP) stops one origin from reading another origin's responses — so the attacker's page can't see your bank balance. But SOP was never designed to stop the browser from sending a cross-site request in the first place. CSRF lives entirely in that gap: sending is allowed even though reading is forbidden. The attacker doesn't need to read; causing the action is the whole goal.
The Role of HTTP Methods — and "Simple Requests"
Methods matter because of how easily each can be forged without any special permission:
| Forgeable via… | Methods / content types | Needs CORS preflight? |
|---|---|---|
| plain HTML (img, form) | GET; POST with form content types | No — fires freely |
| only via fetch/XHR | PUT, DELETE, PATCH, or JSON content type | Yes — preflighted |
A <form> can only send GET or POST, and only with three "simple" content types (application/x-www-form-urlencoded, multipart/form-data, text/plain). These are exactly the requests a browser will send cross-site without a CORS preflight (the automatic OPTIONS check). Anything beyond that — a DELETE, or a Content-Type: application/json — triggers a preflight the attacker's forged request can't satisfy, so it's effectively blocked.
application/json, a classic HTML-form CSRF attack can't even produce a valid request — a cross-site fetch with a JSON content type triggers a preflight, and your server can decline it. Likewise, requiring a custom header (which forms can't set) forces a preflight. This is why JSON APIs with custom headers are harder to CSRF — though, as the myth box warns, "harder" is not "immune," and you should still use explicit defences.
Two Methods, Two Honest Misuses
A recurring root cause: state-changing actions exposed over GET. Because <img>, <script>, and links all issue credentialed GETs silently, an endpoint like GET /account/delete?id=5 can be fired by merely loading an image tag on any page — no form, no script even needed. The HTTP spec says GET must be safe (no side effects) for exactly this reason. POST is not magically secure either, but at least it can't be triggered by a bare <img>.
Hands-On Exercises
Open DevTools → Network on a site you're logged into, trigger a request, and find the Cookie request header. Then articulate the rule that decided those cookies were attached — is it based on the page you're on, or the request's destination? Why does that rule enable CSRF?
Explain why CORS does not protect against CSRF. Be precise about what CORS actually governs (reading vs sending) and walk through why a forged POST still executes on the server even though the attacker's JavaScript can't read the response.
📄 View solutionClassify each as easily-forgeable-cross-site or not, and say why: (a) GET /search?q=x via an img tag; (b) a form POST with application/x-www-form-urlencoded; (c) a fetch with DELETE; (d) a fetch sending Content-Type: application/json. Reference preflight where relevant.
Chapter 2 Quick Reference
- Cookies attach by destination, not origin — a request to yourbank.com gets the bank's cookies whoever triggered it
- Same-site (page & destination match) vs cross-site (differ) — historically cookies sent on both
- Credentialed request = sent with the user's cookies; forms/navigations & resource loads always are,
fetchonly withcredentials:'include' - Critical asymmetry: the browser sends cross-site requests freely, but the Same-Origin Policy blocks reading the response — CSRF lives in that gap (write-only)
- CORS is NOT a CSRF defence — it governs reading responses, not whether the request is sent/processed
- HTML forms send only GET/POST with "simple" content types → no preflight → freely forgeable; JSON/DELETE/custom headers trigger a preflight
- State-changing GET is the worst offender — forgeable by a bare
<img>; GET must be safe (no side effects) - Next chapter: anatomy of an attack — crafting the malicious page, GET vs POST forgery, real-world patterns