NginX+PHP-FPM setup: Why is this configuration workaround needed to properly set PATH_INFO? [Debian7.4]

4

3

It took several hours to find out how to properly configure NginX, but I still think I might have missed something. Mainly because this perfectly working setup raises more questions than it answers. Perhaps this even is an NginX-bug or something introduced by Debian in Version: 1.2.1-2.2+wheezy2 of Package: nginx-extras, I simply do not know.

Please note that my example already is stripped down to the bare minimum (so take this as a starting point) but it still shall contain all necessary current secure fixes.

Please note that I dislike error prone heuristics. Therefor this runs with /etc/php/fpm/pool.d/www.conf set such, that php-fpm takes the script directly from PATH_TRANSLATED instead of guessing it:

php_admin_value[cgi.fix_pathinfo]=0
security.limit_extensions = .php
php_admin_flag[expose_php] = off

Here is the php-file /home/tino/www/index.php which dumps the variables:

<?
header("content-type: text/plain");
$a = array("PATH_TRANSLATED", "SCRIPT_FILENAME", "DOCUMENT_URI", "PATH_INFO", "QUERY_STRING" );
foreach ($a as &$v)
  printf("%-15s %s\n", $v, $_SERVER[$v]);
?>

Working NginX configuration

Note that /home/tino/www is where my test script lives. And you possibly want to populate all the other params from /etc/nginx/fastcgi_params. For example you need

fastcgi_param SCRIPT_NAME $fastcgi_script_name;

to populate PHP_SELF.

server_tokens off;

server {
        listen          80;
        root            /home/tino/www;

        if ($request_uri ~ " ") { return 444; }

        location ~ [^/]\.php(/|$) {
                limit_except GET HEAD POST { deny all; }

                fastcgi_split_path_info ^((?U).+\.php)(.*)$;

                try_files $fastcgi_script_name =404;

                set             $wtf              $fastcgi_path_info;
                fastcgi_param   PATH_INFO         $wtf;
                fastcgi_param   REQUEST_METHOD    $request_method;
                fastcgi_param   PATH_TRANSLATED   $document_root$fastcgi_script_name;
                fastcgi_param   SCRIPT_FILENAME   $request_filename;
                fastcgi_param   DOCUMENT_URI      $document_uri;

                fastcgi_pass unix:/var/run/php5-fpm.sock;
                fastcgi_index index.php;
        }
}

This gives me following output for http://example.com/index.php/a.php?b=b.php

PATH_TRANSLATED /home/tino/www/index.php
SCRIPT_FILENAME /home/tino/www/index.php
DOCUMENT_URI    /index.php
PATH_INFO       /a.php
QUERY_STRING    b=b.php

This is exactly what I want. Note that this works for less problematic URIs too, of course.

The not working config

However, the straight forward configuration does not work, and I am really puzzled, why:

server_tokens off;

server {
        listen          80;
        root            /home/tino/www;

        if ($request_uri ~ " ") { return 444; }

        location ~ [^/]\.php(/|$) {
                limit_except GET HEAD POST { deny all; }

                fastcgi_split_path_info ^((?U).+\.php)(.*)$;

                try_files $fastcgi_script_name =404;

                fastcgi_param   PATH_INFO         $fastcgi_path_info;
                fastcgi_param   REQUEST_METHOD    $request_method;
                fastcgi_param   PATH_TRANSLATED   $document_root$fastcgi_script_name;
                fastcgi_param   SCRIPT_FILENAME   $request_filename;
                fastcgi_param   DOCUMENT_URI      $document_uri;

                fastcgi_pass unix:/var/run/php5-fpm.sock;
                fastcgi_index index.php;
        }
}

This is the same as above, just the $wtf workaround is missing:

                set             $wtf              $fastcgi_path_info;
                fastcgi_param   PATH_INFO         $wtf;

became

                fastcgi_param   PATH_INFO         $fastcgi_path_info;

But this does not work, output for http://example.com/index.php/a.php?b=b.php now is:

PATH_TRANSLATED /home/tino/www/index.php
SCRIPT_FILENAME /home/tino/www/index.php
DOCUMENT_URI    /index.php
PATH_INFO       
QUERY_STRING    b=b.php

As you can see: PATH_INFO vanished. This has nothing to do with php-fpm, it's purely an NginX thing!

Insecure config works again

Following configuration, which opens a major security hole due to the missing file check, works again:

server_tokens off;

