Current situation: PHP2Shell. Two critical PHP vulnerabilities (CVSS 9.2) allow malicious code without login. Patches since 21 October 2026.What to do now →

Configuring Nginx for PHP Securely

Most weaknesses in Nginx setups for PHP are not in Nginx but in a few lines of the PHP location. Here is how to configure the handover to PHP-FPM so that only intended scripts are executed.

Last updated 6 min readHardening and configuration

In short: To configure Nginx for PHP securely, pass only scripts that really exist to PHP-FPM, and for front-controller applications only index.php. In addition come blocked dotfiles such as .git and .env, a default server for unknown host names, suitable size limits and security.limit_extensions = .php in the pool. This does not protect against vulnerabilities in PHP itself, but it limits what can be executed after a mistake.

The most dangerous lines are in the PHP location

Since PHP2Shell, many teams have been checking their PHP-FPM versions and pools, as described on the page Securing Nginx with PHP-FPM, and after the update restart all workers. A second look goes to the Nginx configuration itself. That is where it is decided which files are executed as PHP at all.

In many configurations there is a block that hands every request for .php to PHP-FPM, no matter where the file is located. If an injected PHP file ends up in an upload folder, the server executes it. A file error thus becomes a backdoor.

This article concentrates on the Nginx side: which directives matter, how the order of location blocks works and how to test the result.

How Nginx decides which block applies

Anyone who wants to configure Nginx for PHP securely must know how location blocks are selected. According to the Nginx documentation, it works like this:

  1. An exact match with location = ends the search immediately.
  2. Among the prefix locations, the longest matching one is remembered. If it carries ^~, no regular expressions are checked any more.
  3. Otherwise regular expressions (~, ~*) are checked in the order of the configuration file; the first match wins.
  4. If no regular expression matches, the remembered prefix location applies.

Two practical rules follow. A block for PHP in upload folders must either be created as a ^~ prefix or be placed as a regular expression before the general PHP block. And an ordinary prefix location such as location /uploads/ does not protect, because the regular expression for .php overrides it.

A secure basic configuration

The following example shows a server block for an application with a front controller, as is usual with Laravel, Symfony, Shopware 6 or many custom developments. Adapt paths, socket and certificates.

server {
    listen 443 ssl;
    server_name www.example.de;
    root /var/www/seite1/public;
    index index.php;

    ssl_certificate     /etc/ssl/seite1/fullchain.pem;
    ssl_certificate_key /etc/ssl/seite1/privkey.pem;

    server_tokens off;
    client_max_body_size 32m;

    # Allow Let's Encrypt and other well-known paths
    location ^~ /.well-known/ {
        try_files $uri =404;
    }

    # Block hidden files and folders (.git, .env, .htaccess)
    location ~ /\. {
        deny all;
    }

    location / {
        try_files $uri $uri/ /index.php$is_args$args;
    }

    # Only the front controller is passed to PHP-FPM
    location = /index.php {
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        fastcgi_param DOCUMENT_ROOT $realpath_root;
        fastcgi_hide_header X-Powered-By;
        fastcgi_pass unix:/run/php/seite1.sock;
    }

    # Any other PHP file is not executed
    location ~ \.php$ {
        return 404;
    }
}

$realpath_root resolves symbolic links. This matters for deployments with a changing symlink to the current release, so that PHP-FPM and OPcache see the right path. Also note: according to the Nginx documentation, fastcgi_param directives are inherited from higher levels only if the current level has none of its own. That is why include fastcgi_params; belongs in the same block.

Classic CMS such as WordPress, TYPO3 or Contao have several entry points. There, replace the front-controller block with a general PHP location that passes only existing files (try_files $uri =404;), and block execution in all writable folders. How to do that is covered under Forbidding PHP execution in upload folders.

The second safeguard in PHP-FPM

Nginx decides what is passed on, PHP-FPM decides what it executes. According to the PHP manual, PHP-FPM executes files with .php and .phar by default and itself recommends limiting this to .php. This catches configuration errors in the web server.

