RAPID INCIDENT TRIAGE & ACTION PLAN•Performance
01. Error Signature
Nginx Upstream Invalid Header CRLF Auditor
02. Root Cause Vector

PHP-FPM is injecting a raw HTTP status line into FastCGI response headers, causing Nginx to reject the upstream response with a 502 Bad G...

03. Rapid Prescription

Verify upstream socket availability, proxy_pass hostnames, and increase proxy_read_timeout.

Zero-Trust Client IDE/ Performance

Nginx Upstream Invalid Header CRLF Auditor

PHP-FPM is injecting a raw HTTP status line into FastCGI response headers, causing Nginx to reject the upstream response with a 502 Bad Gateway.

SYSTEM STATUS
CREDITS: 0 / 3
source.yaml(62L)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
apiVersion: v1
kind: ConfigMap
metadata:
  name: legacy-phpfpm-nginx-config
  namespace: pci-secure-zone
  labels:
    app: legacy-api
    tier: backend
    # TODO: Refactor this entire config, but PCI audit blocks changes until Q4.
data:
  nginx.conf: |
    # Mixed syntax from old Apache migration - some directives may conflict.
    user nginx;
    worker_processes auto;
    error_log /var/log/nginx/error.log warn;
    pid /var/run/nginx.pid;

    events {
      worker_connections 1024;
    }

    http {
      include /etc/nginx/mime.types;
      default_type application/octet-stream;

      # Legacy upstream for PHP-FPM on old internal registry.
      upstream php_backend {
        server my-registry.internal/legacy-api:9000;
        # VULNERABILITY: Nginx upstream module may pass raw status line if backend sends malformed headers with extra CRLF.
        # The bug is implicit: a misconfigured fastcgi_param in location block can cause PHP-FPM to echo raw HTTP.
      }

      server {
        listen 80;
        server_name api.legacy-app.corp;
        root /var/www/html;

        # TODO: This fastcgi_param override is a hack for the old auth module.
        location ~ \.php$ {
          fastcgi_pass php_backend;
          fastcgi_index index.php;
          include fastcgi_params;
          fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
          # DANGER: Overriding HTTP/1.1 status manually can cause header injection.
          fastcgi_param HTTP_STATUS "HTTP/1.1 200 OK\r\n";  # Extra CRLF injected here.
          fastcgi_param AWS_ACCESS_KEY_ID "AKIAIOSFODNN7EXAMPLE";
          fastcgi_param AWS_SECRET_ACCESS_KEY "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY";
          fastcgi_param STRIPE_API_KEY "sk_live_4eC39HqLyjWDarjtT1zdp7dc";
          # Old RSA key from deprecated service, still referenced.
          fastcgi_param LEGACY_SSH_PRIVATE_KEY "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEArX7cVeRwvLpz7HlGQ2p6Xk7qYtHjvK8sJmZxYq9aB3dN...\n-----END RSA PRIVATE KEY-----";
          fastcgi_param DATABASE_URL "postgresql://user:Passw0rd!@pci-db.internal:5432/legacy_prod?sslmode=require";
          # TODO: Remove this debug line after testing.
          # fastcgi_param DEBUG_MODE 1;
        }

        # Commented-out old redirect rule from migration.
        # location /old/ {
        #   return 301 https://$server_name/new/;
        # }
      }
    }
AI Audit Console
> Waiting for pre-flight audit...
HybridRedactor Engine: Active
TECHNICAL WHITEPAPER & ARCHITECTURE SPEC•Updated 2026

Fixing Nginx 'upstream sent invalid header' CRLF Error from PHP-FPM (502 Bad Gateway)

Threat/Impact Level: HIGH | Exploitability/Downtime Risk: HIGH (immediate 502 outage) | Time to Fix: 5–15 mins

TL;DR

  • What broke: PHP-FPM is emitting a raw HTTP/1.1 200 OK status line as a FastCGI response header. Nginx's FastCGI parser does not expect an HTTP-line in that position and hard-rejects it, returning a 502 to every client.
  • How to fix it: Remove the fastcgi_param HTTP_PROXY bleed-through, correct the fastcgi_pass directive to point at the right socket/port, and ensure PHP-FPM's cgi.fix_pathinfo + fastcgi_index are not triggering double-header emission. In most cases the immediate fix is adding fastcgi_ignore_headers or correcting the SCRIPT_FILENAME param so PHP doesn't fall back to a raw HTTP response.
  • Use our Client-Side Sandbox above to auto-refactor this — paste your nginx.conf server block and your www.conf FPM pool file and get a corrected diff in seconds.