server {
        listen          80;
        root            /home/tino/www;

        if ($request_uri ~ " ") { return 444; }

        location ~ [^/]\.php(/|$) {
                limit_except GET HEAD POST { deny all; }

                fastcgi_split_path_info ^((?U).+\.php)(.*)$;

                fastcgi_param   PATH_INFO         $fastcgi_path_info;
                fastcgi_param   REQUEST_METHOD    $request_method;
                fastcgi_param   PATH_TRANSLATED   $document_root$fastcgi_script_name;
                fastcgi_param   SCRIPT_FILENAME   $request_filename;
                fastcgi_param   DOCUMENT_URI      $document_uri;

                fastcgi_pass unix:/var/run/php5-fpm.sock;
                fastcgi_index index.php;
        }
}

It has a little bit different outcome on http://example.com/index.php/a.php?b=b.php though (SCRIPT_FILENAME looks wrong, DOCUMENT_URI maybe correct from a different point of view).

PATH_TRANSLATED /home/tino/www/index.php
SCRIPT_FILENAME /home/tino/www/index.php/a.php
DOCUMENT_URI    /index.php/a.php
PATH_INFO       /a.php
QUERY_STRING    b=b.php

So please

Apparently try_files breaks how $fastcgi_path_info is parsed after setting the regexp with fastcgi_split_path_info.

So, please, can anybody tell me, if this $wtf workaround is indeed the correct solution or do I still have to worry about additional bad sideffects of try_files?

Thanks. (If I missed some important security bit, please comment.)

Tino

Posted 2014-02-17T08:20:16.383

Reputation: 906

Note that the tags are perhaps a bit misleading. This question is "how to correctly (and securely) calculate $fastcgi_path_info in NginX" to stuff it into PATH_INFO and not "how to put this correctly into params such that fpm sees it in PATH_INFO correctly". Sorry for that disturbance. – Tino – 2014-02-28T19:01:28.337

Answers

4

I was working yesterday on setting up my first Nginx server with PHP-FPM and I can have ran into the same issue as yours. Long story short, I wasn't able to properly and securely setup Nginx with all PHP's server variables being properly set. I've finally found a proper setup that I will share with you. However, it would be very nice to hear from a Nginx expert on that subject. Seriously, we ran into really odd issues that should never happen.

Your setup issues

So, basically, you want a working, secure Nginx+PHP-FPM installation. I mean with "working" that PHP's server variables are properly set and with "secure" that Nginx is protected against known vulnerabilities.

First of, I was able to reproduce your configuration and your issues. For some reason, the try_files is breaking something. I cannot say why, but you are right when you say that. Replacing the try_files directive with the following code seems to fix that issue:

if (!-f $document_root$fastcgi_script_name) {
    return 404;
}

EDIT: As discuss below in the comment, the code above should be avoided. Moreover, the original try_files directive is working as expected.

Second, I've notice that you didn't include the fastcgi_params file, which is responsible to set all the needed (and required) FastCGI's parameters that will be used to set the PHP's server variables. I don't know why you didn't include this file, since every bit of documentation I've read on the web tell us to use that file. Now that the file is included, we need to modify some parameters that are not properly set by default. At the end, we need to add the following directives:

include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_param PATH_TRANSLATED $document_root$fastcgi_path_info;

Notice how SCRIPT_FILENAME is different as yours. Again, I've read lots of documentation that tell us to use this value (I'll link to those below). You can put the fastcgi_param directive either directly in the fastcgi_params file or in your location section.

Then, nothing is working. If I use the same link as yours, I get No input file specified. Looking into the log files, I get Unable to open primary script: /var/www/a.php (No such file or directory). I don't know why it's doing that, but all the fastcgi parameters seems to be mix-up.

Now, the weirdest part, if I remove this line:

fastcgi_param PATH_TRANSLATED $document_root$fastcgi_path_info;

it's working again, but with the PHP's server variables mix-up. Not only PATH_TRANSLATED is not set, PHP_SELF and others are not properly set. This is where I have struggle a long time. Fortunately, I've found a solution that seems to fit what we are aiming for.

Working setup

