BLOG Web Security 15 MIN READ

CORS Misconfiguration Explained: How Unsafe Headers Expose Cross-Origin Data

A technical deep-dive into how CORS works at the browser and HTTP level, why origin reflection and credentials are dangerous together, and how to detect and remediate unsafe CORS headers.

Introduction

CORS misconfiguration is dangerous because it changes what browser JavaScript is allowed to read across origins.

The issue is not that another website can send a request to your API. Browsers can already send many cross-origin requests through forms, images, scripts, and fetch-style requests.

The security boundary is the response. Same-origin policy normally prevents JavaScript on attacker.example from reading sensitive responses from api.example.com.

A bad CORS policy can relax that boundary. If the API reflects an attacker-controlled Origin and also allows credentials, a malicious page may be able to read user-specific API data from the victim’s authenticated browser session.

This post explains CORS at the protocol level, shows vulnerable and secure headers, gives safe detection commands, and provides exact remediation examples for common stacks.

What CORS actually controls

Cross-Origin Resource Sharing is an HTTP-header based browser mechanism. It lets a server tell the browser which origins are allowed to read a cross-origin response.

An origin is not just a domain name. It is the combination of scheme, host, and port.

These are different origins:

```text https://app.example.com http://app.example.com https://app.example.com:8443 https://api.example.com ```

CORS does not replace authentication. It does not stop curl, backend services, mobile clients, or attackers from sending direct HTTP requests.

CORS only tells browsers whether JavaScript from one origin can read the response from another origin.

What CORS does and does not do.
QuestionCorrect answerSecurity implication
Does CORS authenticate users?NoUse normal server-side authentication and authorization
Does CORS stop all cross-origin requests?NoIt controls whether browser JavaScript can read the response
Does CORS protect APIs from curl or server-side clients?NoAPIs must enforce authorization on the server
Can CORS expose sensitive user data?Yes, when misconfigured with trusted credentials and sensitive responsesA malicious origin may read responses in the victim’s browser
Can CORS replace CSRF protection?NoState-changing actions still need CSRF defenses, SameSite cookies, and authorization checks

How CORS works at the protocol level

A browser sends an Origin header when JavaScript makes a cross-origin request.

The server responds with CORS headers. The most important one is Access-Control-Allow-Origin.

If the response allows the requesting origin, the browser makes the response available to JavaScript. If not, the browser blocks JavaScript from reading it, even though the network request may have reached the server.

For simple requests, the browser may send the request directly and then enforce CORS on the response. For non-simple requests, the browser sends a preflight OPTIONS request first.

Preflight asks whether the actual method and headers are allowed before the browser sends the real request.

Core CORS headers.
HeaderDirectionPurpose
OriginRequestTells the server which origin initiated the browser request
Access-Control-Allow-OriginResponseTells the browser which origin may read the response
Access-Control-Allow-CredentialsResponseTells the browser whether credentialed cross-origin responses may be exposed
Access-Control-Allow-MethodsPreflight responseLists methods allowed for the actual request
Access-Control-Allow-HeadersPreflight responseLists request headers allowed for the actual request
Access-Control-Max-AgePreflight responseTells the browser how long it may cache the preflight result
Vary: OriginResponsePrevents caches from serving one origin’s CORS response to another origin

Why credentials change the risk

The most dangerous CORS bugs usually involve credentials.

Credentials can include cookies, HTTP authentication, and client certificates. In browser fetch, JavaScript usually needs `credentials: "include"` before cookies are sent cross-origin.

SameSite cookie settings also matter. Cookies marked SameSite=Lax or SameSite=Strict are less likely to be sent on cross-site fetch requests. Cookies marked SameSite=None; Secure can be sent in cross-site contexts when the browser and request rules allow it.

The high-risk pattern is a sensitive API response plus a trusted user session plus a CORS policy that allows an attacker origin to read the response.

CORS does not let attacker.example read localStorage from api.example.com. It can, however, let attacker.example read an API response if the victim’s browser sends valid credentials and the API tells the browser that attacker.example is allowed.

