CORS misconfiguration: how it is exploited, tested and fixed

A CORS misconfiguration lets a website you do not control read responses from your site in a signed-in user's browser. The usual causes are a reflected Origin header, trusting null, a loose suffix or regex check, and a wildcard on internal APIs. Fix it with an exact origin allowlist, Vary: Origin, and credentials only for origins you own.

A CORS misconfiguration is a server policy that tells browsers to let a website you do not control read responses from your site, using the visitor's cookies. The result is the same as an access control bug: any page the victim visits can read their account data, tokens or API output. CWE-942 classifies it as a permissive cross-domain policy with untrusted domains.

Only test systems you own or have written authorization to test. Every example here uses a fictional app on example.com.

What CORS controls, and what it does not#

Browsers apply the same-origin policy: a script on one origin cannot read responses from another origin. CORS is the server's way to relax that rule for chosen origins, with response headers such as Access-Control-Allow-Origin (ACAO) and Access-Control-Allow-Credentials (ACAC). MDN documents the headers and the rules.

CORS does not stop requests from reaching your server. Microsoft's documentation puts it directly: CORS is not a security feature, it relaxes security, and the server runs the request regardless. Curl, scripts and other servers ignore CORS entirely. It only decides whether browser JavaScript on another origin can read the answer.

That is why the credentials header matters most. PortSwigger notes that without Access-Control-Allow-Credentials: true, the victim's browser refuses to send cookies on a cross-origin request, so most attacks fail. A policy that is wide open but ignores cookies leaks only what an anonymous caller could fetch anyway.

How the attack works#

The attacker hosts a page on their own origin and lures a signed-in user to it. The page makes a credentialed request to the target. If the target echoes the attacker's origin and allows credentials, the browser exposes the response to the attacker's script.

Demonstration only: Harbor Notes, a fictional app

This is a demonstration against a fictional app. No real customer system was tested. The app runs at https://notes.example.com. The attacker page is served from https://attacker.example.net.

The vulnerable API answers any origin like this:

GET /api/me HTTP/1.1
Host: notes.example.com
Origin: https://attacker.example.net
Cookie: session=<victim session>

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

{"email":"user@example.com","apiToken":"<redacted>"}

The attacker page needs a few lines of browser code:

fetch("https://notes.example.com/api/me", { credentials: "include" })
  .then(r => r.json())
  .then(data => console.log(Object.keys(data))); // proof: field names only

The server trusted whatever origin the browser reported. The fix is a fixed allowlist, shown below.

The main variants#

The table lists the patterns testers see most. "Exploitable" means that a credentialed cross-origin read from an attacker's origin is possible, according to the cited behavior of the browser and the header rules.

Pattern Response headers Exploitable? Why
Reflected origin with credentials ACAO echoes the request Origin, ACAC true Yes Any origin can read signed-in responses.
Wildcard on a public API ACAO *, no ACAC Usually no Browsers do not attach cookies. Only public data is readable.
Wildcard with credentials ACAO *, ACAC true No in browsers MDN says the browser blocks access to the response. Treat it as a defect and a sign of copy-pasted config.
null origin allowed ACAO null, ACAC true Yes Sandboxed iframes and data: pages send Origin: null. PortSwigger documents this route.
Suffix, prefix or loose regex check ACAO echoes any origin that ends or starts with the trusted name Yes hackersexample.com passes an endsWith("example.com") check. example.com.evil.net passes a prefix check.
Trusting all subdomains ACAO echoes *.example.com origins, ACAC true Depends One XSS bug or subdomain takeover on any subdomain gives read access to the main app.
Wildcard on internal services ACAO * on an intranet host Yes for internal data PortSwigger warns it lets an outside site use an employee's browser as a proxy to the intranet.

Real incidents and CVEs#

Reflected origin with credentials keeps appearing in shipped software. Each entry below is from the National Vulnerability Database (NVD), checked 9 November 2026.

CVE Published Product What NVD says
CVE-2025-53092 16 Oct 2025 Strapi before 5.20.0 Default installs reflect the Origin header into ACAO without a whitelist, so an attacker-controlled site can send credentialed requests. Fixed in 5.20.0.
CVE-2025-62523 27 Oct 2025 PILOS before 4.8.0 Middleware reflects Origin with ACAC set to true and no whitelist.
CVE-2026-89058 18 Sep 2026 RESTEasy CorsFilter With "*" configured, the filter reflects the request Origin and sends ACAC true, so a malicious site can read authenticated responses.

