skip to content

Every PHP request through Nginx to PHP-FPM returns 'File not found.'; what does SCRIPT_FILENAME do, and what usually went wrong?

level: juniorimportance: should knowfreq 45%

answer

  1. the web server names the file to run
  2. path as FPM sees it
  3. Primary script unknown means 404
  4. unreadable file means 403 Access denied
  5. security.limit_extensions defaults to .php

basics

~20 s

SCRIPT_FILENAME is the FastCGI parameter telling PHP-FPM which file to execute. 'File not found.' means FPM could not resolve or open that path — usually the parameter is missing or points to a path that does not exist where FPM runs.

solid answer

~40 s

PHP-FPM does not map URLs to files; the web server does, and passes the result in the FastCGI parameter `SCRIPT_FILENAME`. For Nginx that is usually `fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;`. If the parameter is missing, or (with the default `cgi.fix_pathinfo=1`) names a path that does not exist **as FPM sees it** — the common case when Nginx and FPM run in different containers with different mount paths, or FPM is chrooted — FPM logs 'Primary script unknown' and answers 404 'File not found.'. A file the pool user cannot read gives 403 'Access denied.', and a file whose extension is not in `security.limit_extensions` (default `.php`) is refused with 403 too, which stops an uploaded image being run as PHP. With Apache's `mod_proxy_fcgi`, FPM strips the `proxy:fcgi://` prefix Apache puts in this parameter.

go deeper

for a junior

Recall that the web server tells PHP-FPM which file to run through SCRIPT_FILENAME, and that 'File not found.' means that path was missing or wrong.

for a middle

Map FPM's responses to causes — Primary script unknown, No input file specified, Access denied — and explain container path mismatches.

for a senior

Explain security.limit_extensions and the uploaded-file attack it blocks, and keep SCRIPT_FILENAME built from a fixed document root.

for a principal

Standardise the web-server-to-FPM wiring across services so paths, mounts and allowed extensions are defined once and tested.

## Who decides which file runs In a web-server-plus-PHP-FPM setup, the **web server** receives the HTTP request and decides it is a PHP request; **PHP-FPM** only runs what it is told. The FastCGI protocol carries the request as a set of parameters (like CGI environment variables), and the one that names the script is **`SCRIPT_FILENAME`**: an absolute path to the PHP file to execute. With Nginx, the PHP manual's example location is: ```nginx location ~* \.php$ { fastcgi_index index.php; fastcgi_pass 127.0.0.1:9000; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param SCRIPT_NAME $fastcgi_script_name; } ``` `$document_root` is the server's `root` and `$fastcgi_script_name` the URI's script part, so `/index.php` under `root /srv/app/public` becomes `/srv/app/public/index.php`. With Apache httpd 2.4, `mod_proxy_fcgi` builds the parameter itself, prefixed with `proxy:fcgi://host:port`; PHP-FPM recognises that prefix (and `proxy:balancer://`) and strips it to recover the file path. ## Reading FPM's answers PHP-FPM checks the script before running it, and each failure has its own response body and status: | Response body | Status | Cause | |---|---|---| | `File not found.` | 404 | No usable script path: the parameter is missing, or (with the default `cgi.fix_pathinfo=1`) neither the path nor any parent of it is an existing file where FPM runs; FPM logs `Primary script unknown`, which the web server shows in its error log | | `No input file specified.` | 404 | A path was accepted but opening it failed for a reason other than permissions — for example a missing file with `cgi.fix_pathinfo=0`; FPM logs `Unable to open primary script` | | `Access denied.` | 403 | The file exists but the pool user cannot read it | | `Access denied.` | 403 | The extension is not allowed by `security.limit_extensions`; FPM logs `Access to the script '…' has been denied (see security.limit_extensions)` | ## The usual culprits 1. **Parameter missing.** The PHP location includes a params file that does not set `SCRIPT_FILENAME`, or the line was lost in a refactor. Result: `Primary script unknown` and `File not found.` for every request — the same symptom as a path that does not exist. 2. **Wrong root.** `$document_root` is inherited from a different `root` than expected, so the path points to a non-existent directory. 3. **Different file systems.** Nginx and FPM in separate containers: Nginx has the code at `/usr/share/nginx/html`, FPM at `/var/www/html`. The path must be the one **FPM** sees — often it is hard-coded in the parameter, or both containers mount the code at the same path. 4. **chroot.** A pool with `chroot` sees paths relative to the jail, so a path valid on the host fails inside it. 5. **Permissions.** The pool's `user` cannot traverse a parent directory or read the file: `Access denied.` with 403. ## security.limit_extensions `security.limit_extensions` (default `.php`) restricts which file extensions FPM will execute as the main script, matched against the end of the path. Its purpose, per the sample configuration, is to prevent configuration mistakes on the web-server side from letting users run other files as PHP. The classic attack: an uploaded `avatar.jpg` containing PHP code, requested as `/uploads/avatar.jpg/x.php`; with a loose web-server rule and PHP's path-info fixing, the script path could resolve to the `.jpg` file. With the default setting FPM refuses it with 403. Keep it at `.php` unless an application genuinely needs another extension; an empty value allows everything. ## A checklist when PHP pages fail - Look at the **body** and **status**: they tell you which check failed. - Read the web server's error log for `Primary script unknown` and FPM's log for `Unable to open primary script` or the `limit_extensions` notice. - Log the parameter value temporarily (for example with a custom access-log format in the web server) and verify the file exists **inside FPM's environment** with `ls -l` as the pool user. - Check that non-PHP static files are served by the web server directly and never passed to FPM. ## Why the split matters Because the web server chooses the file, a mistake there can run the wrong code — or, if FPM is reachable directly, anyone can choose the file. FPM's checks (existence, readability, allowed extension) are the last line; a correct `SCRIPT_FILENAME` built from a fixed document root is the first.

  • What is the difference between 'File not found.' and 'No input file specified.' from PHP-FPM?
    'File not found.' means FPM ended up with no usable script path — `SCRIPT_FILENAME` was missing, or with the default `cgi.fix_pathinfo=1` it named a file that does not exist inside FPM's file system — and it logs 'Primary script unknown'. 'No input file specified.' means a path was accepted but opening it failed, for example a missing file when `cgi.fix_pathinfo=0`. Both are 404s.
  • A request for /uploads/cat.jpg/x.php returns 403 'Access denied.' from PHP-FPM. Is something broken?
    No, that is `security.limit_extensions` working. The resolved script is `cat.jpg`, whose extension is not in the allowed list (default `.php`), so FPM refuses to execute it and logs a notice. It blocks uploaded files being run as PHP.

saying these in an interview costs you the question

  • PHP-FPM maps the URL to a file using its own document root
  • SCRIPT_FILENAME must be the path as the web server sees it
  • security.limit_extensions is a web server setting
  • A 403 Access denied from FPM always means wrong socket permissions
  • Setting security.limit_extensions empty is the safe default