Reputation: 6965
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
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
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:
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