Handbooks / Web Security (OWASP) / Chapter 4

CSRF and CORS

7 pages · ~5 min✓ Reviewed

Builds on Authentication and Sessions. Next up: HTTPS and Secrets.

Web Security (OWASP)Page 1

Same-Origin Policy Mental Model

1. What an Origin Is

Origin = scheme + host + port; all three must match exactly.

Compared with https://a.comSame origin?
http://a.comNo: scheme
https://a.com:8443No: port
https://b.a.comNo: host
https://a.com/pageYes: path ignored

2. What SOP Blocks

  1. evil.com scriptcalls fetch
  2. Request sentsimple request
  3. Server repliesresponse returns
  4. Read blockedJS sees nothing
Cross-origin actionResult
Embed img, script, linkAllowed
Submit a form POSTAllowed
Display an iframeAllowed
JS reads the contentsBlocked

3. SOP vs CORS

  • SOP = browser default, restricts reads
  • CORS = opt-in server headers, relaxes reads
  • Neither protects the server from curl or 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.
Web Security (OWASP)Page 2

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

SynchronizerDouble-submit
Token keptServer, per sessionCookie plus request copy
Sent inHidden field or headerHeader or body
CheckMatches stored tokenCookie equals copy
Attacker failsSOP: cannot read tokenCannot 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.

Web Security (OWASP)Page 3

SameSite Cookies and Defence in Depth

1. The Three Values

ValueCross-site cookie sentCatch
StrictNever, even link clicksLogin lost on inbound links
LaxTop-level GET navigation onlyNot POST, iframe, fetch; Chromium default only; set it explicitly
NoneAll requestsNeeds Secure; CSRF exposure returns
http
Set-Cookie: sid=1; SameSite=Lax; Secure; HttpOnly

2. Site vs Origin

  • site = scheme + eTLD+1; ignores port and subdomain
  • a.com and b.a.com: same-site, different origins
  • http://a.com vs https://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.
Web Security (OWASP)Page 4

CORS Headers and Preflight Requests

1. Origin and Allow-Origin

  • Browser sends Origin; server replies Access-Control-Allow-Origin with it or *
  • Browser decides whether JS may read the response
  • Simple (GET, HEAD, form/text POST, no custom headers): no preflight

2. Preflight

Browser OPTIONSServer answer
Access-Control-Request-MethodAccess-Control-Allow-Methods
Access-Control-Request-HeadersAccess-Control-Allow-Headers
(none)Access-Control-Max-Age caches it
  • Triggers: custom header, PUT/DELETE, Content-Type: application/json
  • Real request waits for the OPTIONS approval

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.
Web Security (OWASP)Page 5

Common CORS Misconfigurations

1. Reflected Origin

  • Server copies Origin into Allow-Origin plus Allow-Credentials: true
  • Any site can then read logged-in responses

2. Weak Origin Checks

CheckBypassed by
endsWith('example.com')evil-example.com
Substring matchexample.com.evil.com
Trusts nullSandboxed 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
Web Security (OWASP)Page 6

Cheat Sheet: SOP, CSRF, SameSite, CORS

1. Origin vs site

IsIgnores
Originscheme + host + portnothing
Sitescheme + eTLD+1port, subdomain
  • SOP blocks reads, not sends
  • No protection from direct requests like curl

2. CSRF defences and cookies

  1. 1. SameSiteLax / Strict
  2. 2. Tokenstate-changing requests
  3. 3. Origin/Referercheck it
  4. 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-Originorigin allowed to read
Allow-Methodspreflight: methods
Allow-Headerspreflight: request headers
Allow-Credentialstrue, explicit origin
Expose-Headersnon-simple response headers
Max-Agecache the preflight
Vary: Originwhen 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.

Web Security (OWASP)Page 7

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