How to Fix Nginx Infinite Rewrite Cycle: Resolving 'rewrite or internal redirection cycle while processing URI'
Threat/Impact Level: HIGH | Exploitability/Downtime Risk: HIGH | Time to Fix: 10 mins
TL;DR
- What broke: An Nginx
rewriterule ortry_filesdirective is redirecting a URI back into the same location block, creating an infinite internal loop. Nginx hits its internal redirect limit (default: 10) and returns a500 Internal Server Error. - How to fix it: Add a terminal flag (
last,break, or=404) to your rewrite rule, or restructure conflicting location blocks so the rewritten URI cannot re-match the originating block. - Sandbox: Use our Client-Side Sandbox below to auto-refactor this — paste your
server {}block and get corrected directives without sending your config to a third-party server.
The Incident (What Does the Error Mean?)
Your Nginx error log is emitting:
2024/01/15 03:42:17 [error] 1234#1234: *89 rewrite or internal redirection cycle
while processing "/index.php", client: 10.0.0.5, server: example.com,
request: "GET /index.php HTTP/1.1", host: "example.com"
Nginx enforces a hard ceiling of 10 internal redirects per request. Once that ceiling is hit, the worker process aborts the request and returns 500. The client sees a failed request. Under load, this saturates worker connections because each looping request holds a worker slot until the cycle limit is reached — it doesn't fail fast.
The Attack Vector / Blast Radius
This is not a security vulnerability in the traditional sense — but the blast radius in production is severe:
- Worker exhaustion: Each looping request occupies a worker connection for the full 10-redirect cycle before dying. Under moderate traffic (500 RPS), you can exhaust
worker_connectionsentirely, causing legitimate requests to queue or be dropped with502/504upstream. - Log flooding: Every cycle emits an error log line. On high-traffic nodes this fills disk partitions hosting
/var/log/nginx/, which can crash the entire Nginx process if the partition hits 100%. - Cascading upstream failures: If Nginx is a reverse proxy, the upstream (PHP-FPM, Node, Gunicorn) never receives the request, but your load balancer health checks may still pass — masking the outage from your alerting stack.
- The most common triggers:
- A
rewrite ^/(.*)$ /index.php?q=$1inside alocation /block withoutlastorbreak, causing/index.phpto re-matchlocation /and rewrite again. try_files $uri $uri/ /index.phpcombined with a separatelocation ~ \.php$that internally rewrites back to/index.php.- Nested
locationblocks where an inner block rewrites to a URI matched by the outer block.
- A
How to Fix It
Basic Fix: Add the Correct Rewrite Flag
The last flag stops processing current rewrite directives and re-searches location blocks — which is exactly what causes the loop when the rewritten URI re-matches the same block. Use break to stop rewriting entirely and serve from the current block.
server {
listen 80;
server_name example.com;
location / {
- rewrite ^/(.*)$ /index.php?q=$1;
+ rewrite ^/(.*)$ /index.php?q=$1 break;
}
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
Why break here: The rewritten URI /index.php would re-enter location / with last. With break, Nginx stops rewriting and passes /index.php directly to the location ~ \.php$ block via normal location matching after the current block finishes.
Enterprise Best Practice: Use try_files + Named Locations
Avoid rewrite for routing entirely in modern Nginx configs. Use try_files with a named @fallback location. Named locations (@name) are never re-evaluated for rewrite cycles.
server {
listen 80;
server_name example.com;
root /var/www/html;
index index.php;
location / {
- rewrite ^/(.*)$ /index.php?q=$1 last;
+ try_files $uri $uri/ @php_fallback;
}
+ location @php_fallback {
+ fastcgi_pass unix:/run/php/php8.2-fpm.sock;
+ fastcgi_index index.php;
+ fastcgi_param SCRIPT_FILENAME $document_root/index.php;
+ fastcgi_param QUERY_STRING q=$uri&$args;
+ include fastcgi_params;
+ }
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
Why this is production-grade: Named locations are terminal. try_files first attempts to serve a real file or directory, only falling through to @php_fallback when nothing matches on disk. This eliminates the rewrite cycle structurally, not just symptomatically.
💡 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
Don't wait for this to surface in production error logs at 3 AM.
1. nginx -t in your deploy pipeline (non-negotiable baseline)
This catches syntax errors but not rewrite logic cycles. You need additional tooling.
2. gixy — Static Nginx Config Analyzer
gixy is Yandex's open-source Nginx security and logic linter. It has a dedicated http_splitting and rewrite cycle detection rule.
pip install gixy
gixy /etc/nginx/nginx.conf
# [ERROR] rewrite cycle detected in /etc/nginx/sites-enabled/example.com
Add to your GitHub Actions workflow:
- name: Lint Nginx Config
run: |
pip install gixy
gixy /etc/nginx/nginx.conf --format json | tee gixy-report.json
# Fail pipeline if any issues found
python -c "import json,sys; r=json.load(open('gixy-report.json')); sys.exit(1 if r['issues'] else 0)"
3. Integration test with curl + redirect-following disabled
In your staging smoke tests, explicitly test that key URIs return 200, not 500:
# --max-redirs 0 forces curl to fail on any redirect, exposing loops
curl --max-redirs 0 -o /dev/null -w "%{http_code}" https://staging.example.com/
# Expected: 200. If 500: rewrite loop present.
4. OPA/Conftest policy for Nginx configs (advanced)
If you manage Nginx configs as code in a GitOps repo, enforce no bare rewrite ... last inside location / blocks via a Rego policy evaluated at PR time with conftest.
package nginx
deny[msg] {
input.location[_].path == "/"
rewrite := input.location[_].rewrite[_]
contains(rewrite, " last")
msg := "Rewrite with 'last' flag inside location / risks infinite cycle. Use 'break' or named locations."
}