Nginx / Server Setup

nginx: client_max_body_size inheritance

by , , revisited on


We have by far the largest RPM repository with dynamic stable NGINX modules and VMODs for Varnish 4.1 and 6.0 LTS. If you want to install nginx, Varnish and lots of useful modules for them, this is your one stop repository to get all performance related software.
You have to maintain an active subscription in order to be able to use the repository!

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.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.