Strapi's entry shows why defaults matter: users who never wrote a CORS rule were still exposed.

How penetration testers detect and prove it#

OWASP WSTG-CLNT-07 describes the method: send requests with different Origin values and read the CORS headers in the reply. A reflected header is a lead. It is not yet a finding.

  1. Send a baseline request to an authenticated endpoint with your own origin, then with an unrelated origin such as https://attacker.example.net. Record both responses.
  2. Run the origin variants: the null origin, a suffix variant of the trusted domain, a prefix variant, a different scheme (http:// instead of https://), and a sibling subdomain.
  3. Run controls beside each test. Send the same requests without cookies. Send them to an endpoint that returns nothing sensitive. A policy that applies to a logo file is not a finding.
  4. Check that Access-Control-Allow-Credentials: true is present, and that session cookies would be sent: look at the cookie's SameSite attribute. A SameSite=Strict or Lax cookie is generally not sent on cross-site fetch requests, which can remove the impact. Confirm it in the browser you test and record it as a limit, not a pass.
  5. Prove the effect in a real browser. Host a test page on a second origin that the authorization covers, sign in with a test-owned account, and read the response from the foreign origin. Log the field names or a redacted value.
  6. Read back from a second account. A finding says: with account A signed in, script on origin B read account A's data, and the same request without cookies returned nothing.

Evidence labels follow our method: "Observed" for the request and response, and "Browser-observed" for the cross-origin read in Chromium. Header reflection with no read of authenticated data is reported at a lower severity, or as an observation. A bare header without a successful read is not proof of impact.

The checks that usually come out clean are worth reporting, for example "wildcard on public endpoints only" or "cookies are SameSite=Lax, so the cross-site read returned an anonymous response."

How to prevent it#

Four rules cover most cases.

  1. Keep a fixed list of exact origins (scheme, host and port). Compare by string equality or parse the URL. Do not use endsWith, startsWith, contains or an unescaped regex.
  2. Never echo the Origin header without checking it against the list. Do not allow null.
  3. Send Access-Control-Allow-Credentials: true only for origins you own. Add Vary: Origin whenever ACAO changes by request origin. MDN requires this so caches do not serve one origin's answer to another.
  4. Do not rely on CORS for access control. Authenticate and authorize every request on the server.

A bad check and a good check in Node.js:

// Bad: matches evilexample.com and example.com.attacker.net patterns
if (origin.endsWith("example.com")) allow(origin);

// Good: exact match against a fixed list
const allowed = new Set(["https://app.example.com", "https://admin.example.com"]);
if (allowed.has(origin)) allow(origin);

Node.js (Express). The cors middleware accepts an origin function. The documentation shows the allowlist pattern, and the middleware adds Vary: Origin for matched origins.

const allowlist = ["https://app.example.com"];
app.use(cors({
  origin: (origin, cb) => cb(null, !origin || allowlist.includes(origin)),
  credentials: true,
}));

Avoid origin: true with credentials: true, because that reflects every origin. Requests without an Origin header (curl, server-to-server) pass in this snippet, which is correct because CORS is a browser control.

Python (Django, django-cors-headers). Use explicit origins and leave the allow-all switch off.

CORS_ALLOWED_ORIGINS = ["https://app.example.com"]
CORS_ALLOW_ALL_ORIGINS = False
CORS_ALLOW_CREDENTIALS = True

Java (Spring). Name the origins in the annotation or in a CorsConfigurationSource. Do not use "*" patterns with credentials.

@CrossOrigin(origins = "https://app.example.com", allowCredentials = "true")
@GetMapping("/api/me")
public Profile me() { ... }

C# (ASP.NET Core). The documentation shows explicit origins with credentials and warns that AllowAnyOrigin is insecure. It also warns that SetIsOriginAllowed(_ => true) with AllowCredentials bypasses the built-in checks.

policy.WithOrigins("https://app.example.com").AllowCredentials();

Go. Compare against a map and set the headers yourself.

var allowed = map[string]bool{"https://app.example.com": true}

func cors(next http.Handler) http.Handler {
  return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    if o := r.Header.Get("Origin"); allowed[o] {
      w.Header().Set("Access-Control-Allow-Origin", o)
      w.Header().Set("Access-Control-Allow-Credentials", "true")
    }
    w.Header().Add("Vary", "Origin")
    next.ServeHTTP(w, r)
  })
}

nginx and gateways. Setting add_header Access-Control-Allow-Origin $http_origin; reflects every origin. Use a map block that returns the origin only when it matches your list. Check that both a CDN and the app are not adding competing CORS headers.