; in the pool, e.g. /etc/php/8.3/fpm/pool.d/seite1.conf
security.limit_extensions = .php
listen = /run/php/seite1.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660

The socket should be reachable only by Nginx. FPM ports on the network, such as 9000, should not be opened to the outside. How the server as a whole exposes only the necessary ports is described in Firewall for web servers: opening only necessary ports.

Unknown host names and further adjustments

Without a default server of its own, Nginx answers requests for unknown host names with the first matching server block. Automated scans that address only the IP address then land directly in your application. A dedicated default server ends such requests without a response.

server {
    listen 80 default_server;
    listen 443 ssl default_server;
    server_name _;
    ssl_reject_handshake on;
    return 444;
}

Further points that have proven worthwhile:

Adjustment Recommendation Purpose
server_tokens off no version number in headers and error pages
client_max_body_size matching post_max_size reject large requests early
autoindex off (default) no directory listings
fastcgi_hide_header X-Powered-By no PHP version to the outside
Status pages pm.status_path internal only no operating data in public
Backup files block or remove .sql, .zip, .bak in the web root no download of databases

More on version information is in the article expose_php and server headers.

Another adjustment concerns the permitted HTTP methods. Most websites need only GET, HEAD and POST. With limit_except, other methods can be rejected in a location; according to the Nginx documentation, allowing GET automatically also allows HEAD. Check beforehand whether your application's interfaces use PUT or DELETE, for example, as REST APIs often do.

Testing before you reload

Check every change first with nginx -t and only then reload Nginx. Then test the behavior from outside with normal requests to your own domain:

sudo nginx -t && sudo systemctl reload nginx
# Expected: 403 or 404
curl -s -o /dev/null -w "%{http_code}\n" https://www.example.de/.git/HEAD
curl -s -o /dev/null -w "%{http_code}\n" https://www.example.de/.env
# Expected: 404, because only the front controller is executed
curl -s -o /dev/null -w "%{http_code}\n" https://www.example.de/info.php
# Which blocks hand over to PHP-FPM?
sudo nginx -T 2>/dev/null | grep -nE "location|fastcgi_pass"

Before every larger change, save the full output of nginx -T with a date. It contains the entire effective configuration including all included files. If something goes wrong after a change, the comparison immediately shows what differs. At the next critical vulnerability you also have a reliable basis for the inventory without having to gather files first.

Tip

Change the configuration first in a test environment or on a copy of the server block. A forgotten entry point, such as a payment callback, otherwise only becomes noticeable when orders stop coming in.

How does PHP SOS help?

  • Read the configuration: We evaluate the entire Nginx configuration with nginx -T and assign every PHP handover to its website and pool.
  • Define entry points: Together with your developers, we determine which scripts must be executable and block the rest.
  • Implement and test: We adapt server blocks and pools, check with nginx -t and test forms, payment paths and uploads.
  • Secure permanently: The configuration becomes part of our server hardening and of your server's documentation.

Frequently asked questions

Does Nginx have to be restarted after a PHP update?

Not for the PHP patch, because Nginx contains no PHP. All PHP-FPM services must be restarted. If you also change the Nginx configuration, check it beforehand with nginx -t and then reload Nginx.

Why pass only index.php to PHP?

Many modern applications have a single entry point, the front controller. If only this file is handed to PHP-FPM, an injected PHP file elsewhere cannot be executed through the browser. For systems with many entry points this pattern is not possible.

What does security.limit_extensions do in PHP-FPM?

The setting defines which file extensions PHP-FPM executes as a script. According to the PHP manual the default is .php .phar; limiting to .php is recommended. This catches mistakes in the web server configuration but does not replace it.

Are Nginx rules protection against PHP2Shell?

No. PHP2Shell is in the PHP interpreter and is closed only by the update to a fixed version and the restart of all workers. A secure Nginx configuration reduces the attack surface and limits the damage after a break-in.

Sources

Immediate contact

Every hour counts. Write to us now.

Briefly describe your system, PHP version and observation. We will get back to you, clarify the situation and work remotely until the incident is closed and documented.

Immediate help