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.
| Question | Correct answer | Security implication |
|---|---|---|
| Does CORS authenticate users? | No | Use normal server-side authentication and authorization |
| Does CORS stop all cross-origin requests? | No | It controls whether browser JavaScript can read the response |
| Does CORS protect APIs from curl or server-side clients? | No | APIs must enforce authorization on the server |
| Can CORS expose sensitive user data? | Yes, when misconfigured with trusted credentials and sensitive responses | A malicious origin may read responses in the victim’s browser |
| Can CORS replace CSRF protection? | No | State-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.
| Header | Direction | Purpose |
|---|---|---|
| Origin | Request | Tells the server which origin initiated the browser request |
| Access-Control-Allow-Origin | Response | Tells the browser which origin may read the response |
| Access-Control-Allow-Credentials | Response | Tells the browser whether credentialed cross-origin responses may be exposed |
| Access-Control-Allow-Methods | Preflight response | Lists methods allowed for the actual request |
| Access-Control-Allow-Headers | Preflight response | Lists request headers allowed for the actual request |
| Access-Control-Max-Age | Preflight response | Tells the browser how long it may cache the preflight result |
| Vary: Origin | Response | Prevents 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.
| Response type | CORS risk | Reason |
|---|---|---|
| Public static asset | Low | The data is intended to be public |
| Unauthenticated public API | Low to medium | Any origin may read data, but data classification still matters |
| Authenticated account API | High | User-specific data may be exposed to attacker-controlled JavaScript |
| Admin API | Critical | Privileged data or actions may be exposed through an authenticated browser session |
| State-changing API | High | CORS may combine with missing CSRF protection or weak authorization |
| Token-returning endpoint | Critical | Session, 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.
| Misconfiguration | Example | Why it is risky |
|---|---|---|
| Origin reflection | Access-Control-Allow-Origin mirrors any Origin value | Any attacker-controlled site can become an allowed origin |
| Credentials with reflected origin | Access-Control-Allow-Credentials: true plus reflected Origin | Browser may expose authenticated responses to attacker JavaScript |
| Wildcard on sensitive data | Access-Control-Allow-Origin: * | Any site can read non-credentialed sensitive or semi-sensitive data |
| Trusting null origin | Access-Control-Allow-Origin: null | Sandboxed documents and local contexts can use null origin behavior |
| Weak suffix checks | Allow origins ending with example.com | attackerexample.com or example.com.attacker.net may pass bad validation |
| Weak substring checks | Allow any origin containing example.com | https://example.com.attacker.net may be accepted |
| Overbroad methods | Allow PUT, PATCH, DELETE for untrusted origins | Increases risk when combined with weak auth or CSRF gaps |
| Missing Vary: Origin | Dynamic ACAO without Vary | Shared 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.
| Step | Browser action | Defensive control |
|---|---|---|
| Victim has an active session | Browser stores valid session cookies | Use SameSite cookies, MFA for sensitive actions, and short-lived sessions |
| Attacker page sends cross-origin request | Browser includes Origin header | Do not trust Origin as authentication |
| Target API receives request | Server evaluates CORS policy | Use exact allowlist validation |
| Target API responds | CORS headers decide whether JavaScript can read the response | Never reflect arbitrary Origin values |
| Browser enforces CORS | Response is exposed or blocked | Avoid credentials unless required and scoped |
| Data is read by attacker JavaScript | Sensitive response becomes accessible cross-origin | Classify 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.
| Fix | How to apply it | Why it matters |
|---|---|---|
| Use exact origin allowlists | Allow https://app.example.com, not *.example.com unless every subdomain is equally trusted | Prevents attacker-controlled lookalike origins from being accepted |
| Validate scheme, host, and port | Treat http://, https://, and custom ports as separate origins | Prevents accidental trust expansion |
| Do not reflect Origin blindly | Return ACAO only after allowlist match | Stops arbitrary attacker origins from becoming trusted |
| Limit credentials | Use Access-Control-Allow-Credentials: true only when cross-origin cookies or auth are truly required | Reduces authenticated data exposure |
| Restrict methods | Allow only required methods per route | Limits dangerous cross-origin request shapes |
| Restrict headers | Allow only required request headers | Prevents unnecessary authorization or custom-header exposure |
| Add Vary: Origin | Use it whenever ACAO changes by request Origin | Prevents cache confusion |
| Separate public and private APIs | Use different CORS rules for public data and authenticated account data | Avoids applying broad public CORS to sensitive routes |
| Keep CSRF protection | Use CSRF tokens, SameSite cookies, and authorization checks for state-changing requests | CORS 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.
| Finding | Priority | Reason |
|---|---|---|
| Origin reflection plus Access-Control-Allow-Credentials: true on account API | Critical | Attacker-controlled origin may read authenticated user data |
| null origin allowed with credentials on sensitive endpoint | High | Sandboxed or unusual origins may read sensitive responses |
| Weak suffix or substring origin validation | High | Attacker-controlled domains may pass validation |
| Wildcard CORS on sensitive unauthenticated API | Medium to high | Any site can read data intended for controlled frontend use |
| Wildcard CORS on public static assets | Low | Often intentional and low impact if data is public |
| Overbroad methods on authenticated API | Medium to high | Can increase risk when paired with CSRF or authorization gaps |
| Missing Vary: Origin with dynamic ACAO | Medium | Can 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.