When implementing a CORS-enabled proxy with Nginx, you might encounter situations where the backend service sends overly permissive headers like Access-Control-Allow-Origin: *
. This becomes problematic when you need to implement cookie-based authentication, as browsers will reject credentials with wildcard origins.
The default behavior in Nginx is to pass through all headers from the proxied server while adding your own headers. This results in duplicate CORS headers, which can confuse browsers:
Access-Control-Allow-Origin: *
Access-Control-Allow-Origin: https://api.yourdomain.com
Nginx provides the proxy_hide_header
directive to remove unwanted headers from upstream responses before adding your custom ones:
server {
location / {
proxy_pass https://myrestserver.com/api;
# Remove the upstream CORS header first
proxy_hide_header Access-Control-Allow-Origin;
# Add our properly configured CORS headers
add_header Access-Control-Allow-Origin $cors_header;
add_header Access-Control-Allow-Credentials true;
}
}
For more sophisticated origin validation, you can extend the map block to handle multiple domains or subdomains:
map $http_origin $cors_header {
default "";
"~^https?://([a-z0-9-]+\\.)?(mydomain\\.com|myotherdomain\\.net)(:[0-9]+)?$" $http_origin;
"https://trusted-thirdparty.com" $http_origin;
}
For complete CORS support, you should also configure OPTIONS requests:
location / {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' $cors_header;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}
proxy_pass https://myrestserver.com/api;
proxy_hide_header Access-Control-Allow-Origin;
add_header Access-Control-Allow-Origin $cors_header;
add_header Access-Control-Allow-Credentials true;
}
When implementing CORS with credentials:
- Never use wildcard origins with credentials
- Validate the Origin header against a whitelist
- Consider adding Vary: Origin header
- Implement proper CSRF protection
# Add this to your location block
add_header 'Vary' 'Origin';
When implementing cookie-based authentication through an Nginx proxy, the default wildcard Access-Control-Allow-Origin: *
header from upstream becomes problematic. The security implications are serious:
- Wildcard origins don't work with credentialed requests (cookies, auth headers)
- Multiple ACAO headers may cause browser rejection
- Origin validation is bypassed entirely
The solution requires two key operations:
- Removing the upstream ACAO header
- Injecting our validated ACAO header
Here's the complete configuration:
map $http_origin $cors_header {
default "";
"~^https?://[^/]+\\.mydomain\\.com(:[0-9]+)?$" $http_origin;
}
server {
location / {
proxy_pass https://myrestserver.com/api;
# Remove upstream CORS headers
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Allow-Credentials;
# Add our validated headers
add_header Access-Control-Allow-Origin $cors_header;
add_header Access-Control-Allow-Credentials true;
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
add_header Access-Control-Allow-Headers 'DNT,User-Agent,X-Requested-With,Content-Type';
# Handle preflight requests
if ($request_method = 'OPTIONS') {
add_header Access-Control-Max-Age 1728000;
add_header Content-Type 'text/plain; charset=utf-8';
add_header Content-Length 0;
return 204;
}
}
}
proxy_hide_header: This directive prevents the upstream headers from reaching the client. It's crucial for removing the wildcard ACAO header.
add_header: We construct our own CORS headers with proper validation. The $cors_header
variable comes from our origin validation map.
For more complex validation patterns, consider this enhanced map block:
map $http_origin $cors_header {
default "";
# Exact domain matches
"https://app.mydomain.com" $http_origin;
"https://api.mydomain.com" $http_origin;
# Subdomain patterns
"~^https?://([a-z0-9-]+\\.)?mydomain\\.com$" $http_origin;
# Local development
"http://localhost:3000" $http_origin;
"http://127.0.0.1:[0-9]+" $http_origin;
}
Verify your setup with curl:
curl -I -H "Origin: https://app.mydomain.com" https://yourproxy.com/api/resource
You should see exactly one properly formatted ACAO header reflecting your origin.
For high-traffic APIs:
- Move origin validation to a separate file with
include
- Consider caching valid origins
- Benchmark with and without complex regex patterns
This configuration provides proper security controls:
- Origin validation prevents unauthorized cross-origin access
- Credential support is explicitly enabled
- No wildcard headers remain to weaken security