It is common to use Nginx coupled with PHP-FPM to run PHP websites: WordPress, Magento, etc.
Nginx is a great web server, but what makes it so tricky to configure is lack of information how a particular directive is applied in each location.
Let’s say you want to increase the limit for max size of uploaded files in a PHP website. One would expect that placing client_max_body_size
into the ~ \.php$
location block will suffice.
Suppose that we want to allow uploads up to 64MB. And someone is uploading a file that is 32MB.
This is our configuration:
server {
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
location ~ \.php$ {
client_max_body_size 64M;
fastcgi_pass ...
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
Uploading to /upload.php
will succeed. But uploading to /user/upload
will result in the file being rejected with 413 Request Entity Too Large
message.
Why wouldn’t client_max_body_size
work? It appears that client_max_body_size
is not exactly inherited. There are 2 cases which I have identified.
Case 1. If your PHP file is accessed directly
If your request URL matches to the PHP regex location directly, e.g. /test.php
, then the client_max_body_size
directive that is defined there applies cleanly and as stated. It disregards whichever limit was set in outer scopes.
Case 2. If your location is the result of internal rewrites
Things get very interesting when your location match happened as the result of internal rewrites (try_files
). That is, you are uploading to a URL that requires rewriting of request to match with the PHP file to process the upload.
The client_max_body_size
directive will not apply unless it’s more restricting than the default 1MB or the one that is set in outer scopes. In other words, client_max_body_size
cannot be increased in a rewritten location. You have to define it at server context if you want to increase the limit for rewritten URLs.
So the following will work:
server {
client_max_body_size 64M;
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
location ~ \.php$ {
fastcgi_pass ...
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
Why do we have to care?
One would be just fine putting the large client_max_body_size
in server {}
context and be happy that things work. However, this exposes some risk to the DDoS. Many large file uploads will consume disk space on your server. They will consume network bandwidth as well as TCP sockets.
So what is the rule of thumb here? It depends on your use case.
First, if you are certain that your website will not accept large file uploads exceeding the default 1MB, do not specify any extra configuration. Let the default value apply.
If you only accept large uploads directly at PHP URLs which don’t involve any internal rewriting by nginx, you can put the client_max_body_size
in the ~ \.php$
location only and this would be most safe.
Finally, if you accept uploads exceeding 1MB at SEO friendly URLs (e.g. /user/upload
), then you can put client_max_body_size 64M;
in server {}
context. But to address security concerns you might want to take a different approach, which we are going to detail next.
Increase file upload limit in nginx for SEO-friendly upload endpoints.
So your upload must be taken at a SEO friendly location, e.g. /user/upload
and you want to increase the upload limit just there. This would be quite better in terms of security, since other locations will be less prone to large uploads DDoS.
You can achieve this by specifying a separate location for your “pretty upload handler”, like the following:
server {
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
location = /user/upload {
client_max_body_size 64M;
fastcgi_pass ...
fastcgi_param SCRIPT_FILENAME $document_root/index.php;
include fastcgi_params;
fastcgi_param SCRIPT_NAME /index.php;
}
location ~ \.php$ {
fastcgi_pass ...
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
What we did there is relaxed the upload limit just for our upload handler. Note that we have to adjust fastcgi_param SCRIPT_FILENAME
and point it directly to /index.php
script as there is no longer any rewrite to it. We have essentially converted an indirect match (our initial configuration) into direct match to our upload handler with the pretty URL.
Note how we put fastcgi_param SCRIPT_NAME /index.php
at the end of /user/upload
location. This is so it overrides the one which is set in fastcgi_params
file.