# CORS misconfiguration: how it is exploited, tested and fixed

Source: https://penetrationtestservices.com/vulnerabilities/cors-misconfiguration/
Publisher: Excession Labs (penetrationtestservices.com)
Last updated: 2026-11-09

CORS misconfiguration explained: reflected origins, null, weak origin checks and credentials. How testers prove a cross-origin read, plus safe config for five stacks.

> **Short answer:** 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:
> 
> ```http
> GET /api/me HTTP/1.1
> Host: notes.example.com
> Origin: https://attacker.example.net
> Cookie: 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":""}
> ```
> 
> The attacker page needs a few lines of browser code:
> 
> ```js
> 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:

```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.

```js
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.

```python
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.

```java
@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.

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

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

```go
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](/guides/csrf-testing/) and [content security policy](/vulnerabilities/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](/web-application-penetration-testing/). For APIs that use token or cookie auth see [API penetration testing](/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

- [MDN: Cross-Origin Resource Sharing (CORS)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS)
- [PortSwigger Web Security Academy: Cross-origin resource sharing (CORS)](https://portswigger.net/web-security/cors)
- [OWASP WSTG: Testing Cross Origin Resource Sharing (WSTG-CLNT-07)](https://wstg.owasp.org/v4.2/4-Web_Application_Security_Testing/11-Client-side_Testing/07-Testing_Cross_Origin_Resource_Sharing)
- [PortSwigger Scanner issue: Cross-origin resource sharing](https://portswigger.net/kb/issues/00200600_cross-origin-resource-sharing)
- [CWE-942: Permissive Cross-domain Policy with Untrusted Domains](https://cwe.mitre.org/data/definitions/942.html)
- [Microsoft Learn: Enable Cross-Origin Requests (CORS) in ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/security/cors)
- [Express cors middleware documentation](https://expressjs.com/en/resources/middleware/cors.html)
- [NVD: CVE-2025-53092 (Strapi)](https://nvd.nist.gov/vuln/detail/CVE-2025-53092)
- [NVD: CVE-2025-62523 (PILOS)](https://nvd.nist.gov/vuln/detail/CVE-2025-62523)
- [NVD: CVE-2026-89058 (RESTEasy CorsFilter)](https://nvd.nist.gov/vuln/detail/CVE-2026-89058)
