CORS Policy: Tight by Default
CORS misconfiguration is a security gap.
Default
CORS (Cross-Origin Resource Sharing) is one of those security mechanisms that looks like a header configuration and acts like a security boundary. A misconfigured CORS policy is a security gap that allows arbitrary origins to make authenticated requests to your API. The most common configuration mistake (Access-Control-Allow-Origin: *) is also the most dangerous, and it is depressingly common.
What a tight CORS policy looks like:
- Allow specific origins, not '*'.: The Access-Control-Allow-Origin header should list specific origins explicitly: https://app.example.com, https://admin.example.com. Wildcard is appropriate only for genuinely public APIs that do not handle authenticated requests, and it is rarely the right answer for product APIs.
- Reject untrusted origins.: Requests from origins not on the allowlist do not just have CORS headers omitted; they get rejected. The rejection prevents the browser from making the request in the first place. The allowlist is the only sanctioned set of origins; everything else fails closed.
- No reflection of arbitrary Origin header.: A common mistake is to read the request's Origin header and echo it back as Access-Control-Allow-Origin. This is functionally equivalent to allowing all origins; an attacker can send any Origin and have it allowed. The allowlist must be a static list, not a dynamic one.
- Per-environment configuration.: Production allows the production frontend origin. Staging allows the staging frontend. Dev allows localhost. The CORS configuration per environment matches the consumer per environment; treating them all the same is how dev origins get accidentally allowed in production.
- Credentials require strict origin handling.: When an API supports cookies or auth headers across origins (Access-Control-Allow-Credentials: true), the origin allowlist must be tight. The browser refuses to enforce credentials with wildcard origins, which the spec correctly mandates as a safety net. Do not try to circumvent.
The default that most teams should ship with is "no CORS at all" (same-origin only), then explicitly opening up the specific cross-origin needs the product has. Defaulting to permissive and tightening later inverts the security posture.
Methods
The methods allowed by CORS policy is the second axis of the configuration. Each allowed method is a class of operation that cross-origin requests can perform. Wide-open method allowlists turn CORS misconfiguration into a much larger problem.
- Only allowed methods.: The Access-Control-Allow-Methods header lists exactly the methods the API needs to support cross-origin. GET and POST for typical web frontends. Any additional method requires an explicit reason.
- Don't allow PUT/DELETE without need.: PUT, DELETE, and PATCH are mutating methods. Allowing them cross-origin opens up a wider attack surface. Many APIs do not actually need them cross-origin (the frontend submits them through same-origin paths). Default to disallowing; allow only when the architecture genuinely requires.
- Per-route allowlists.: Different routes need different methods. The /search endpoint needs GET. The /admin/users endpoint needs POST and DELETE. The CORS configuration can be per-route, which is more work to maintain but more secure than a blanket allowlist.
- Preflight handling.: Browsers send a preflight OPTIONS request before non-simple cross-origin requests. The preflight response declares which methods are allowed. The handler must return the preflight response correctly without invoking the actual route handler. Misconfiguration here either breaks the API or leaks information about route existence.
- Headers allowlist matters too.: Access-Control-Allow-Headers lists which custom headers the cross-origin request can send. Authorization, Content-Type, custom company headers. Each entry on the allowlist is reviewed for whether the cross-origin scenario actually needs it.
The method and header allowlists are where most CORS hardening happens after the origin allowlist is in place. Each entry is a deliberate decision, not a default.
Audit
CORS configuration drifts. New routes get added with default permissive policies. Old routes accumulate exceptions that nobody remembers why. The discipline that keeps the policy tight is regular audit, with automated tooling where possible.
- Quarterly: any CORS misconfig?.: Once a quarter, run an automated audit across every route in the API. Flag any route that allows '*' as origin, any route that allows methods beyond what the consuming frontend uses, any route that disagrees with the documented allowlist. The audit produces a list; the list gets remediated.
- Catches drift.: The audit catches the case where a developer added a new route with copy-pasted boilerplate from an old route that had wider CORS than necessary. Or where a route was tightened in dev but the production config was not updated. Drift is the natural state; the audit is the corrective force.
- Continuous scanning, not just quarterly.: The audit runs on every PR, not just every quarter. CORS configuration changes get scrutinized when they are introduced, when the cost of fixing is lowest. The quarterly review is a backstop for changes that slipped through.
- Document the allowlist.: The list of origins, methods, and headers each route allows is documented next to the route. The docs are the source of truth; the actual configuration is verified to match. When the two diverge, one of them is wrong.
- External penetration testing.: CORS misconfiguration is a class of issue that external pentests routinely find. Including it in the scope of every annual pentest catches the mistakes the internal audit missed. The two layers of review compound.
CORS is one of those security boundaries that is easy to misconfigure and hard to debug after a breach. Nova AI Ops integrates with API gateway audit streams, surfaces CORS configuration anomalies (wildcard origins, unusually permissive method lists, mismatched per-environment configs) and tracks the policy posture across the API surface so the audit happens continuously instead of quarterly.