Cross-Origin Resource Sharing (CORS) controls which external domains are allowed to interact with your web application or API. When configured carelessly, CORS policies become one of the most common and impactful web security misconfigurations — granting untrusted sites access to authenticated data, session tokens, and sensitive endpoints. This guide is for developers, API engineers, and technical managers who want to understand CORS from the ground up and implement it in a way that is both functional and secure.
Understand What CORS Does and Why It Matters
Browsers enforce the Same-Origin Policy (SOP) by default, blocking JavaScript on one origin (e.g. https://app.example.com) from reading responses from a different origin (e.g. https://api.example.com). CORS is the mechanism that selectively relaxes this restriction by instructing browsers which cross-origin requests are permitted. If you misconfigure CORS, you effectively punch holes in the SOP for every user of your application.
- Understand that CORS headers are consumed by browsers — they do not protect server-to-server or curl-based requests.
- Know the difference between simple requests (GET, POST with basic content types) and preflighted requests (OPTIONS preflight for complex methods or headers).
- Recognise that a permissive CORS policy combined with
Access-Control-Allow-Credentials: truecan let an attacker's site read authenticated responses on behalf of your users. - Map every endpoint that returns sensitive or authenticated data — these require the strictest CORS rules.
Define an Explicit Allowlist of Trusted Origins
The most critical decision in CORS configuration is deciding which origins are trusted. A wildcard (*) tells the browser that any website may read the response — this is acceptable only for fully public, unauthenticated resources such as open CDN assets or public APIs that carry no user-specific data.
- Never use
Access-Control-Allow-Origin: *on endpoints that require authentication or return personalised data. - Maintain a server-side allowlist of permitted origins (e.g.
https://app.example.com,https://admin.example.com). - When responding to a cross-origin request, dynamically check the incoming
Originheader against your allowlist and echo back the exact matched origin — do not echo arbitrary origins. - Include only production origins in your allowlist; handle development/staging origins via environment-specific configuration, never in production code.
- Treat subdomains as separate origins —
https://evil.example.comis distinct fromhttps://app.example.com; do not use broad subdomain wildcards like*.example.comunless you fully control all subdomains.
Restrict Allowed HTTP Methods
The Access-Control-Allow-Methods header tells the browser which HTTP methods are permitted in cross-origin requests. Listing every method increases the attack surface unnecessarily.
- Only include methods your API genuinely needs — e.g.
GET, POST, PUTrather than a blanketGET, POST, PUT, PATCH, DELETE, OPTIONS. - Remove
TRACEandCONNECTentirely; these are rarely needed and can assist certain attack classes. - Confirm your application framework or reverse proxy is not automatically adding permissive method lists without your knowledge.
Control Allowed and Exposed Headers
Two separate headers govern which headers cross-origin requests may carry and which response headers JavaScript may read. Both should be as restrictive as possible.
- Set
Access-Control-Allow-Headersto only the headers your API actually reads from incoming requests (e.g.Content-Type, Authorization, X-Requested-With). - Do not reflect the incoming
Access-Control-Request-Headersvalue directly back without validation — this can allow clients to inject arbitrary headers. - Use
Access-Control-Expose-Headerssparingly; only expose non-sensitive custom headers that client-side JavaScript genuinely needs to read (e.g.X-Request-ID). - Never expose headers containing tokens, session identifiers, or internal routing information.
Handle Credentials with Extra Care
Setting Access-Control-Allow-Credentials: true instructs the browser to include cookies, HTTP authentication, and TLS client certificates in cross-origin requests and to expose the response to JavaScript. This combination dramatically raises the stakes of any CORS misconfiguration.
- Only set
Access-Control-Allow-Credentials: truewhen your application genuinely requires it (e.g. session-cookie–authenticated APIs consumed by a specific trusted front-end). - When credentials are enabled, you must specify an explicit origin — the browser will ignore
Access-Control-Allow-Credentials: trueif the origin is*, and many frameworks will incorrectly fall back to a wide-open policy. - Audit cookie attributes alongside CORS: use
SameSite=StrictorSameSite=Lax,Secure, andHttpOnlyto add defence-in-depth even if CORS is misconfigured. - If an endpoint does not require credentials, ensure
Access-Control-Allow-Credentialsis omitted or set tofalse.
Set a Sensible Preflight Cache Duration
For preflighted requests, browsers send an OPTIONS request before the actual request. The Access-Control-Max-Age header tells the browser how long (in seconds) it may cache the preflight response, reducing latency. Setting this too high means browsers will cache a stale or permissive policy even after you have tightened your configuration.
- Set
Access-Control-Max-Ageto a reasonable value — typically between300(5 minutes) and7200(2 hours) depending on how frequently your policy changes. - Avoid values above
86400(24 hours); most browsers cap the value anyway, but unnecessarily long caching delays policy updates. - During active development or policy changes, reduce
Max-Agetemporarily to force browsers to re-validate.
Validate Origins Server-Side
Browser enforcement of CORS is a client-side control. Malicious actors using curl, scripts, or modified browsers can bypass it entirely and hit your API directly. CORS does not replace authentication, authorisation, or input validation — it complements them.
- Require proper authentication (e.g. Bearer tokens, signed JWTs, session validation) on all sensitive endpoints regardless of CORS policy.
- Implement server-side origin validation: parse the
Originheader, check it against your allowlist, and reject or log requests from unexpected origins. - Do not trust the
RefererorOriginheaders as the sole access control mechanism — they can be spoofed in non-browser contexts. - Apply rate limiting and anomaly detection on your API independently of CORS logic.
- Ensure your reverse proxy (Nginx, Apache, Caddy, etc.) is not adding its own permissive CORS headers that override your application-level policy.
Test and Monitor Your CORS Configuration
CORS misconfigurations are notoriously easy to introduce and hard to spot through code review alone. Systematic testing — both manual and automated — is essential to catch issues before attackers do. Sensagraph continuously scans your web application for CORS misconfigurations, including overly permissive origins, credential exposure, and wildcard policies on authenticated endpoints.
- Manually test by sending cross-origin requests with crafted
Originheaders (e.g. an untrusted domain) and inspecting the response headers to ensure your server does not echo them back. - Test with both a matching trusted origin and a non-matching origin to confirm your dynamic allowlist logic works correctly.
- Test credentialed requests explicitly — send requests with
withCredentials: truefrom an untrusted origin and verify the browser blocks the response. - Include CORS header validation in your CI/CD pipeline using integration tests that assert correct header values for each endpoint.
- Review server and proxy logs for unexpected OPTIONS preflight requests from unknown origins, which may indicate probing.
- Re-audit your CORS configuration whenever you add new endpoints, change authentication mechanisms, or onboard new front-end consumers.
- Use automated continuous scanning to detect regressions — CORS policies can silently regress when dependencies or framework defaults change.