Remember register globals?
Remember how you had to code as if it was off, because it might be?
Remember how you had to consider the security implications of it being on, because it might be?
The might be and might not be is something which has plagued a lot of early PHP features.
Register globals is in no way alone in this, in the effort of making things versatile the PHP developers managed to introduce the worst of both worlds and the best of none.
Today I want to cover a very known feature, which many people often don’t think of as being in the same group as register globals.
Namely path info. The idea of path info is brilliant enough, it is most often used as a way to have SEO “friendly” URIs in cases where one might not be able to rewrite the URL. In short, you can have a URI like so:
/index.php/user/dashboard
In standard Unix this would read as “file dashboard in directory /index.php/user/”. Of course, /index.php/user/ is not a directory and there’s no file called dashboard. Instead, PHP sees this and translates it into /index.php with /user/dashboard as the path info. In case you’re shaking your head already, this is actually in the CGI spec so it’s not really a fault of PHP, there is literally an RFC specifying this behaviour.
And for the longest time this was perfectly fine. The web server model used with Apache made this a non-issue. PHP was embedded in Apache and as such would only be called for actual files configured as such. But these days people aren’t just using Apache any more, however, they still think most things function like they do in Apache. Since I’m an Nginx person and this is primarily an Nginx and PHP blog, lets look at how path info works with Nginx.
The issue is that where Apache sees files Nginx sees URIs. Nginx is at the heart of it a reverse proxy, it does not embed scripting languages and it does not execute code. Instead, it sees a URI and either try to serve a static file or pass it onto a backend. What this means is that when using PHP we see locations like the following:
location ~ \.php$ { fastcgi_pass upstream; }
This location actually does not allow for normal path info to work as the location defines the URI as having to end in .php. However, lets look at what happens when we reverse the path info request URI like so:
/uploads/avatar32.jpg/index.php
In this case PHP will see that there is no index.php file in /uploads/avatar32.jpg/ and as such will instead execute /uploads/avatar32.jpg with /index.php as the path info. We are essentially allowing PHP to execute any arbitrary file in our defined nginx root by just appending /index.php to the URI!
What makes this scary is that there’s a ton of ways to hide PHP code in file uploads. For instance if you run forum software like VB you can embed PHP code inside an EXIF tag and upload it as an avatar without VB ever batting a virtual eyelash. I trust I don’t need to tell you how bad it is to allow attackers to execute arbitrary PHP code on your server.
And the best thing is that this is not even a security vulnerability in either Nginx or PHP, Nginx is doing exactly what a reverse proxy should be doing and PHP is simply following the CGI specification. As such there won’t be a “fix” for this, it’s solely up to the developers and server admins to educate themselves and understand the tools they’re actually using.
With all that dire info out of the way, the good news is that you can secure yourself very easily. The simplest way is to tell PHP not to translate the path info by setting the php.ini variable cgi.fix_pathinfo to 0. This means that PHP will instead try to execute the /index.php file which doesn’t exist and thus return 404 and “no input file specified”
Changing the above setting means that path info will no longer work. If path info is still needed then nginx offers a way to have this done by using the fastcgi_split_path_info directive This will let legitimate requests through while having bad requests not re-evaluate into a PHP location block.
So why is this like register globals and short open tags? Because it’s a php.ini setting. You can turn the behaviour on and off. Your code has to consider the security implications in case it might be on, but it cannot take advantage of it in case it’s off, you’re getting the worst of both worlds. In practice this is a dangerous feature that should be deprecated and set to off in PHP by default.
14 Comments. Leave new
Great I fixed that issue now.
But what about “no input file specified “?
could this be translated into an error page like 404 or a Redirect for example.
Thank you for this great blog.
Hi! I have a post on that specific issue as well: http://blog.martinfjordvald.com/2011/01/no-input-file-specified-with-php-and-nginx/
Why not this:
if (!-f $request_filename) {
return 404;
}
This will produce a clear 404 instead of the weird No input file specified.
That will just produce a Nginx 404 instead of a PHP 404. It’s better to catch it with error page than to actively check up front. Also Nginx is a reverse proxy, sometimes the PHP files are on backend server so you cannot check if they exist.
can’t you tackle this with try_files directive ?
You can, but at the expense of an extra stat() call on each page load. I personally prefer this method as it avoids a 404 and instead serves up the proper page safely.
Another well explained article, Thank you !
Thank you for explaining this all. Lots of different guides out there but none of them explain what pathinfo is all about. It’s 2014 now and I don’t think wordpress uses pathinfo anymore which is good.
if nginx is responsible to decide what files should be executed as php, then nginx should also handle pathinfo properly, and check if a file exists and is php source code in an authorized location.
for one that means that the location that is passed to php should never match user uploaded files.
if nginx is not responsible for making those checks and decisions, then we have no business to even check for the extension. instead, everything in a certain path should be directed to php and then php should be responsible for verifying which files should be executed as php, and again, user uploaded files should never be allowed to run, no matter what filename they have.
if neither php nor nginx can handle this then i’d rather avoid using them together. i prefer to blame php here though.
greetings, eMBee.
As far as nginx is concerned we are just telling it every file in a certain path needs to be processed by PHP. That path is just given by a regular expression. So yes, PHP is to blame here. It’s a legacy feature which doesn’t work well with the way things are done today.
This isn’t a problem with php-fpm and security.limit_extensions right?
I think it should still be a problem. The URL will have .php ending and fix path info will make it map to the actual .php file so security.limit_extensions shouldn’t prevent this.
I highly recommend always turning fix path info off in php.ini
[…] Why Path Info is the Worst PHP Feature Since Register Globals by Martin Fjordvald […]
Thanks for such insightful articles. I’m learning a lot here.
I suggest you to add the Nginx tag to this post so it’s as easier to find as the rest related to Nginx.
Also, your articles are linked from several places but using an URL schema that seem’s you have changes. It would be great you add a rewrite rule in your configuration so these URLs can be redirected to the new ones.
Thanks again!