In the Browser

Chapter 2
How the Browser Enables It
Cookies, credentialed requests, and which elements send credentials cross-site

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.

"Same-site" is about the cookie's domain vs the page's domain
A request is same-site when the page making it and the destination share the same site (e.g. a page on yourbank.com posting to yourbank.com). It's cross-site when they differ (evil.com posting to yourbank.com). Historically the browser attached cookies on both — that's the gap CSRF exploits. The 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 fetch sends no cookies; you must opt in with credentials: '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:

ActionAllowed cross-site by default?Governed by
Browser sends a credentialed request (form/img/script)YES — always has beencookie attach rules (the CSRF gap)
Attacker page reads the cross-site responseNO — blockedSame-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.

A common myth: "CORS protects me from CSRF." It doesn't.
Developers often assume that because CORS blocks cross-origin reads, it also blocks cross-origin writes — and that their API is therefore CSRF-safe. This is wrong and dangerous. CORS governs whether JavaScript may read a response; it does not prevent the request from being sent and processed by your server. A forged form POST reaches your endpoint and executes regardless of CORS — the attacker simply never reads the reply, which they don't care about. CORS is not a CSRF defence. (Chapter 9 returns to this for APIs.)

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 typesNeeds CORS preflight?
plain HTML (img, form)GET; POST with form content typesNo — fires freely
only via fetch/XHRPUT, DELETE, PATCH, or JSON content typeYes — 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.

Why this hands you a defence (preview of Chapters 8–9)
This asymmetry is quietly load-bearing for modern defences. If your endpoint only accepts 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>.

<!-- a state-changing GET is forgeable with a single tag on ANY page --> <img src="https://yourbank.com/account/delete?id=5"> <!-- this is why "safe" methods must not change state -->

Hands-On Exercises

Exercise 1

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?

📄 View solution
Exercise 2

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 solution
Exercise 3

Classify 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.

📄 View solution

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, fetch only with credentials:'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