Initializing Enclave...

How to Fix nginx proxy_redirect Replacing the Wrong Domain in Location Headers

Threat/Impact Level: HIGH | Exploitability/Downtime Risk: HIGH | Time to Fix: 10 mins

TL;DR

  • What broke: proxy_redirect is matching and replacing the upstream Location header with the wrong domain — either clobbering your public hostname with an internal upstream address, or vice versa, depending on directive order.
  • How to fix it: Explicitly scope proxy_redirect with the exact upstream URL as the first argument and your public-facing URL as the second, or disable it entirely and control redirects via proxy_set_header Host.
  • Use our Client-Side Sandbox above to paste your failing nginx location {} block and auto-refactor the proxy_redirect directive.

The Incident (What does the error mean?)

Your upstream app returns:

HTTP/1.1 302 Found
Location: http://internal-app:8080/dashboard

nginx is supposed to rewrite that to https://app.example.com/dashboard. Instead, clients receive a Location pointing to a mangled URL — wrong scheme, wrong host, or a partially-substituted path. The browser follows it and hits a dead endpoint.

Common raw symptom in browser devtools:

Location: http://app.example.cominternal-app:8080/dashboard
# or
Location: http://internal-app:8080/dashboard   ← rewrite never fired
# or
Location: https://wrong-vhost.internal/dashboard  ← wrong server_name matched

This silently breaks OAuth2 redirect_uri callbacks, POST-redirect-GET flows, and SAML ACS responses — all without a 5xx. Logs look clean. Users just can't log in.


The Attack Vector / Blast Radius

proxy_redirect applies globally across all location blocks if placed in the server {} context. A single misconfigured directive bleeds into every proxied endpoint on that vhost.

Cascading failure chain:

  1. OAuth provider POSTs to /callback → upstream issues 302 /dashboard → nginx mangles Location → token exchange fails → users locked out.
  2. If proxy_redirect default is active, nginx uses proxy_pass URL as the match pattern. Any upstream that returns a Location not matching that exact base URL gets silently passed through unrewritten — internal hostnames leak to clients.
  3. In multi-upstream configs (upstream {} blocks with multiple servers), the mangled hostname can expose your internal service mesh topology to end users via response headers — an unintentional information disclosure.

How to Fix It (The Solution)

Basic Fix — Explicit proxy_redirect mapping

 location /app/ {
     proxy_pass http://internal-app:8080/;
 
-    proxy_redirect default;
+    proxy_redirect http://internal-app:8080/ https://app.example.com/app/;
 }

default expands to proxy_redirect <proxy_pass_url> <scheme>://<host>/ using the current request's Host header — which is unreliable behind CDNs or when proxy_set_header Host is overridden downstream.

Enterprise Best Practice — Disable proxy_redirect, control via headers

For most production setups, the upstream app should never be issuing redirects to its own internal address. Fix the root cause: make the upstream redirect-aware via X-Forwarded-* headers, then disable nginx's rewriting entirely.

 server {
     server_name app.example.com;
 
     location /app/ {
         proxy_pass         http://internal-app:8080/;
 
+        proxy_set_header   Host              $host;
+        proxy_set_header   X-Forwarded-Proto $scheme;
+        proxy_set_header   X-Forwarded-Host  $host;
+        proxy_set_header   X-Forwarded-Port  $server_port;
 
-        proxy_redirect     default;
+        proxy_redirect     off;
     }
 }

With proxy_redirect off, nginx touches nothing. The upstream app reads X-Forwarded-Host and X-Forwarded-Proto to construct correct absolute redirect URLs itself. This is the only approach that survives blue/green deploys, canary routing, and CDN layer changes without nginx config edits.

If you cannot modify the upstream app, use an explicit map:

 location /app/ {
     proxy_pass       http://internal-app:8080/;
-    proxy_redirect   http://internal-app:8080/ /;
+    proxy_redirect   ~^http://internal-app:8080(/.*)$  https://app.example.com$1;
 }

The regex form (~ prefix) handles path variations that the literal form misses when upstream returns sub-paths not anchored to /.


💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.


Prevention in CI/CD

1. nginx config linting in CI — nginx -t is not enough.

Use gixy, Yandex's static analyzer for nginx:

# In your pipeline
pip install gixy
gixy /etc/nginx/nginx.conf
# Catches: proxy_redirect misuse, SSRF vectors, header injection risks

2. Integration test for Location headers — catch this before deploy:

# Assert the Location header on a known redirect endpoint
RESPONSE=$(curl -sI -o /dev/null -w "%{redirect_url}" http://staging.app.example.com/app/login)
if [[ "$RESPONSE" != https://app.example.com/* ]]; then
  echo "FAIL: Location header domain mismatch: $RESPONSE"
  exit 1
fi

Plug this into your GitHub Actions or GitLab CI test stage. It runs in under 2 seconds and blocks any merge that breaks redirect behavior.

3. OPA policy for nginx ConfigMaps (Kubernetes):

If you're managing nginx config via ConfigMaps in-cluster, enforce via OPA Gatekeeper:

# Deny any nginx ConfigMap containing 'proxy_redirect default'
violation[{"msg": msg}] {
  input.request.object.data[_] == v
  contains(v, "proxy_redirect default")
  msg := "proxy_redirect default is banned. Use explicit URL mapping or proxy_redirect off."
}

4. Renovate/Dependabot for nginx base image pinning — upstream behavior of proxy_redirect default has shifted across nginx minor versions. Pin your Docker base image digest, not just the tag.

Related Diagnostics

"Part of the Performance Utility Matrix."

View all 219 Performance Tools →