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 in API Gateway, CloudFront, or Nginx, or reviewing a cross-origin browser-facing endpoint.
Scanned 9/6/2026
Install to Claude Code
npx -y skills add ShieldNet-360/secure-vibe --skill cors-security --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Cors Security?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/shieldnet-360-cors-security-secure-vibe)More formats (shields.io, HTML) on the badges page.
---
name: cors-security
description: "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 in API Gateway, CloudFront, or Nginx, or reviewing a cross-origin browser-facing endpoint."
---
<!-- Native skill bundle for Claude Code. Generated by `secure-vibe dev regenerate`. -->
<!-- Do not edit by hand; the source of truth is skills/cors-security/SKILL.md. -->
# 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 in API Gateway, CloudFront, or Nginx, or reviewing a cross-origin browser-facing endpoint.
## 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 with `Access-Control-Allow-Credentials: true` and a **single explicit origin**. On a credentialed request every CORS header loses its wildcard meaning: `*` in `Allow-Origin`, `Allow-Methods`, `Allow-Headers`, or `Expose-Headers` is 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")` accepts `https://example.com.evil.com`, and an unescaped `.` in `^https://api.example.com$` accepts `https://apiXexample.com`.
- Include `Vary: Origin` on any response whose headers depend on the request `Origin`, so a shared cache cannot hand one origin's `Access-Control-Allow-Origin` to another.
- Restrict `Access-Control-Allow-Methods` to the methods the endpoint accepts and `Access-Control-Allow-Headers` to the headers it consumes. An app-set `Authorization: Bearer …` header is not "credentials" in the Fetch sense — it is an ordinary header that must be listed in `Allow-Headers` and that triggers a preflight.
- Set a bounded `Access-Control-Max-Age` sized 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 `Origin` is not in the allowlist, omit `Access-Control-Allow-Origin` entirely. 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 `Origin` header 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 `null` origin on a credentialed or sensitive endpoint. Every browser serializes an opaque origin to `null` — sandboxed iframes without `allow-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.
## Reference files
Read these only when the task calls for them.
- `references/verifying-findings.md`
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!