Sessions, cookies, CORS and CSRF: the browser's trust rules
Which origin can read what, why a cookie is scoped differently from everything else in the browser, and the four ways a session is stolen without ever finding an XSS: CSRF, a permissive CORS policy, cookie tossing, and session fixation.
A large share of web challenges come down to one question: can this origin read that response? The browser's answer is the same-origin policy, and every bug in this post is a place where the rules have an exception, a legacy carve-out, or a header that opts out of them.
Worth being precise up front, because the imprecision is where the bugs are. The same-origin policy governs *reading*. It does not govern *sending*. A page on any origin may send a request to any other origin with the victim's cookies attached; it simply may not read the reply. CSRF exploits the sending; CORS misconfiguration restores the reading.
Cookies are scoped differently from everything else
An origin is scheme, host and port together. A cookie's scope is none of those three the same way:
| Property | Origin | Cookie |
|---|---|---|
| Scheme | Part of the origin | Ignored unless Secure is set |
| Port | Part of the origin | Ignored entirely |
| Host | Exact match | Domain=example.com covers every subdomain |
| Path | Not part of it | Path=/admin restricts sending, but not reading by script |
Three consequences that challenges are built on. Any subdomain can set a cookie for the parent domain, so an XSS or a takeover on blog.target reaches app.target's session. Any subdomain can overwrite a parent-domain cookie, which is cookie tossing. And `http://` and `https://` share cookies unless Secure is set, so a plaintext request leaks a session that was only ever issued over TLS.
The flags, and what each actually prevents
- `HttpOnly` hides the cookie from
document.cookie. It does not stop an XSS from *using* the session - the script can still make authenticated requests. It stops exfiltration of the value, not abuse of it. - `Secure` stops the cookie being sent over plain HTTP. Without it, one
http://subresource leaks the session. - `SameSite=Strict` stops the cookie on every cross-site request, including top-level navigation. Effective, and it breaks incoming links, which is why it is rare.
- `SameSite=Lax` (the modern default) sends the cookie on top-level GET navigations only. So a CSRF that works with a
GETlink still works, and one requiring a POST does not. - `SameSite=None` requires
Secureand opts back in to everything. A challenge that sets it is telling you CSRF is intended. - `__Host-` prefix enforces
Secure, noDomain, andPath=/- which specifically blocks cookie tossing from a subdomain. Its absence on a session cookie in a multi-subdomain challenge is a hint.
CSRF: making the victim send the request
The attack is a page on your origin that causes the victim's browser to issue a state-changing request to the target, with their cookies attached. You never see the response and you do not need to.
<form action="https://target/account/email" method="POST">
<input name="email" value="attacker@you.example">
</form>
<script>document.forms[0].submit()</script>For it to work, three things must hold: the action is state-changing, the session rides on a cookie rather than a header, and there is no unpredictable token. The defences and their failures:
| Defence | How it fails |
|---|---|
| A CSRF token | Not validated on some methods; validated only if present; tied to no session, so any valid token works; predictable or reflected in a GET response. |
SameSite=Lax | Bypassed by a GET-based state change, or by a route that accepts _method=POST override. |
| Referer checking | Missing Referer allowed; checked with startsWith, so https://target.you.example passes; suppressed entirely with Referrer-Policy: no-referrer on your page. |
| Requiring JSON | Content-Type: text/plain sends cross-origin without preflight, and a lenient parser still reads the body as JSON. |
| A custom header | Genuinely effective - it forces a preflight. Unless CORS is misconfigured, below. |
CORS: opting out of the same-origin policy
CORS lets a server say "this other origin may read my responses". It is a deliberate relaxation, and the misconfigurations are all about *which* origins get named.
Access-Control-Allow-Origin: https://app.target
Access-Control-Allow-Credentials: true- Origin reflection. The server echoes whatever
Originyou send. WithAllow-Credentials: truethis is total: your page reads authenticated responses from the target. Test by sendingOrigin: https://you.exampleand looking at what comes back. - `null` allowed.
Access-Control-Allow-Origin: nullis exploitable because a sandboxed iframe, adata:URL, or a redirect produces anullorigin, and you control those. - Sloppy suffix matching. A check for
endsWith("target.com")acceptseviltarget.com. A check forstartsWith("https://target.com")acceptshttps://target.com.you.example. - Subdomains trusted wholesale. Any XSS or dangling DNS record on any subdomain becomes a read primitive against the main application.
- **
Allow-Origin: *with credentials.** The browser refuses this combination, so it is not directly exploitable - but it does mean unauthenticated responses are readable, which still leaks internal API data.
A working CORS misconfiguration is strictly better than XSS for reading data, because it needs no injection point at all - just a victim who visits your page while logged in.
Cookie tossing and session fixation
Tossing
From any subdomain, document.cookie = "session=X; Domain=target.com; Path=/" writes a cookie the parent domain will send. The browser does not tell the server which host set a cookie, so the application cannot distinguish yours from its own. When two cookies of the same name exist with different paths, the more specific path wins - which is how you shadow a session cookie without deleting it.
This is the reason subdomain takeover matters in challenges that seem to have nothing to do with DNS, and the reason the __Host- prefix exists.
Fixation
If the application accepts a session id you supply and does not rotate it at login, you can set a known id in the victim's browser, wait for them to authenticate, and then use that id yourself. The test is simple: note your session id, log in, and see whether it changed. If it did not, fixation is available - and combined with tossing, you do not even need an injection point to set it.
Where the session actually lives
Before attacking the transport, look at the token itself. Several challenges in this space are really token challenges:
- A JWT in a cookie brings its own attack surface -
alg=none, a weak HMAC secret, algorithm confusion. - A Flask session cookie is base64 with a signature; the payload is readable without the key and forgeable with a cracked one.
- A cookie that is base64 of a serialised object is an insecure deserialization target, not a session problem.
- An encrypted cookie with no MAC is malleable - see block cipher modes for bit flipping and padding oracles.
- A sequential or timestamp-derived id is predictable, and PRNG prediction covers recovering the generator.
A session-attack checklist
- Decode the session token. Half the time the challenge is inside it and none of this post applies.
- Read the cookie flags. Missing
Secure, missingSameSite, aDomaincovering subdomains, and no__Host-prefix are each a route. - Send an
Originheader on an authenticated endpoint and see what comes back. Reflection with credentials ends the challenge. - Try
Origin: nullseparately - it is a different code path. - Test CSRF by removing the token, not by changing it. Then by changing the method, then the Content-Type.
- Check whether the session id rotates at login.
- If there are subdomains in scope, consider what one of them could set for the parent.
The unifying idea is that the browser enforces a boundary the server cannot see. The server receives a request with a valid cookie and has no way to know whether the user meant to send it, which origin's page caused it, or which host set the cookie in the first place. Every attack here is a way of exploiting that blindness, and every defence is the server asking for a piece of evidence - a token, an origin header, a flag - that only a legitimate request would carry.