CORS risk by response type.
Response typeCORS riskReason
Public static assetLowThe data is intended to be public
Unauthenticated public APILow to mediumAny origin may read data, but data classification still matters
Authenticated account APIHighUser-specific data may be exposed to attacker-controlled JavaScript
Admin APICriticalPrivileged data or actions may be exposed through an authenticated browser session
State-changing APIHighCORS may combine with missing CSRF protection or weak authorization
Token-returning endpointCriticalSession, API key, or token material may be exposed

What the vulnerable configuration looks like

The classic vulnerable pattern is origin reflection.

The application reads the Origin request header and copies it into Access-Control-Allow-Origin without checking whether that origin is trusted.

Vulnerable response:

```http HTTP/1.1 200 OK Access-Control-Allow-Origin: https://attacker.example Access-Control-Allow-Credentials: true Content-Type: application/json {"email":"user@example.com","api_key":"redacted-example"} ```

That response tells the browser that JavaScript running on https://attacker.example may read the authenticated response.

The dangerous part is not one header alone. It is the combination of a sensitive response, an attacker-controlled allowed origin, and credentialed access.

Common CORS misconfigurations.
MisconfigurationExampleWhy it is risky
Origin reflectionAccess-Control-Allow-Origin mirrors any Origin valueAny attacker-controlled site can become an allowed origin
Credentials with reflected originAccess-Control-Allow-Credentials: true plus reflected OriginBrowser may expose authenticated responses to attacker JavaScript
Wildcard on sensitive dataAccess-Control-Allow-Origin: *Any site can read non-credentialed sensitive or semi-sensitive data
Trusting null originAccess-Control-Allow-Origin: nullSandboxed documents and local contexts can use null origin behavior
Weak suffix checksAllow origins ending with example.comattackerexample.com or example.com.attacker.net may pass bad validation
Weak substring checksAllow any origin containing example.comhttps://example.com.attacker.net may be accepted
Overbroad methodsAllow PUT, PATCH, DELETE for untrusted originsIncreases risk when combined with weak auth or CSRF gaps
Missing Vary: OriginDynamic ACAO without VaryShared caches may reuse a response generated for another origin

How attackers exploit CORS misconfiguration step by step

A safe way to understand the attack is to follow the browser decision path.

First, the attacker hosts JavaScript on an origin they control, such as https://attacker.example.

Second, the victim visits that page while already authenticated to the target application in the same browser.

Third, the attacker page makes a cross-origin request to the target API with credentials enabled.

Fourth, the target API responds with Access-Control-Allow-Origin set to the attacker origin and Access-Control-Allow-Credentials set to true.

Finally, the browser exposes the response body to the attacker page’s JavaScript because the server explicitly allowed that origin.

This is why CORS bugs are often data-exposure bugs rather than direct server compromise bugs.

Defensive view of the attack flow.
StepBrowser actionDefensive control
Victim has an active sessionBrowser stores valid session cookiesUse SameSite cookies, MFA for sensitive actions, and short-lived sessions
Attacker page sends cross-origin requestBrowser includes Origin headerDo not trust Origin as authentication
Target API receives requestServer evaluates CORS policyUse exact allowlist validation
Target API respondsCORS headers decide whether JavaScript can read the responseNever reflect arbitrary Origin values
Browser enforces CORSResponse is exposed or blockedAvoid credentials unless required and scoped
Data is read by attacker JavaScriptSensitive response becomes accessible cross-originClassify and restrict sensitive endpoints

How to detect CORS misconfiguration safely

Only test domains and applications you own or have explicit permission to assess.

curl does not enforce CORS like a browser. It is still useful because it shows whether the server returns dangerous CORS headers.

Start with an attacker-controlled test origin that should never be trusted:

```bash curl -i -s \ -H "Origin: https://attacker.example" \ https://api.example.com/account ```

Vulnerable output:

```http HTTP/1.1 200 OK Access-Control-Allow-Origin: https://attacker.example Access-Control-Allow-Credentials: true Content-Type: application/json ```

That result is high risk if the endpoint returns sensitive user-specific data and the browser can send credentials.

