How to Override Access-Control-Allow-Origin Header in Nginx Proxy Responses


2 views

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:

  1. Removing the upstream ACAO header
  2. 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