Initializing Enclave...

Fixing Nginx 'Primary script unknown' FastCGI Error: Missing fastcgi_param SCRIPT_FILENAME

Threat/Impact Level: HIGH | Exploitability/Downtime Risk: HIGH | Time to Fix: 5 mins


TL;DR

  • What broke: Nginx is proxying to PHP-FPM without passing SCRIPT_FILENAME, so PHP-FPM has no idea which .php file to execute and throws Primary script unknown — your site is returning 502s or blank pages.
  • How to fix it: Add fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; inside your location ~ \.php$ block, or include fastcgi_params after your custom param overrides.
  • Use our Client-Side Sandbox above to auto-refactor this — paste your broken server {} block and get a corrected diff instantly.

The Incident (What does the error mean?)

Raw error surfacing in /var/log/nginx/error.log or journalctl -u php-fpm:

2024/01/15 03:42:17 [error] 1234#1234: *56 FastCGI sent in stderr:
"Primary script unknown" while reading response header from upstream,
client: 10.0.0.1, server: example.com, request: "GET /index.php HTTP/1.1",
upstream: "fastcgi://unix:/run/php/php8.2-fpm.sock"

PHP-FPM received the FastCGI request but the SCRIPT_FILENAME parameter was either absent, empty, or pointed to a non-existent path. FPM cannot resolve which script to run, immediately aborts, and writes this to stderr. Nginx surfaces it as a 502 Bad Gateway or a silent empty response depending on fastcgi_ignore_client_abort settings.


The Attack Vector / Blast Radius

This is a full application outage for every PHP request hitting that location block. No PHP executes. Every dynamic page — login, checkout, API endpoints — returns 502. Static assets served directly by Nginx are unaffected, which often misleads engineers into thinking the issue is intermittent.

Secondary blast radius:

  • Health checks fail → load balancer marks the instance unhealthy → autoscaling group spins replacement nodes that have the same broken config → cascading replacement loop.
  • Monitoring gaps: If your uptime check hits a static /health.html, it stays green while 100% of PHP traffic is down.
  • Misconfigured SCRIPT_FILENAME with a hardcoded path (e.g., fastcgi_param SCRIPT_FILENAME /var/www/html$fastcgi_script_name; missing trailing context) can also silently execute the wrong script — a less obvious but more dangerous failure mode.

How to Fix It (The Solution)

Basic Fix

The single most common cause: fastcgi_param SCRIPT_FILENAME is missing entirely, or fastcgi_params include file is included before a custom param that gets overridden to empty.

 location ~ \.php$ {
     include snippets/fastcgi-php.conf;
-    fastcgi_pass unix:/run/php/php8.1-fpm.sock;
+    fastcgi_pass unix:/run/php/php8.2-fpm.sock;
+    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
+    include fastcgi_params;
 }

Order matters. include fastcgi_params must come after your explicit fastcgi_param directives, otherwise the include file's default (often an empty or relative SCRIPT_FILENAME) silently overwrites yours.

Enterprise Best Practice

Stop relying on the distro-provided fastcgi_params file — it varies across Ubuntu, Alpine, and custom builds. Define a hardened, project-owned snippet:

- # /etc/nginx/fastcgi_params (distro default — do not trust blindly)
- fastcgi_param SCRIPT_FILENAME $request_filename;

+ # /etc/nginx/snippets/php-fpm-hardened.conf (version-controlled, explicit)
+ fastcgi_param SCRIPT_FILENAME    $realpath_root$fastcgi_script_name;
+ fastcgi_param DOCUMENT_ROOT      $realpath_root;
+ fastcgi_param QUERY_STRING       $query_string;
+ fastcgi_param REQUEST_METHOD     $request_method;
+ fastcgi_param CONTENT_TYPE       $content_type;
+ fastcgi_param CONTENT_LENGTH     $content_length;
+ fastcgi_param SCRIPT_NAME        $fastcgi_script_name;
+ fastcgi_param REQUEST_URI        $request_uri;
+ fastcgi_param SERVER_NAME        $server_name;
+ fastcgi_param SERVER_PORT        $server_port;
+ fastcgi_param SERVER_PROTOCOL    $server_protocol;
+ fastcgi_param GATEWAY_INTERFACE  CGI/1.1;
+ fastcgi_param SERVER_SOFTWARE    nginx/$nginx_version;
+ fastcgi_param REMOTE_ADDR        $remote_addr;
+ fastcgi_param HTTPS              $https if_not_empty;

Use $realpath_root instead of $document_root — it resolves symlinks, which is critical in containerized environments where /var/www/html is a symlink to a mounted volume.

 location ~ \.php$ {
-    include fastcgi_params;
-    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
-    fastcgi_pass unix:/run/php/php8.1-fpm.sock;
+    include snippets/php-fpm-hardened.conf;
+    fastcgi_pass unix:/run/php/php8.2-fpm.sock;
+    fastcgi_read_timeout 60;
+    fastcgi_buffer_size 128k;
+    fastcgi_buffers 4 256k;
 }

💡 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 -t is not enough. It validates syntax but will not catch a missing fastcgi_param because the directive is syntactically valid even when empty.

2. Lint with gixy (Nginx static analyzer):

pip install gixy
gixy /etc/nginx/nginx.conf
# Catches common misconfigs including SSRF-enabling fastcgi_pass setups

3. Integration test with a real PHP-FPM process in CI:

# .github/workflows/nginx-validate.yml
- name: Spin up Nginx + PHP-FPM and probe
  run: |
    docker compose -f docker-compose.test.yml up -d
    sleep 3
    curl -sf http://localhost/index.php | grep -q "OK" || (docker compose logs && exit 1)

4. Checkov for Dockerfile/Nginx configs:

checkov -d ./nginx --framework dockerfile

Write a custom Checkov check that asserts SCRIPT_FILENAME is present in any file containing fastcgi_pass.

5. Git pre-commit hook:

#!/bin/sh
# .git/hooks/pre-commit
if git diff --cached --name-only | grep -q 'nginx'; then
  grep -rL 'SCRIPT_FILENAME' nginx/conf.d/*.conf && echo "ERROR: fastcgi location block missing SCRIPT_FILENAME" && exit 1
fi

This catches the regression at commit time, before it reaches staging.

Related Diagnostics

"Part of the Syntax Utility Matrix."

View all 153 Syntax Tools →