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_redirectis matching and replacing the upstreamLocationheader 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_redirectwith the exact upstream URL as the first argument and your public-facing URL as the second, or disable it entirely and control redirects viaproxy_set_header Host. - Use our Client-Side Sandbox above to paste your failing nginx
location {}block and auto-refactor theproxy_redirectdirective.
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:
- OAuth provider POSTs to
/callback→ upstream issues302 /dashboard→ nginx manglesLocation→ token exchange fails → users locked out. - If
proxy_redirect defaultis active, nginx usesproxy_passURL as the match pattern. Any upstream that returns aLocationnot matching that exact base URL gets silently passed through unrewritten — internal hostnames leak to clients. - 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.