CORS best practices

Security and performance best practices for Cross-Origin Resource Sharing (CORS) including origin restrictions, preflight caching, and credential handling.

Best practices for configuring Cross-Origin Resource Sharing (CORS) to balance security, performance, and functionality in production environments.

Avoid the CORS Wildcard Origin on Private APIs

Cross-Origin Resource Sharing (CORS) permits the wildcard * for Access-Control-Allow-Origin, but private APIs should never use it. The wildcard grants every website on the internet access to the API response. An attacker's page can read sensitive data from the API if the user is authenticated through cookies or session tokens.

Set Access-Control-Allow-Origin to the specific origin that needs access. Maintain an allowlist of permitted origins on the server and validate each incoming Origin header against it. Return the matched origin in the response header and include Vary: Origin to prevent cache poisoning.

Restrict CORS Allowed Methods to Required Operations

Cross-Origin Resource Sharing (CORS) should expose only the HTTP methods the client application needs. A front-end that only reads data should receive Access-Control-Allow-Methods: GET-- not GET, POST, PUT, DELETE, PATCH. Overly permissive method lists increase the attack surface by letting malicious scripts issue state-changing requests.

Audit the API endpoints each client calls and set Access-Control-Allow-Methods per route or location block. Separate read-only resources from write endpoints and apply different CORS policies to each.

Restrict CORS Allowed Headers to Required Headers

Cross-Origin Resource Sharing (CORS) should list only the request headers the application sends. Avoid using the * wildcard for Access-Control-Allow-Headers when credentials are enabled, because the Authorization header does not respond to the wildcard in credentialed requests. List each required header explicitly: Content-Type, Authorization, and any custom headers the API expects.

Cache CORS Preflight Responses with Access-Control-Max-Age

Cross-Origin Resource Sharing (CORS) preflight requests add latency to every non-simple cross-origin call. The Access-Control-Max-Age header lets the browser cache the preflight response and skip repeated OPTIONS requests. Set the value to 86400 (24 hours) for stable APIs where the CORS policy does not change frequently.

Different browsers enforce different maximum values. Chrome caps Access-Control-Max-Age at 7200 seconds (2 hours). Firefox caps it at 86400 seconds (24 hours). Set the header to the highest value your API tolerates and let each browser apply its own limit.

Handle CORS Preflight OPTIONS Requests Explicitly

Cross-Origin Resource Sharing (CORS) preflight requests use the HTTP OPTIONS method. Configure the server to respond to OPTIONS requests with the CORS headers and a 204 No Content status. Do not route OPTIONS requests through application logic, authentication middleware, or rate limiters. A blocked or slow preflight response prevents the actual request from reaching the server.

Return the CORS headers on the OPTIONS response at the web server or reverse proxy level (Nginx, Apache, or a load balancer). This approach avoids unnecessary processing in the application layer.

Never Combine CORS Wildcard Origin with Credentials

Cross-Origin Resource Sharing (CORS) does not allow Access-Control-Allow-Origin: * when Access-Control-Allow-Credentials: true. The browser rejects this combination and blocks the response. The server must return an explicit origin when credentials are enabled.

Reflect the validated Origin request header back in the Access-Control-Allow-Origin response header after checking it against the allowlist. Include Vary: Origin to ensure CDNs and intermediate caches serve the correct header for each origin.

Set the Vary: Origin Header When CORS Origin Changes Per Request

Cross-Origin Resource Sharing (CORS) responses that change Access-Control-Allow-Origin based on the request's Origin header must include Vary: Origin. Without this header, a CDN or browser cache may store the response for one origin and serve it to a different origin. The mismatched Access-Control-Allow-Origin value causes the browser to block the response.

Configure CORS at One Layer Only

Cross-Origin Resource Sharing (CORS) headers set by both the web server (Nginx, Apache) and the application framework (Django, Express, Spring) produce duplicate headers. A response with two Access-Control-Allow-Origin headers causes the browser to reject the request. Configure CORS at the reverse proxy layer or the application layer -- not both.

If the application sets CORS headers, use proxy_hide_header Access-Control-Allow-Origin in Nginx to strip the duplicate. If the reverse proxy sets CORS headers, disable the CORS middleware in the application.

Validate CORS Origins with Exact Matching

Cross-Origin Resource Sharing (CORS) origin validation must use exact string matching or a strict regular expression with start and end anchors. A regex like https://example\.com without a $ anchor matches https://example.com.attacker.com, which lets an attacker's domain pass the check. Append $ to the regex: ^https://example\.com$.

Avoid substring matching, prefix matching, or partial hostname checks when validating origins. Maintain a static allowlist when possible and fall back to anchored regex only for dynamic subdomain patterns.