Cully
Cully

Reputation: 6965

Nginx redirect all traffic to index.php, but don't allow arbitrary file access

I have a legacy PHP app (Zend Framework) that I'm moving from Apache to Nginx. Except for things like web files (JS, CSS, images, fonts, etc.), all traffic needs to go through the index.php file in the root of the project folder. So I need to redirect all traffic to index.php, except for web files.

There are a lot of solutions to this that use try_files as a catch-all. Basically if a file is not found on the filesystem, nginx will send the request to index.php.

The problem with this is that the app stores its configuration file in a web-accessible path (i.e. in a subdirectory of the folder that holds index.php). So you could point your browser to the configuration file path and read it (e.g. https://example.com/config/app.xml). Since the file exists on the filesystem, nginx will serve it. There are actually a few more project config files that can be accessed in this way too.

So, how do I send all requests to index.php, except for web files, and also not allow arbitrary files to be read from the web?

Yes, I know I could update the app to access the config files in another location, but it's legacy and not super crucial. I don't want to spend more time on it than I have to and I don't want to break it.

I could also forbid access to any extensions that happen to be config files (e.g. xml, yml, etc.), but it's a big project and I don't want to risk missing something.

I figured out a solution. I didn't find anything that worked exactly the way I wanted online, so I'm asking this question in order to answer it.

Upvotes: 0

Views: 5707

Answers (2)

Ivan Shatsky
Ivan Shatsky

Reputation: 15527

Looking at your configuration I have a strong desire to comment it, however this would be much beyond the single comment format so writing this as an answer.

location ~* \.(js|ico|gif|jpg|png|svg|css|jpeg|wav|mp3|eot|woff|ttf)$ {}

Of course, this would work. But in a real life this would effectively blocking the files such as robots.txt, sitemap.xml, preventing SSL certificate validation via the /.well-known/pki-validation/<signature>.txt etc.

location ~* ^/(?!(index\.php)) {
  rewrite ^ /index.php$is_args$args;
}

Both rewrite and location directives works with the normalized URI (which doesn't include the query part of the request). While the try_files or return directives usually require this $is_args$args suffix to be used, with the rewrite directive all the query arguments are preserved and follows the rewritten URI. This means the /users?blah=1 request is being rewrited to - surprise, surprise - /index.php?blah=1?blah=1. This makes nginx consider the last ?blah=1 to be a query string and the preceding /index.php?blah=1 to be the filename giving you "HTTP 404 Not found" error. The right way to write this location would be the

location ~* ^/(?!index\.php) {
  rewrite ^ /index.php last;
}

and it can be even more simple:

location / {
  rewrite ^ /index.php last;
}

location = /index.php { # instead of ~ \.php$
  ...
}

However if you want to pass all the other requests to your index.php controller it could be done in a much more effective way. I assume the snippets/fastcgi-php.conf file you are using is the default one from some Debian-based Linux distro:

# regex to split $uri to $fastcgi_script_name and $fastcgi_path
fastcgi_split_path_info ^(.+\.php)(/.+)$;

# Check that the PHP script exists before passing it
try_files $fastcgi_script_name =404;

# Bypass the fact that try_files resets $fastcgi_path_info
# see: http://trac.nginx.org/nginx/ticket/321
set $path_info $fastcgi_path_info;
fastcgi_param PATH_INFO $path_info;

fastcgi_index index.php;
include fastcgi.conf;

You don't need it at all, as you don't need the location ~ \.php$ { ... } since the only PHP file you want to allow access to is the index.php. Use the following:

location / {
  # define the default FastCGI parameters
  include fastcgi.conf;
  # always use the 'index.php' as the FastCGI script
  fastcgi_param SCRIPT_FILENAME $document_root/index.php;
  # pass the request to the FastCGI backend
  fastcgi_pass unix:/run/php/php7.4-fpm.sock;
}

The whole configuration will be the

server {
  root /path/to/www;

  location ~* \.(js|ico|gif|jpg|png|svg|css|jpeg|wav|mp3|eot|woff|ttf)$ {}

  location / {
    include fastcgi.conf;
    fastcgi_param SCRIPT_FILENAME $document_root/index.php;
    fastcgi_pass unix:/run/php/php7.4-fpm.sock;
  }
}

Update @ 2022.05.17

Since the new media file types and formats comes to the scene time to time, requiring you to add those types and change the nginx configuration every time it happens, if one day you'd decide to switch from the list all the allowed to list all the denied approach, the most optimal way to do it can be the following:

# deny hidden files and files with the extensions listed below
location ~ /\.|\.(?:xml|yml|php|phar|inc)$ {
    deny all;
}

location / {
    try_files $uri /index.php$is_args$args;
    # cache policy for the static files can be added here
}

location = /index.php {
    include fastcgi.conf;
    fastcgi_param SCRIPT_FILENAME $document_root/index.php;
    fastcgi_pass unix:/run/php/php7.4-fpm.sock;
}

Since the exact match locations takes precedence over the regex match ones, this will effectively block any PHP (or other listed types) file access while the index.php one will remain allowed.

Upvotes: 3

Cully
Cully

Reputation: 6965

Here's the configuration I came up with (leaving out listen, server_name, etc. since they aren't important):

server {
  root /path/to/www;

  # this just catches all web files and allows them through
  location ~* \.(js|ico|gif|jpg|png|svg|css|jpeg|wav|mp3|eot|woff|ttf)$ {}

  # send all other traffic to index.php. this is important because it
  # blocks access to things like configuration.xml
  location / {
    rewrite ^ /index.php last;
  }

  location = /index.php {
    include snippets/fastcgi-php.conf;
    fastcgi_pass unix:/run/php/php7.4-fpm.sock;
  }
}

I'll go through each block and explain it's purpose.

location ~* \.(js|ico|gif|jpg|png|svg|css|jpeg|wav|mp3|eot|woff|ttf)$ {}

The first block catches all web files. The body of the block is empty, which just lets nginx send the matched files directly. You could add caching directives in here if you want.

location / {
  rewrite ^ /index.php last;
}

The second block sends all other traffic to index.php. Since the first block matches all the web files, this block won't apply to them. This is the block that prevents arbitrary files from being accessed from the web, since all of those file requests will be sent to index.php which will respond with a 404.

location = /index.php {
  include snippets/fastcgi-php.conf;
  fastcgi_pass unix:/run/php/php7.4-fpm.sock;
}

The last block is just to process PHP requests through FPM. Though, as Ivan helpfully pointed out, it's important to only target index.php otherwise any PHP file could be directly executed if someone knew the URL for it.

With all of this, I get:

  1. If I access a JS, CSS, images, etc. they are served directly.
  2. If I try to access a configuration file, I get a 404.
  3. If I try to access one of the actions that the app knows about, it works.
  4. Query parameters work as expected.

EDIT: After seeing Ivan's answer I was able to eliminate one block and simplify another. I updated the config to reflect the change.

Upvotes: 2

Related Questions