Initializing Enclave...

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 rewrite rule or try_files directive 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 a 500 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_connections entirely, causing legitimate requests to queue or be dropped with 502/504 upstream.
  • 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=$1 inside a location / block without last or break, causing /index.php to re-match location / and rewrite again.
    • try_files $uri $uri/ /index.php combined with a separate location ~ \.php$ that internally rewrites back to /index.php.
    • Nested location blocks where an inner block rewrites to a URI matched by the outer block.

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."
}

Related Diagnostics

"Part of the Performance Utility Matrix."

View all 219 Performance Tools →