A safer response is no CORS header for the attacker origin, or a header that allows only a known trusted origin.

Testing preflight behavior

Some requests trigger a preflight OPTIONS request before the actual request.

Test preflight when the application allows custom headers, Authorization headers, JSON content types, or methods such as PUT, PATCH, and DELETE.

The preflight request itself should not include credentials. The preflight response can allow credentials for the later actual request.

Example:

```bash curl -i -s -X OPTIONS \ -H "Origin: https://attacker.example" \ -H "Access-Control-Request-Method: DELETE" \ -H "Access-Control-Request-Headers: authorization,content-type" \ https://api.example.com/account ```

Dangerous output:

```http HTTP/1.1 204 No Content Access-Control-Allow-Origin: https://attacker.example Access-Control-Allow-Credentials: true Access-Control-Allow-Methods: GET,POST,PUT,PATCH,DELETE,OPTIONS Access-Control-Allow-Headers: authorization,content-type ```

This does not automatically prove exploitation. It proves the server is willing to approve a dangerous cross-origin request shape.

To confirm impact, test in a controlled browser session against an owned account and avoid collecting real user data.

Testing null origin and weak allowlists

Some applications trust null origin or use weak string matching.

Test null origin safely:

```bash curl -i -s \ -H "Origin: null" \ https://api.example.com/account ```

Risky output:

```http Access-Control-Allow-Origin: null Access-Control-Allow-Credentials: true ```

Test weak suffix or substring validation by using origins that contain the trusted brand but are not actually trusted:

```bash curl -i -s -H "Origin: https://example.com.attacker.example" https://api.example.com/account curl -i -s -H "Origin: https://attacker-example.com" https://api.example.com/account curl -i -s -H "Origin: https://app.example.com.evil.example" https://api.example.com/account ```

A correct allowlist should reject all of those origins unless they are intentionally owned and trusted.

Browser proof-of-risk test

Header inspection is not always enough because real impact depends on browser behavior, credentials, SameSite cookies, endpoint sensitivity, and response exposure.

Use a controlled test account and an owned test origin. Do not exfiltrate data. Confirm whether browser JavaScript can read the response.

Safe proof-of-risk snippet for your own lab:

```html <script> fetch("https://api.example.com/account", { credentials: "include" }) .then(async response => { const text = await response.text(); console.log("status", response.status); console.log("readable_length", text.length); }) .catch(error => { console.log("blocked_or_failed", error.message); }); </script> ```

If the browser logs the readable response length for a sensitive authenticated endpoint from an untrusted origin, treat the finding as confirmed.

If the browser blocks the read, still fix dangerous headers if they indicate weak validation. Browser behavior, cookie settings, and endpoint shape can change later.

What a secure result looks like

A secure API only allows trusted origins that genuinely need browser access.

For an untrusted origin, the response should not grant cross-origin read access:

```http HTTP/1.1 200 OK Content-Type: application/json {"status":"ok"} ```

curl can still display the response because curl does not enforce browser CORS. The security decision is whether browser JavaScript from the untrusted origin can read it.

For a trusted origin, the response can be specific:

```http HTTP/1.1 200 OK Access-Control-Allow-Origin: https://app.example.com Access-Control-Allow-Credentials: true Vary: Origin Content-Type: application/json ```

Do not use Access-Control-Allow-Origin: * for sensitive API responses.

Do not combine credentials with broad or reflected origins.

Remediation — exact CORS fixes

Fix CORS by making trust explicit.

Start by deciding which browser origins truly need access to each endpoint. Then allow only those exact origins by scheme, host, and port.

Avoid global CORS rules at the CDN, reverse proxy, or framework middleware unless every route behind that rule has the same sensitivity and trust model.

When CORS behavior changes based on Origin, return Vary: Origin so shared caches do not mix responses across origins.

