What Is CORS and Why Does It Matter?
Cross-Origin Resource Sharing (CORS) is a browser security mechanism that restricts web pages from making requests to a different domain than the one that served the page. Without CORS headers, a JavaScript application on app.example.com cannot fetch data from api.example.com — the browser blocks the response.
The browser enforces CORS; servers simply signal their policy via response headers. An API server that wants to allow cross-origin requests must respond with Access-Control-Allow-Origin and related headers.
CORS is purely a browser enforcement mechanism — it does not protect APIs from server-to-server calls, curl, or native apps. It only controls browser behavior.
Why Centralize CORS at the Gateway?
When each microservice handles its own CORS configuration:
- One service might return
Access-Control-Allow-Origin: *while another requires specific origins — inconsistency causes mysterious browser errors - Preflight (OPTIONS) requests reach backend services unnecessarily, consuming resources for what is essentially a gateway-level concern
- CORS policy changes (adding a new allowed origin) require updating every service
Handling CORS at the gateway solves all three problems: consistent policy, preflights answered without hitting backends, and a single configuration point.
How Preflight Works
For requests with custom headers or non-simple methods, browsers send an OPTIONS "preflight" request before the actual request to check if the server allows it:
# Browser sends preflight:
OPTIONS /api/users HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization
# Gateway responds:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
Vary: Origin
The gateway intercepts OPTIONS requests, responds immediately without contacting the backend, and the browser proceeds with the actual request.
Access-Control-Max-Age: Preflight Caching
Access-Control-Max-Age tells the browser how long to cache the preflight response in seconds. Set this to a large value (86400 = 24 hours) to minimize preflight overhead — browsers will not re-send OPTIONS for the same request pattern within the cache window. Note: Chrome caps this at 7200 seconds regardless of the value.
Origin Configuration
Specific Origin Whitelist
The most secure approach: only allow explicitly listed origins:
# Kong CORS plugin — specific origins
plugins:
- name: cors
config:
origins:
- https://app.example.com
- https://admin.example.com
- https://partner.other.com
methods: [GET, POST, PUT, DELETE, OPTIONS]
headers: [Content-Type, Authorization, X-Request-ID]
max_age: 86400
credentials: true
The gateway compares the Origin request header against the whitelist and reflects the matching origin in Access-Control-Allow-Origin. This is important: you cannot return multiple origins in Access-Control-Allow-Origin — you must reflect the specific requesting origin. Always include Vary: Origin to prevent caches from serving one client's CORS headers to a different-origin client:
Vary: Origin
Access-Control-Allow-Origin: https://app.example.com ← reflected, not wildcard
Wildcard Origin
Access-Control-Allow-Origin: *
Wildcard allows any origin. Use it only for truly public, read-only APIs — public datasets, open APIs, CDN-served assets. Wildcard cannot be combined with Access-Control-Allow-Credentials: true. Browsers will reject the response:
# This combination is invalid and will be blocked by browsers:
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true ← INVALID
Dynamic Origin Reflection (and Its Risks)
Some gateways reflect the Origin header back unconditionally — regardless of whether it is in a whitelist. This is dangerous because it effectively allows any origin, even while appearing to restrict access:
# DANGEROUS — do not do this:
# Gateway reflects any Origin it receives, no validation
Access-Control-Allow-Origin: https://evil.com ← reflected without checking
Access-Control-Allow-Credentials: true
A malicious site can exploit this to make credentialed cross-origin requests to your API. Always validate the Origin against a whitelist before reflecting.
Regex Matching for Subdomains
Allow all subdomains of a trusted domain:
# Envoy CORS filter with regex
http_filters:
- name: envoy.filters.http.cors
typed_config:
allow_origin_string_match:
- safe_regex:
regex: "https://[a-z0-9-]+\.example\.com"
Credential Handling
Credentials (cookies, authorization headers, TLS client certificates) are not sent in cross-origin requests by default. To include them, the client must opt in:
// Browser: opt in to sending credentials
fetch('https://api.example.com/users', {
credentials: 'include', // send cookies and Authorization header
headers: { Authorization: 'Bearer ...' }
})
And the server must explicitly allow it:
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: https://app.example.com ← must be specific, not *
SameSite Cookie Implications
Cookies with SameSite=Strict or SameSite=Lax will not be sent on cross-origin requests even if CORS allows them. Cookies that must travel cross-origin require SameSite=None; Secure. Ensure gateway CORS configuration aligns with cookie SameSite settings, otherwise credentialed requests will fail silently.
Gateway Configuration Reference
# AWS API Gateway CORS (OpenAPI 3 extension)
x-amazon-apigateway-cors:
allowOrigins:
- "https://app.example.com"
- "https://admin.example.com"
allowMethods:
- GET
- POST
- PUT
- DELETE
- OPTIONS
allowHeaders:
- Content-Type
- Authorization
- X-Amz-Date
exposeHeaders:
- X-Request-ID
- RateLimit-Remaining
maxAge: 86400
allowCredentials: true
Summary
Handle CORS at the gateway to ensure consistent policy across all services and to answer preflight requests without involving backends. Use a specific origin whitelist rather than wildcard for any API that uses credentials. Always include Vary: Origin to prevent cache poisoning. Never reflect Origin without whitelist validation — unconditional reflection is equivalent to Allow-Origin: * with the appearance of restriction. Set Access-Control-Max-Age to 86400 to minimize preflight overhead.