Handbooks / Web Security (OWASP) / Chapter 4
CSRF and CORS
7 pages · ~5 min✓ Reviewed
Builds on Authentication and Sessions. Next up: HTTPS and Secrets.
Same-Origin Policy Mental Model
1. What an Origin Is
Origin = scheme + host + port; all three must match exactly.
| Compared with https://a.com | Same origin? |
|---|---|
http://a.com | No: scheme |
https://a.com:8443 | No: port |
https://b.a.com | No: host |
https://a.com/page | Yes: path ignored |
2. What SOP Blocks
- evil.com scriptcalls fetch
- Request sentsimple request
- Server repliesresponse returns
- Read blockedJS sees nothing
| Cross-origin action | Result |
|---|---|
Embed img, script, link | Allowed |
| Submit a form POST | Allowed |
Display an iframe | Allowed |
| JS reads the contents | Blocked |
3. SOP vs CORS
- SOP = browser default, restricts reads
- CORS = opt-in server headers, relaxes reads
- Neither protects the server from
curlor direct clients
4. Interview Point
- What is an origin? Scheme + host + port; all three must match.
- Does SOP stop a request being sent? No, only the read; a preflighted fetch stops at OPTIONS.
- Why does a form POST work? It is a send, not a read; the attacker never reads the reply.
CSRF Attacks and CSRF Tokens
1. What CSRF Is
- Browser auto-attaches cookies to a forged cross-site request
- Server sees an authenticated, state-changing action
- Attacker never reads the response
- Needs all three: cookie auth, state-changing endpoint, predictable params
2. Token Defences
| Synchronizer | Double-submit | |
|---|---|---|
| Token kept | Server, per session | Cookie plus request copy |
| Sent in | Hidden field or header | Header or body |
| Check | Matches stored token | Cookie equals copy |
| Attacker fails | SOP: cannot read token | Cannot read cookie |
- Sign or HMAC the double-submit token: stops subdomain cookie injection
3. Interview Point
- Why does a token defeat CSRF? Attacker cannot read it (SOP), so cannot forge it
- Why must GET never change state? Links and img tags send GET with cookies, no token
- Cookie-auth JSON API safe? No: cookies still attach, and forms can post JSON as text/plain
Remember
A token works only because the attacker cannot read it.
SameSite Cookies and Defence in Depth
1. The Three Values
| Value | Cross-site cookie sent | Catch |
|---|---|---|
| Strict | Never, even link clicks | Login lost on inbound links |
| Lax | Top-level GET navigation only | Not POST, iframe, fetch; Chromium default only; set it explicitly |
| None | All requests | Needs Secure; CSRF exposure returns |
http
Set-Cookie: sid=1; SameSite=Lax; Secure; HttpOnly2. Site vs Origin
- site = scheme + eTLD+1; ignores port and subdomain
a.comandb.a.com: same-site, different originshttp://a.comvshttps://a.com: cross-site- Compromised subdomain can still attack
Defence in depth
SameSite is a layer, not a replacement: keep CSRF tokens, Origin checks, no state-changing GET.
3. Interview Point
- Strict vs Lax, who loses login? Strict: a user clicking a link from email lands logged out; Lax keeps it.
- SameSite a full replacement? No: None, subdomains and non-Chromium defaults remain.
- Origin vs site? Origin is scheme+host+port; site is scheme + eTLD+1.
CORS Headers and Preflight Requests
1. Origin and Allow-Origin
- Browser sends
Origin; server repliesAccess-Control-Allow-Originwith it or* - Browser decides whether JS may read the response
- Simple (GET, HEAD, form/text POST, no custom headers): no preflight
2. Preflight
| Browser OPTIONS | Server answer |
|---|---|
Access-Control-Request-Method | Access-Control-Allow-Methods |
Access-Control-Request-Headers | Access-Control-Allow-Headers |
| (none) | Access-Control-Max-Age caches it |
- Triggers: custom header,
PUT/DELETE,Content-Type: application/json - Real request waits for the
OPTIONSapproval
3. Credentials
javascript
fetch(url, { credentials: 'include' })client side
- Response needs
Access-Control-Allow-Credentials: true - Allowed origin explicit, never
* - Non-simple response headers need
Access-Control-Expose-Headers
4. Interview Point
- When is a preflight sent? Custom header, PUT/DELETE or JSON content type.
- Why no
*with credentials? Browsers reject it; a named origin is required. - Does CORS stop CSRF? No; it limits reads, and simple requests are still sent.
Common CORS Misconfigurations
1. Reflected Origin
- Server copies
Origininto Allow-Origin plusAllow-Credentials: true - Any site can then read logged-in responses
2. Weak Origin Checks
| Check | Bypassed by |
|---|---|
endsWith('example.com') | evil-example.com |
| Substring match | example.com.evil.com |
Trusts null | Sandboxed iframe, file:// |
3. Caches and Safe Validation
- Dynamic origin needs Vary: Origin
- Without it, a cache serves one origin's headers to another
javascript
const ok = new Set(['https://app.example.com']); if (ok.has(req.headers.origin)) { res.set('Access-Control-Allow-Origin', req.headers.origin); res.set('Vary', 'Origin'); }
exact match against an allowlist
Never
Reflect Origin, trust null, or pair * with credentials.
4. Interview Point
- Exploit reflected origin? Evil page fetches with
credentials:'include'and reads the reply - Why no
*with credentials? Any site could read authenticated data - Validate safely? Exact match against an allowlist, never suffix or substring
Cheat Sheet: SOP, CSRF, SameSite, CORS
1. Origin vs site
| Is | Ignores | |
|---|---|---|
| Origin | scheme + host + port | nothing |
| Site | scheme + eTLD+1 | port, subdomain |
- SOP blocks reads, not sends
- No protection from direct requests like
curl
2. CSRF defences and cookies
- 1. SameSiteLax / Strict
- 2. Tokenstate-changing requests
- 3. Origin/Referercheck it
- 4. No GET writesGET stays safe
http
Set-Cookie: sid=abc; HttpOnly; Secure; SameSite=Lax
set SameSite explicitly
3. CORS headers
| Access-Control-... | Job |
|---|---|
Allow-Origin | origin allowed to read |
Allow-Methods | preflight: methods |
Allow-Headers | preflight: request headers |
Allow-Credentials | true, explicit origin |
Expose-Headers | non-simple response headers |
Max-Age | cache the preflight |
Vary: Origin | when origin is dynamic |
Debug rule
A CORS error is the browser blocking the read. Check response headers and the OPTIONS preflight status in DevTools. Never reflect Origin, trust null, or pair * with credentials.
Check yourself
A server copies the request Origin into Access-Control-Allow-Origin and sends Allow-Credentials: true. What can evil.com do?
- Its JS calls fetch with credentials:'include' and reads the victim's logged-in responses
- Fix: exact allowlist match, plus Vary: Origin
Cookie is SameSite=Lax. Which is still sent from evil.com: a form POST to /transfer, or a link click to GET /transfer?to=x?
- The link click: Lax sends cookies on cross-site top-level GET navigations
- The POST is blocked, so state changes on GET stay exploitable
Page on a.com fetches api.a.com with Content-Type: application/json. Is a preflight sent?
- Yes: different origin, and JSON is not a simple content type
- If OPTIONS lacks Access-Control-Allow-Headers: Content-Type, the real request is never sent