CORS remediation checklist.
FixHow to apply itWhy it matters
Use exact origin allowlistsAllow https://app.example.com, not *.example.com unless every subdomain is equally trustedPrevents attacker-controlled lookalike origins from being accepted
Validate scheme, host, and portTreat http://, https://, and custom ports as separate originsPrevents accidental trust expansion
Do not reflect Origin blindlyReturn ACAO only after allowlist matchStops arbitrary attacker origins from becoming trusted
Limit credentialsUse Access-Control-Allow-Credentials: true only when cross-origin cookies or auth are truly requiredReduces authenticated data exposure
Restrict methodsAllow only required methods per routeLimits dangerous cross-origin request shapes
Restrict headersAllow only required request headersPrevents unnecessary authorization or custom-header exposure
Add Vary: OriginUse it whenever ACAO changes by request OriginPrevents cache confusion
Separate public and private APIsUse different CORS rules for public data and authenticated account dataAvoids applying broad public CORS to sensitive routes
Keep CSRF protectionUse CSRF tokens, SameSite cookies, and authorization checks for state-changing requestsCORS is not CSRF protection

Safe Express configuration

In Express, use the official cors middleware with an explicit allowlist.

Do not set origin: true globally unless your validation function rejects untrusted origins.

Example:

```javascript const express = require("express"); const cors = require("cors"); const app = express(); const allowedOrigins = new Set([ "https://app.example.com", "https://admin.example.com" ]); const corsOptions = { origin(origin, callback) { if (!origin) { return callback(null, false); } if (allowedOrigins.has(origin)) { return callback(null, origin); } return callback(null, false); }, credentials: true, methods: ["GET", "POST"], allowedHeaders: ["Content-Type", "Authorization"], maxAge: 600 }; app.use((req, res, next) => { res.vary("Origin"); next(); }); app.use("/api", cors(corsOptions)); ```

Add PUT, PATCH, or DELETE only on routes that require those methods and have separate authorization and CSRF controls.

For endpoints that do not need credentialed browser access, omit credentials entirely.

Safe Django configuration

For Django, avoid allowing all origins on authenticated APIs.

Use exact origins and enable credentials only when required:

```python # settings.py CORS_ALLOWED_ORIGINS = [ "https://app.example.com", "https://admin.example.com", ] CORS_ALLOW_CREDENTIALS = True CORS_ALLOW_METHODS = [ "GET", "POST", "OPTIONS", ] CORS_ALLOW_HEADERS = [ "authorization", "content-type", ] CSRF_TRUSTED_ORIGINS = [ "https://app.example.com", "https://admin.example.com", ] ```

For cross-origin state-changing requests that rely on Django CSRF validation, configure CSRF_TRUSTED_ORIGINS for the exact trusted frontend origins.

Avoid this pattern on sensitive applications:

```python CORS_ALLOW_ALL_ORIGINS = True CORS_ALLOW_CREDENTIALS = True ```

Also keep Django’s CSRF protections for state-changing routes. CORS and CSRF solve different problems.

Safe Spring configuration

In Spring, allow credentials only with specific allowed origins.

Example:

```java @Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") .allowedOrigins("https://app.example.com", "https://admin.example.com") .allowedMethods("GET", "POST") .allowedHeaders("Content-Type", "Authorization") .allowCredentials(true) .maxAge(600); } } ```

Add PUT, PATCH, or DELETE only for route groups that require those methods and have separate authorization and CSRF controls.

Do not combine credentialed routes with wildcard origins.

Review route-level sensitivity. Admin routes, account routes, and token-returning routes should not inherit broad public CORS settings.

Nginx and CDN CORS caution

CORS is usually safer at the application layer because the application knows which routes are public, authenticated, or administrative.

If you must set CORS at Nginx, CDN, API gateway, or reverse proxy level, avoid global wildcard rules.

Use exact origin matching and make sure backend and proxy do not both emit duplicate Access-Control-Allow-Origin headers.

Use this only when the proxy is the single place where CORS is managed. Do not also emit CORS headers from the application for the same routes.

A simplified Nginx pattern looks like this:

```nginx map $http_origin $cors_origin { default ""; "https://app.example.com" $http_origin; "https://admin.example.com" $http_origin; } server { location /api/ { add_header Vary "Origin" always; add_header Access-Control-Allow-Origin $cors_origin always; add_header Access-Control-Allow-Credentials "true" always; proxy_pass http://api_backend; } } ```