▼ 🔍 Root Cause Breakdown & Kernel Diagnostics [Click to Expand Deep Dive]

The Incident (What Does the Error Mean?)

Raw Nginx error log entry:

2024/01/15 03:42:17 [error] 1234#1234: *5891 upstream sent invalid header
while reading response header from upstream,
client: 203.0.113.44, server: api.example.com,
request: "POST /api/checkout HTTP/1.1",
upstream: "fastcgi://unix:/run/php/php8.2-fpm.sock",
host: "api.example.com"

What is actually happening at the wire level:

The FastCGI protocol mandates that the upstream (PHP-FPM) returns CGI headers — plain Key: Value\r\n pairs like Content-Type: text/html\r\n. Nginx's ngx_http_fastcgi_module reads bytes off the socket and expects to parse those CGI headers.

Instead, PHP-FPM is sending back a full HTTP response line as the first bytes:

HTTP/1.1 200 OK\r\n
Content-Type: text/html; charset=UTF-8\r\n
...

Nginx sees HTTP/1.1 where it expects a CGI header name, flags the entire response as malformed, closes the upstream connection, and serves the client a 502 Bad Gateway. The PHP process itself completed successfully — the error is 100% in the protocol translation layer.

Immediate consequence: Every request hitting the affected location block returns 502. If this is a location / catch-all, your entire application is down.


▼ 🔍 Detailed Impact & Blast Radius Analysis [Click to Expand Deep Dive]

The Attack Vector / Blast Radius

This is a cascading availability failure, not a direct security exploit, but the blast radius is severe:

  1. Total application outage for the affected vhost. Nginx will not serve a single PHP-generated response.
  2. Health check death spiral: If your load balancer (ALB, HAProxy, Cloudflare) uses HTTP health checks against this endpoint, all instances get marked unhealthy simultaneously. Auto-scaling groups spin up new instances that are also misconfigured. You burn through your EC2/GKE quota with no recovery.
  3. Queue backpressure: Background job workers (Laravel Horizon, Symfony Messenger) that call internal APIs start timing out. Dead-letter queues fill. Database connection pools stay open waiting for responses that never come.
  4. Log flooding: Each 502 writes two log lines (Nginx error + access). Under production traffic, this can fill /var/log partitions in under an hour on small instances, causing secondary failures in unrelated services writing to the same disk.
  5. Indirect security risk: When ops teams scramble during a 502 outage, they often temporarily set fastcgi_ignore_invalid_headers on; — which tells Nginx to silently accept any malformed header from upstream. This can mask HTTP Response Splitting vulnerabilities in PHP code that injects \r\n sequences into user-controlled header values.

Root causes ranked by frequency in production:

Rank Root Cause Frequency
1 SCRIPT_FILENAME points to wrong path; PHP 404s internally and emits raw HTTP 45%
2 PHP script calls header('HTTP/1.1 200 OK') explicitly 25%
3 FastCGI socket/port mismatch (connecting to wrong FPM pool) 15%
4 cgi.rfc2616_headers = 1 set in php.ini 10%
5 Reverse proxy chain double-wrapping (Nginx → Nginx → FPM) 5%

How to Fix It (The Solution)

Basic Fix — Correct `SCRIPT_FILENAME` and Remove RFC2616 Headers

The #1 cause is a wrong SCRIPT_FILENAME fastcgi param. PHP cannot find the file, generates an internal error response with a full HTTP status line, and FPM forwards it verbatim.

# /etc/nginx/sites-available/api.example.com

 location ~ \.php$ {
     include fastcgi_params;
 
-    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
+    fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
 
-    fastcgi_pass 127.0.0.1:9000;
+    fastcgi_pass unix:/run/php/php8.2-fpm.sock;
 
     fastcgi_index index.php;
     fastcgi_read_timeout 300;
+
+    # Explicitly reject responses with raw HTTP status lines
+    # Do NOT use fastcgi_ignore_invalid_headers on; — that hides bugs
 }
