How to Fix Nginx proxy_pass Trailing Slash Stripping the URI Incorrectly
Threat/Impact Level: HIGH | Exploitability/Downtime Risk: HIGH | Time to Fix: 5 mins
TL;DR
- What broke: A trailing slash on
proxy_passcauses nginx to strip the matchedlocationprefix before forwarding, so/api/usershits the upstream as/users— silently wrong. - How to fix it: Remove the trailing slash from
proxy_passif you want the full URI forwarded as-is, or explicitly rewrite if you intend to strip the prefix. - Shortcut: Use our Client-Side Sandbox below to auto-refactor this — paste your broken location block and get the corrected config instantly.
The Incident (What Does the Error Mean?)
There is no error log. That is the problem. Nginx silently mangles the upstream path.
Symptom in access logs:
# Incoming request
GET /api/users/42 HTTP/1.1
# What your upstream actually receives
GET /users/42 HTTP/1.1 ← prefix /api stripped
Your upstream returns 404. Your app returns 404. Your on-call rotation gets paged. Nothing in nginx error logs explains why.
The offending config:
location /api/ {
proxy_pass http://backend:8080/; # <-- trailing slash triggers URI rewrite
}
When proxy_pass contains a URI (anything after the host — even just /), nginx replaces the part of the request URI matched by location with that URI. So /api/users/42 becomes /users/42 at the upstream. If you omit the trailing slash entirely, nginx forwards the full original URI unchanged.
The Attack Vector / Blast Radius
This is a silent routing misconfiguration — no 5xx, no crash, just wrong-path 404s or, worse, accidental path collisions on the upstream.
Cascading failure scenarios:
- Auth bypass risk: If your upstream has an unprotected
/root or a different resource at the stripped path, requests may resolve to unintended handlers. A/api/admin/deletesilently becomes/admin/delete— which may have different middleware applied. - Full service degradation: Every proxied endpoint under the location block is affected simultaneously. Not one route — all of them.
- Debugging black hole: Because upstream logs show the stripped path as the actual request, backend engineers see clean 404s with no indication the path was mutated by the proxy layer. Median time-to-diagnosis in production: 45–90 minutes.
- Blue/green deployment risk: If this config ships in a rolling deploy, 100% of traffic to that location block is broken the moment the new config reloads.
How to Fix It (The Solution)
Basic Fix — Preserve Full URI (Most Common Intent)
Remove the trailing slash so nginx passes the complete original URI to the upstream.
location /api/ {
- proxy_pass http://backend:8080/;
+ proxy_pass http://backend:8080;
}
Result: GET /api/users/42 → upstream receives GET /api/users/42. No mutation.
Enterprise Best Practice — Explicit Prefix Strip with rewrite
If you intentionally want to strip /api before hitting the upstream (e.g., upstream has no /api prefix in its routing), do it explicitly and visibly with a rewrite directive. Never rely on the trailing-slash side effect — it is unreadable to the next engineer.
location /api/ {
- proxy_pass http://backend:8080/;
+ rewrite ^/api/(.*) /$1 break;
+ proxy_pass http://backend:8080;
}
Why this matters at scale:
- The
rewritedirective is self-documenting. Any engineer reading this config immediately understands the path transformation. - Avoids the ambiguity of the trailing-slash rule entirely.
- Works correctly with
proxy_passpointing to an upstream group name (e.g.,proxy_pass http://backend_pool;) where the trailing-slash trick breaks entirely anyway.
Upstream group pattern (load-balanced):
upstream backend_pool {
server backend1:8080;
server backend2:8080;
}
location /api/ {
- proxy_pass http://backend_pool/; # broken — nginx ignores URI rewrite with upstream blocks in some versions
+ rewrite ^/api/(.*) /$1 break;
+ proxy_pass http://backend_pool;
}
💡 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
This class of misconfiguration is entirely preventable before it reaches production.
1. nginx -t in your pipeline (minimum viable gate):
# GitHub Actions step
- name: Validate nginx config
run: docker run --rm -v $(pwd)/nginx.conf:/etc/nginx/nginx.conf nginx nginx -t
This catches syntax errors but not semantic path-stripping bugs. Necessary but not sufficient.
2. gixy — nginx static analyzer:
pip install gixy
gixy /etc/nginx/nginx.conf
gixy by Yandex flags proxy_pass trailing-slash issues and SSRF-prone configurations. Add it as a CI step.
- name: Run gixy nginx linter
run: gixy nginx/nginx.conf --severity medium
3. Integration test against a real upstream stub:
Spin up a mock upstream (e.g., mockoon, httpbin) in your CI environment and assert that the full path is preserved end-to-end:
# Assert /api/health is NOT stripped to /health
curl -sf http://localhost/api/health | grep '"path": "/api/health"'
4. OPA/Conftest policy for nginx configs:
# policy/nginx.rego
package nginx
deny[msg] {
input.proxy_pass
endswith(input.proxy_pass, "/")
not input.rewrite
msg := "proxy_pass trailing slash without explicit rewrite is forbidden. Use rewrite or remove trailing slash."
}
Enforce via conftest test in your PR pipeline to block this class of config from merging.