This simplified example does not include full OPTIONS handling. Production gateways should explicitly handle allowed methods, allowed headers, denied origins, cache behavior, and upstream header stripping.

Test this in staging before production. Empty variables, duplicate headers, OPTIONS handling, cache behavior, and upstream headers can change how browsers interpret the final response.

Common mistakes that keep CORS bugs alive

CORS bugs often survive because teams test for whether the frontend works, not whether untrusted origins are rejected.

A permissive header can look like a quick fix during frontend development and then remain in production.

Treat CORS as a security boundary configuration, not only as a developer-experience setting.

  • Copying development CORS to production — Localhost and wildcard rules added for development should not reach production APIs.
  • Trusting every subdomain — A subdomain takeover or forgotten staging host can become a trusted browser origin.
  • Using contains or endsWith checks — String checks can trust attacker-controlled lookalike domains.
  • Ignoring null origin — null can appear from sandboxed or local contexts and should not be trusted for sensitive data.
  • Adding CORS at multiple layers — CDN, gateway, proxy, and app headers can conflict or create duplicate ACAO values.
  • Assuming wildcard plus credentials works safely — Modern browsers reject wildcard ACAO for credentialed reads, but wildcard CORS is still unsafe for sensitive non-credentialed responses.
  • Forgetting Vary: Origin — Dynamic origin responses without Vary can create cache confusion.
  • Treating CORS as authorization — Backend authorization must still verify the user and action on every request.

How to prioritize CORS findings

Not every CORS finding has the same severity.

A public image CDN with wildcard CORS is usually not urgent. An account API that reflects arbitrary origins and allows credentials is urgent.

Prioritize by endpoint sensitivity, credential behavior, allowed origin validation, data returned, and whether state-changing methods are allowed.

Confirm impact in a browser with a controlled test account before labeling a finding critical.

CORS misconfiguration priority model.
FindingPriorityReason
Origin reflection plus Access-Control-Allow-Credentials: true on account APICriticalAttacker-controlled origin may read authenticated user data
null origin allowed with credentials on sensitive endpointHighSandboxed or unusual origins may read sensitive responses
Weak suffix or substring origin validationHighAttacker-controlled domains may pass validation
Wildcard CORS on sensitive unauthenticated APIMedium to highAny site can read data intended for controlled frontend use
Wildcard CORS on public static assetsLowOften intentional and low impact if data is public
Overbroad methods on authenticated APIMedium to highCan increase risk when paired with CSRF or authorization gaps
Missing Vary: Origin with dynamic ACAOMediumCan create cache-based exposure depending on infrastructure

How ExternalSight helps detect CORS misconfiguration

ExternalSight includes CORS checks as part of its external attack surface monitoring workflow for internet-facing domains.

CORS findings are more useful when they are connected to context. ExternalSight can combine CORS results with related checks such as HTTP headers, CSP, cookie security, API discovery, JavaScript endpoints, redirects, login surface, sensitive files, host header issues, GraphQL, exposed services, subdomains, TLS, and attack-chain evaluation.

That context helps teams separate low-risk public CORS from credentialed exposure on sensitive APIs.

Findings can be classified, included in remediation planning, compared against scan history, exported in reports, and monitored for drift on verified domains using supported plans.

Some external-source checks may report unavailable when API keys or upstream services are not configured. Review scan coverage before treating a clean scan as a clean surface.

ExternalSight does not replace secure application design, framework-level CORS review, browser testing, penetration testing, SIEM, SOC, WAF, or cloud security controls. Its role is to help detect externally visible CORS exposure and keep verified domains under monitoring.

Real-world bug bounty and testing-guide context

CORS misconfiguration is a well-known web security issue, not a theoretical header mistake.

OWASP’s Web Security Testing Guide includes testing for Cross Origin Resource Sharing and explains that Access-Control-Allow-Origin tells the browser which domains are allowed to read the response.

PortSwigger’s Web Security Academy covers CORS vulnerabilities including origin reflection, trusted null origin, and insecure trusted-origin validation.

