cors-security
Strict CORS: no wildcard with credentials, exact-match origin allowlists, sane preflight cache, minimal exposed headers. Use when generating CORS middleware or framework config, setting CORS headers i
SKILL.mdskills/cors-security/SKILL.md
GitHubCORS Security
Rules (for AI agents)
ALWAYS
- For any non-public endpoint, allow origins from an explicit allowlist.
*is correct only when the resource is intentionally public and cross-origin access never carries credentials. - When a cross-origin request carries credentials — the browser's credentials mode is
include, meaning cookies, TLS client certificates, or browser-managed HTTP auth — respond withAccess-Control-Allow-Credentials: trueand a single explicit origin. On a credentialed request every CORS header loses its wildcard meaning:*inAllow-Origin,Allow-Methods,Allow-Headers, orExpose-Headersis read as the literal string"*"and the browser blocks the response. That is why "just set it to*for now" fails in a way that is hard to debug. - Compare origins by exact match on the serialized origin — scheme, host, and port,
all three. Never substring, prefix, or suffix matching:
endsWith("example.com")acceptshttps://example.com.evil.com, and an unescaped.in^https://api.example.com$acceptshttps://apiXexample.com. - Include
Vary: Originon any response whose headers depend on the requestOrigin, so a shared cache cannot hand one origin'sAccess-Control-Allow-Originto another. - Restrict
Access-Control-Allow-Methodsto the methods the endpoint accepts andAccess-Control-Allow-Headersto the headers it consumes. An app-setAuthorization: Bearer …header is not "credentials" in the Fetch sense — it is an ordinary header that must be listed inAllow-Headersand that triggers a preflight. - Set a bounded
Access-Control-Max-Agesized to the deployment; an hour to a day is a conservative production default. Browsers cap this value regardless of what you send, so a very large number buys no extra caching — it only lengthens how long a bad allowlist entry stays cached in clients that already fetched it. - Source allowed origins only from operator-controlled configuration. Do not derive them from tenant- or user-controlled data unless an addition passes an authenticated, authorized, validated administrative workflow. The risk is not "stored in a database", it is "writable by anyone who can create a row".
- When the request
Originis not in the allowlist, omitAccess-Control-Allow-Originentirely. Do not fall back to a default trusted origin and do not echo the rejected value — a default-origin fallback silently grants every rejected caller whatever that default origin is allowed to do.
NEVER
- Reflect the
Originheader without an allowlist check (Access-Control-Allow-Origin: <Origin>for every caller). With credentials this is strictly worse than*: the browser refuses*, but accepts a reflected origin, so the misconfiguration fails open instead of closed. - Accept the serialized
nullorigin on a credentialed or sensitive endpoint. Every browser serializes an opaque origin tonull— sandboxed iframes withoutallow-same-origin,data:URLs,file://documents, some cross-origin redirects. Treat any exception as a documented compatibility decision that passed security review. - Allow arbitrary subdomains (
.*\.example\.com$) without accounting for subdomain takeover. Exact matching does not help here: the pattern is implemented correctly and a dangling DNS record hands an attacker a matching origin. Pin specific subdomains; treat a wildcard as a decision tied to subdomain-ownership controls. - Expose internal headers via
Access-Control-Expose-Headers. Limit it to the minimal set the frontend genuinely reads. - Use CORS as authorization. It is a browser policy: it does not stop curl, server-to-server calls, or any non-browser client. Authenticate the request.
KNOWN FALSE POSITIVES
- Intentionally public, non-credentialed resources legitimately use
Access-Control-Allow-Origin: *— open-data APIs, and static assets served for cross-origin reuse (fonts, images, JS on a CDN). This is the common correct case for*. - A few integrations (Stripe.js, Plaid, Auth0) expect specific CORS headers; read the provider's CORS section before relaxing the baseline.
Context (for humans)
CORS is widely misunderstood as a security control. It isn't — it's a relaxation of the same-origin policy. The security control is authentication. CORS misconfiguration matters because, when combined with cookies or credentialed requests, it gives untrusted origins the ability to make cross-origin requests and read the response.
Two failure modes account for most real findings: an origin check that matches loosely (suffix or unescaped-regex matching), and a mismatch path that falls back to reflecting or defaulting instead of omitting the header. Both look correct in review and both fail open.
References
references/verifying-findings.md— confirm or refute a finding, then lock itrules/cors_safe_config.json- OWASP CORS Origin Header Scrutiny.
- CWE-942.
- Fetch — CORS protocol.