If we re-enable cgi.fix_pathinfo in the PHP configuration, everything works. At first, I though that it was a bad idea to do that. However, I read about the security risks with cgi.fix_pathinfo enabled, only to find out that we are already protected against them. With security.limit_extensions set to .php by default, we are protected against cgi.fix_pathinfo issues. Furthermore, the file existance check (the if condition we've added previously) protect us even more, by preventing the case where FastCGI tries to interpret other files as PHP.

The final location section should look-like this:

location ~ [^/]\.php(/|$) {
    fastcgi_split_path_info ^((?U).+\.php)(.*)$;

    try_files $fastcgi_script_name =404;

    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_param PATH_INFO $fastcgi_path_info;
    fastcgi_param PATH_TRANSLATED $document_root$fastcgi_path_info;

    fastcgi_pass unix:/var/run/php5-fpm.sock;
    fastcgi_index index.php;
}

Other toughts

There is one thing nobody talk about, even if they are all doing it differently. I'm talking about the regexes used in the location and fastcgi_split_path_info directive. Personally, I've choose these one since they are making more sense to me:

location ~ ^.+\.php(/|$) { ... }
fastcgi_split_path_info ^((?U).+\.php)(/?.+)$;

I would really like to know the advantages and disadvantages of the other one. Again, it would be very nice to hear from a Nginx expert on that subject.

Sources

DjDCH

Posted 2014-02-17T08:20:16.383

Reputation: 41

1

Please also note that cgi.fix_pathinfo=1 is a security risk even with security.limit_extensions=.php: http://stackoverflow.com/questions/15493207/php-what-are-the-effects-of-cgi-fix-pathinfo-1-in-php-ini-on-a-webserver#comment35880085_16177823

– icyrock.com – 2014-12-19T22:49:29.017

1

Re1: I try to abstain from if in location if all possible. So instead if .. return 404 the recommended way is to use try_files .. =404. Sigh. See http://wiki.nginx.org/IfIsEvil

– Tino – 2014-02-28T18:04:19.390

Re2: I did not use include for transparency reasons. The file provided by NginX cannot be included because it sticks to the CGI spec, which, however, is not followed by php-fpm (before you answer, first look into the fpm source, try it and you will notice I am right). – Tino – 2014-02-28T18:10:16.743

Re3: Perhaps you have php_admin_value[cgi.fix_pathinfo]=1, in that case it appears that fpm follows CGI spec. However it ignores most variables and calculates them, in a lengthy and costly heuristics. Obscure heuristics are, by definition, insecure. So I will never even consider using such. With php_admin_value[cgi.fix_pathinfo]=0 you have to change the CGI variables, as then fpm does plain crap. Again: Looking into the source of fpm cured me from thinking about something else than cgi.fix_pathinfo=0. – Tino – 2014-02-28T18:24:19.447

Re4: Also my example is minimal, in that, if you add any other variable, fpm completely changes how the variables are parsed. WTF? Just look into the fpm code and start to weep! Your answer is helpfup for others to set PATH_INFO correctly in fpm (Thank you!) but my question is purely on the NginX side, because such a bug in NginX makes me worry there might be some additional unknown hidden pitfalls in NginX. – Tino – 2014-02-28T18:34:06.320

1Re5: RegEx: /?.+ and .+ match exactly the same strings. And ^.+\.php(/|$) matches something like /a/.php like from http://example.com/a/.php. Usually the rule to prohibit "dotfiles" is at the end (it is not shown in my example), so I do not want to have other location rules to match "dotfiles" before. – Tino – 2014-02-28T18:45:25.137

@Tino: Thanks a lot for your comments. I've updated my post regarding the try_files directive. Also, thanks for your regexes explanation. It's sad to see that PHP-FPM is not CGI compliant (or that we have to fix things ourself to make it compliant). Now, about the issues, I think we should try to get in touch with guys from Nginx and PHP-FPM project. I think we got enough evidence here to say that there is a problem. Do you know how we can get them to read this? Or, what would be the best way to contact them? – DjDCH – 2014-02-28T23:11:55.950

Sorry, I have no idea for NginX, and no time to find out, too. Eventually they find out. About php: I don't think we need to contact them, as you can live with cgi.fix_pathinfo=0. It's non-compliant, but for compatibility reasons we should not change that behavior. Note that with cgi.fix_pathinfo=1 it fixes some issues from apache (hence the name). No way to repair that, instead use an NginX setup which violates the CGI spec like apache does, that should do the trick (never tested it as I do not want to). – Tino – 2014-03-02T05:44:59.923

1

Not sure why the following link didn't pop up in this conversation: try_files & $fastcgi_path_info

The ​try_files directive changes URI of a request to the one matched on the file system, and subsequent attempt to split the URI into $fastcgi_script_name and $fastcgi_path_info results in empty path info - as there is no path info in the URI after try_files.

I don't think this should be considered as a bug, rather than a feature of how try_files work. It makes try_files not very convenient for use with fastcgi_split_path_info, but there are more than one way to workaround it, including the one provided above.

Mentioned above workaround is the one you mentioned in your question.

So, what you have found out is one of the pitfalls of nginx and it is not(as of 2016) going to be fixed. Your workaround is perfectly valid and, actually, quoted in the distributed with Debian nginx code snippets.

Timur Bakeyev

Posted 2014-02-17T08:20:16.383

Reputation: 36