PortSwigger Research also published practical research on exploiting CORS misconfigurations in bug bounty contexts, helping popularize this class of web vulnerability.

The defensive lesson is consistent across these sources: CORS should be explicit, route-aware, credential-aware, and tested from untrusted origins.

Key takeaways

  • {'text': 'CORS controls whether browser JavaScript can read a cross-origin response; it does not replace authentication or authorization.'}
  • {'text': 'The highest-risk CORS bugs combine sensitive responses, credentialed browser requests, and attacker-controlled allowed origins.'}
  • {'text': 'Never reflect arbitrary Origin values into Access-Control-Allow-Origin.'}
  • {'text': 'Allow credentials only for exact trusted origins and only on routes that truly require credentialed browser access.'}
  • {'text': 'Test CORS with untrusted origins, null origin, weak suffix cases, preflight requests, and controlled browser proof-of-risk checks.'}
  • {'text': 'Use Vary: Origin whenever Access-Control-Allow-Origin changes based on the request Origin.'}

Frequently asked questions

What is CORS misconfiguration?
CORS misconfiguration happens when a server returns cross-origin headers that trust origins too broadly. The most dangerous case is reflecting an attacker-controlled Origin and allowing credentials on sensitive API responses.
Can Access-Control-Allow-Origin: * expose user data?
It can expose non-credentialed responses to any browser origin. Modern browsers do not allow credentialed reads when Access-Control-Allow-Origin is * and credentials are included, but wildcard CORS is still unsafe for sensitive data that should only be read by specific frontends.
Is CORS a replacement for CSRF protection?
No. CORS controls cross-origin response readability in browsers. CSRF protection controls whether cross-site requests can perform unwanted state-changing actions. Sensitive applications often need both correct CORS and CSRF defenses.
How do I test for CORS misconfiguration safely?
Send requests with an untrusted Origin header to domains you own or are authorized to test. Check whether the response reflects that origin and whether Access-Control-Allow-Credentials is true. Then confirm impact in a controlled browser session with a test account.
How do I fix CORS misconfiguration?
Use exact origin allowlists, validate scheme, host, and port, avoid wildcard or reflected origins on sensitive APIs, limit credentials, restrict methods and headers, add Vary: Origin for dynamic CORS, and keep server-side authorization and CSRF protections.

References and further reading

  • MDN — Cross-Origin Resource Sharing — https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS
  • MDN — Access-Control-Allow-Credentials — https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Access-Control-Allow-Credentials
  • WHATWG Fetch Standard — CORS protocol and HTTP caches — https://fetch.spec.whatwg.org/
  • OWASP Web Security Testing Guide — Testing Cross Origin Resource Sharing — https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/11-Client-side_Testing/07-Testing_Cross_Origin_Resource_Sharing
  • OWASP — CORS OriginHeaderScrutiny — https://owasp.org/www-community/attacks/CORS_OriginHeaderScrutiny
  • PortSwigger Web Security Academy — Cross-origin resource sharing — https://portswigger.net/web-security/cors
  • PortSwigger Research — Exploiting CORS misconfigurations for Bitcoins and bounties — https://portswigger.net/research/exploiting-cors-misconfigurations-for-bitcoins-and-bounties
  • Express — CORS middleware — https://expressjs.com/en/resources/middleware/cors.html
  • django-cors-headers — https://pypi.org/project/django-cors-headers/
  • Spring Framework — CORS — https://docs.spring.io/spring-framework/reference/web/webmvc-cors.html

Find unsafe CORS before permissive headers expose sensitive responses

ExternalSight helps teams scan internet-facing domains, detect CORS and related web exposure issues, classify findings, generate remediation plans, compare scan history, receive alerts, export reports, review scan coverage, and monitor verified domains on supported plans. Use it to catch CORS drift before permissive CORS exposes sensitive responses.

Ethan Brooks SENIOR ATTACK SURFACE SECURITY ENGINEER · EXTERNALSIGHT

Find your shadow IT before someone else does

Run a deterministic external scan and get an evidence-backed inventory of every asset attackers can reach.

No agents to install Results in under 2 minutes Signed, audit-ready findings