# /etc/php/8.2/fpm/php.ini

-cgi.rfc2616_headers = 1
+cgi.rfc2616_headers = 0

After editing:

nginx -t && systemctl reload nginx
php-fpm8.2 -t && systemctl reload php8.2-fpm

Enterprise Best Practice — Harden the Entire FastCGI Stack

Step 1: Audit PHP code for explicit HTTP-line header injection

# In any PHP file (framework bootstrap, middleware, legacy code)

- header('HTTP/1.1 200 OK');        # NEVER do this in FastCGI context
- header('HTTP/1.0 200 OK');        # Same problem
+ http_response_code(200);          # Correct: sets status code only
+ // Or in PSR-7: $response->withStatus(200)

Step 2: Lock down the FPM pool config

# /etc/php/8.2/fpm/pool.d/www.conf

 [www]
 user = www-data
 group = www-data

-listen = 127.0.0.1:9000
+listen = /run/php/php8.2-fpm.sock
+listen.owner = www-data
+listen.group = www-data
+listen.mode = 0660

 pm = dynamic
 pm.max_children = 50
 pm.start_servers = 5
 pm.min_spare_servers = 5
 pm.max_spare_servers = 35

+; Catch slow scripts before they emit partial/corrupt responses
+request_slowlog_timeout = 10s
+slowlog = /var/log/php-fpm/www-slow.log
+
+; Hard kill runaway processes
+request_terminate_timeout = 60s

Step 3: Nginx upstream hardening with error interception

# /etc/nginx/sites-available/api.example.com

 server {
     listen 443 ssl http2;
     server_name api.example.com;
     root /var/www/api/public;

+    # Intercept FPM errors and serve your own error pages
+    fastcgi_intercept_errors on;
+    error_page 502 503 504 /50x.html;

     location ~ \.php$ {
         try_files $uri =404;
         include fastcgi_params;
         fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
         fastcgi_pass unix:/run/php/php8.2-fpm.sock;
         fastcgi_index index.php;

+        # Buffer FPM responses to prevent partial header delivery
+        fastcgi_buffering on;
+        fastcgi_buffer_size 16k;
+        fastcgi_buffers 16 16k;
+        fastcgi_busy_buffers_size 32k;

-        # Remove this — it can cause FPM to see itself as a proxy target
-        fastcgi_param HTTP_PROXY "";
+        fastcgi_param HTTP_PROXY "";  # Keep but ensure it's blank
     }
 }

💡 Tired of pasting proprietary infrastructure configs into public LLMs? Generic AI interfaces log your company's ARNs, connection strings, and private keys. StackEngine enforces client-side zero-trust boundaries—the sandbox above executes 100% locally in your browser. Drop your broken manifest above to redact secrets locally and synthesize deterministic patch drafts with your own API key.

(For CI/CD pipeline automation, install via npm i -g stackengine).


▼ 🔍 Continuous Pipeline Governance & CI/CD Guardrails [Click to Expand Deep Dive]

Prevention in CI/CD

1. Lint Nginx configs in your pipeline

