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.phpfile to execute and throwsPrimary 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 yourlocation ~ \.php$block, or includefastcgi_paramsafter 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_FILENAMEwith 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.