Two further defenses limit damage when a policy is wrong. Set session cookies with SameSite=Lax or Strict, and keep sensitive APIs on authorization tokens in headers rather than cookies.

CORS compared with CSRF and CSP#

CORS CSRF CSP
What it is A browser relaxation of the same-origin read rule An attack that sends a state-changing request from a signed-in browser A header that limits what a page may load and run
Failure result Attacker page reads private responses Attacker causes an action Injected script runs more easily
Main fix Exact origin allowlist SameSite cookies, anti-forgery tokens Strict policy, nonces

A bad CORS policy can also defeat CSRF tokens, because the attacker's page can read the token from a response. Test them together. See CSRF testing and content security policy.

Severity: real finding or noise#

Scanners often flag CORS headers with low severity. PortSwigger's scanner issue for CORS is classified as informational with CWE-942. Rate by what the read exposes:

  • High: a reflected or trusted-attacker origin with credentials, on an endpoint that returns personal data, tokens or admin functions.
  • Medium or low: the same pattern on endpoints with limited data, or where SameSite cookies stop the browser from sending credentials.
  • Observation: wildcard on public, cookie-free resources.

Testing checklist#

  • List every endpoint that returns private data and read its CORS headers.
  • Send foreign, null, suffix, prefix, http:// and sibling subdomain origins.
  • Confirm ACAC is true only for origins the team owns.
  • Check Vary: Origin on every response whose ACAO varies.
  • Check preflight (OPTIONS) responses match the policy on the real request.
  • Compare signed-in and anonymous responses to the same origin.
  • Check the session cookie's SameSite and Domain attributes.
  • Test intranet and admin hosts for *.
  • Prove any finding with a read from a second origin in a browser, with a redacted body.
  • Confirm all subdomains in the allowlist are owned and free of takeover risk.

When to get a professional test#

CORS bugs hide in gateways, CDNs and framework defaults, so the policy a developer wrote may not be the policy the browser sees. A professional web application test reads the real headers on every authenticated endpoint, proves the cross-origin read with a second account, and reports the controls that held. For web apps see web application penetration testing. For APIs that use token or cookie auth see API penetration testing. Testing starts only after written authorization from the system owner.

Questions

Is CORS a security feature?

No. CORS relaxes the browser's same-origin policy. Microsoft's ASP.NET Core documentation says an API is not safer by allowing CORS. The server still receives and runs every request. CORS only decides whether JavaScript on another origin may read the response.

What is the difference between CORS and CSRF?

CSRF makes a signed-in browser send a state-changing request. The attacker usually cannot read the reply. A CORS misconfiguration lets the attacker's page read the reply, so it leaks data and can also read CSRF tokens. Fix both: allowlist origins and use SameSite cookies or tokens.

Is Access-Control-Allow-Origin: * a vulnerability?

On a public API with no cookies it is often fine. Browsers refuse to expose a response that carries credentials when the value is *, so the wildcard alone does not expose signed-in data. It becomes a problem on internal or intranet services, where PortSwigger notes it can turn a visitor's browser into a proxy.

Does CORS replace a Content Security Policy?

No. They control different things. CORS governs which origins may read responses from your server. CSP governs what a page you serve may load and run. See the content security policy guide on this site.

Why does the browser send a preflight request?

For requests that are not simple, such as a PUT or a JSON content type, the browser first sends an OPTIONS request to ask which methods and headers are allowed. A correct preflight response does not make an unsafe policy safe. The allowlist decision is the same.

Sources

  1. MDN: Cross-Origin Resource Sharing (CORS) (checked 9 November 2026)
  2. PortSwigger Web Security Academy: Cross-origin resource sharing (CORS) (checked 9 November 2026)
  3. OWASP WSTG: Testing Cross Origin Resource Sharing (WSTG-CLNT-07) (checked 9 November 2026)
  4. PortSwigger Scanner issue: Cross-origin resource sharing (checked 9 November 2026)
  5. CWE-942: Permissive Cross-domain Policy with Untrusted Domains (checked 9 November 2026)
  6. Microsoft Learn: Enable Cross-Origin Requests (CORS) in ASP.NET Core (checked 9 November 2026)
  7. Express cors middleware documentation (checked 9 November 2026)
  8. NVD: CVE-2025-53092 (Strapi) (checked 9 November 2026)
  9. NVD: CVE-2025-62523 (PILOS) (checked 9 November 2026)
  10. NVD: CVE-2026-89058 (RESTEasy CorsFilter) (checked 9 November 2026)