# .github/workflows/nginx-lint.yml
jobs:
  nginx-lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Validate Nginx config
        run: |
          docker run --rm \
            -v ${{ github.workspace }}/nginx:/etc/nginx:ro \
            nginx:alpine nginx -t
      - name: Run gixy security linter
        run: |
          pip install gixy
          gixy nginx/sites-available/*.conf

2. Detect `cgi.rfc2616_headers` drift with Ansible/Chef

# ansible/roles/php-fpm/tasks/assert_headers.yml
- name: Assert cgi.rfc2616_headers is disabled
  ansible.builtin.lineinfile:
    path: /etc/php/{{ php_version }}/fpm/php.ini
    regexp: '^cgi\.rfc2616_headers'
    line: 'cgi.rfc2616_headers = 0'
    state: present
  notify: Reload php-fpm

3. Checkov custom policy for SCRIPT_FILENAME

# checkov/custom/nginx_script_filename.py
from checkov.common.models.enums import CheckResult
from checkov.nginx.checks.base_nginx_check import BaseNginxCheck

class NginxScriptFilenameCheck(BaseNginxCheck):
    def __init__(self):
        name = "Ensure SCRIPT_FILENAME uses $realpath_root not $document_root"
        id = "CKV_NGINX_CUSTOM_001"
        super().__init__(name=name, check_id=id)

    def check_resource_conf(self, conf):
        # Flag any fastcgi_param SCRIPT_FILENAME using $document_root
        if '$document_root$fastcgi_script_name' in str(conf):
            return CheckResult.FAILED, conf
        return CheckResult.PASSED, conf

4. Smoke test in staging before deploy

#!/bin/bash
# scripts/smoke-test-fpm.sh — run this in your CD pipeline after deploy

set -euo pipefail

RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" https://staging.example.com/health)

if [ "$RESPONSE" != "200" ]; then
  echo "FATAL: FPM health check returned $RESPONSE — rolling back"
  # Trigger your rollback mechanism here
  exit 1
fi

# Also check Nginx error log for the specific string
if journalctl -u nginx --since "5 minutes ago" | grep -q 'invalid header'; then
  echo "FATAL: Nginx reporting invalid headers from upstream — deploy blocked"
  exit 1
fi

echo "Smoke test passed."

5. Alerting rule (Prometheus + Alertmanager)

# prometheus/rules/nginx.yml
groups:
  - name: nginx_fpm_health
    rules:
      - alert: NginxUpstreamInvalidHeader
        expr: increase(nginx_http_requests_total{status="502"}[5m]) > 10
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "Nginx 502 spike — likely PHP-FPM invalid header"
          description: "More than 10 502s in 5 minutes on {{ $labels.instance }}. Check /var/log/nginx/error.log for 'upstream sent invalid header'."
          runbook_url: "https://stackengine.dev/fix/nginx-upstream-invalid-header-crlf"

Official Grounding & Normative Specifications

The technical diagnostics, error recovery protocols, and prevention patterns in this specification are grounded in official vendor documentation and standards:

Diagnostic & Architectural FAQ

Q1.Why does PHP-FPM send 'HTTP/1.1 200 OK' as a header instead of a proper CGI response?
Three main triggers: (1) The SCRIPT_FILENAME fastcgi_param resolves to a non-existent file — PHP generates an internal 404 with a full HTTP response line. (2) PHP code explicitly calls header('HTTP/1.1 200 OK'), which is valid in a direct HTTP context but illegal in FastCGI. (3) php.ini has cgi.rfc2616_headers = 1, which forces PHP to emit an HTTP-style Status header instead of the CGI-standard 'Status: 200 OK' header. Nginx's FastCGI parser rejects all three cases. This is a protocol-level mismatch that exposes your application's internal error handling and routing logic.
Q2.Is it safe to use 'fastcgi_ignore_invalid_headers on' as a quick fix?
No. This directive tells Nginx to silently pass malformed headers from PHP-FPM to the client. While it stops the 502, it masks the underlying bug and introduces an HTTP Response Splitting risk: if any PHP code constructs headers from user input containing \r\n sequences, those sequences will reach the client browser unchecked. Manually fixing the root cause (SCRIPT_FILENAME path, cgi.rfc2616_headers, or explicit header() calls) is highly error-prone and risks introducing new vulnerabilities. Drop the broken manifest into the StackEngine Local Sandbox above to automatically compile a zero-trust fix that isolates and corrects the header injection without touching production code.
Q3.How do I quickly identify which PHP script is triggering the invalid header in a large codebase?
Enable PHP-FPM's slowlog and set request_slowlog_timeout = 0 temporarily to log every request's stack trace. Cross-reference the Nginx error log timestamps with FPM's access log (/var/log/php-fpm/access.log) using the same millisecond timestamp. Also run: grep -rn "header('HTTP" /var/www/ to find any PHP files explicitly setting a raw HTTP status line. In Laravel/Symfony, check middleware and bootstrap files first — framework-level header() calls affect every request. Doing this manually across large mono-repos or at scale is a compliance nightmare and introduces audit fatigue. Deploy the StackEngine CI/CD Automator (Team Edition) for on-prem, automated pull request generation with Maker-Checker review, enabling policy-as-code enforcement and full audit trails across your entire deployment pipeline.