homepage/content/papers/php-cgi.md

166 lines
6.3 KiB
Markdown

---
title: Running PHP as a CGI
description: Tricking PHP into behaving properly outside of Apache
section: computing
---
I'm the author of the
[eris HTTPd](https://github.com/nealey/eris),
a small web server intended for use on embedded Linux devices with low RAM and low storage.
I've used other web servers (boa, mathopd, thttpd, etc.) for years,
and this problem has been present for as long as I can remember.
A [recent gripe post about PHP](https://eev.ee/blog/2012/04/09/php-a-fractal-of-bad-design/)
inspired me to document it.
The Situation
-------------
Let's say I'm using something that isn't Apache,
and I need to run some PHP code.
PHP comes with a CGI variant,
so I'll use that:
#! /usr/bin/php-cgi
<?php phpinfo(); ?>
When run from the command-line, this does exactly what I expect.
Superb!
When I try it from a browser, though, I get:
Unable to load the webpage because the server sent no data.
Well that's puzzling.
So I do my standard thing,
wrapping it with something to dump stderr to stdout.
#! /bin/sh
echo "Content-type: text/plain"
echo
exec foo.php.cgi 2>&1
Now I get this (reformatted for narrow displays):
<b>Security Alert!</b> The PHP CGI cannot
be accessed directly.
<p>This PHP CGI binary was compiled with
force-cgi-redirect enabled. This means
that a page will only be served up if the
REDIRECT_STATUS CGI variable is set, e.g.
via an Apache Action directive.</p>
<p>For more information as to <i>why</i>
this behaviour exists, see the
<a href="http://php.net/security.cgi-bin">manual
page for CGI security</a>.</p>
<p>For more information about changing this
behaviour or re-enabling this webserver,
consult the installation file that came with
this distribution, or visit
<a href="http://php.net/install.windows">the
manual page</a>.</p>
It's writing out HTML, sort of: I mean, the first paragraph doesn't have `P` tags, but whatever.
So clearly they intend me to see it from a browser.
I mean, the interpreter binary is called `php-cgi`, after all.
And yet, they're not actually writing out a CGI header,
so the web server thinks it's a broken script and dies.
So maybe they meant me to view this from the command-line;
except the only way to get this error is to run it as CGI.
Anyway.
PHP is asking me to set the `REDIRECT_STATUS` variable,
and telling me that this is somehow a security concern.
It's interesting how Python, Perl, and Lua don't think the lack of this variable is a security problem.
A quick Google search tells me this is a non-standard CGI variable that Apache sets.
But I'm not using Apache, otherwise I'd just use mod_php.
Why are you making me set this, PHP?
What I thought would fix it
---------------------------
Okay, whatever.
#! /bin/sh
echo "Content-type: text/plain"
echo
REDIRECT_STATUS=1 export REDIRECT_STATUS
exec foo.php.cgi 2>&1
Now I get this:
Status: 404 Not Found
X-Powered-By: PHP/5.3.3-7+squeeze15
Content-type: text/html
No input file specified.
However, it continues to run just fine from the command-line.
404 means "file not found".
Which is very strange, because clearly the file *was* found,
given that PHP has sent its "X-Powered-By" header.
If it couldn't find the file, it wouldn't have known enough to start php-cgi.
And, remember, this all works fine from the command line.
Not to mention Python, Perl, Lua, and even the lowly Awk and Bourne shell,
have no trouble finding their input file when run as a CGI with the shebang (`#! /path/to/binary`)
as the first line.
What actually fixed it
----------------------
After nearly a full day trying to chase this cryptic message down in web searches,
I landed on a PHP bug open since 2004:
[PHP CGI depends on non-standard SCRIPT_FILENAME](https://bugs.php.net/bug.php?id=28227).
Included in the comments on this ancient but still unresolved bug is a now-broken link to
a wrapper
which proports to fix the problem.
So the ultimate fix to make `php-cgi` actually run like a CGI is to wrap it
with a second CGI that convinces PHP that I really meant it:
#! /bin/sh
REDIRECT_STATUS=1 export REDIRECT_STATUS
SCRIPT_FILENAME=$(pwd)/foo.php.cgi export SCRIPT_FILENAME
exec $SCRIPT_FILENAME 2>&1
If I have multiple PHP CGIs,
I must wrap each one.
Or just give up and install Apache,
which is, I'm sure,
the path taken by most system administrators who haven't written their own web server.
Why does PHP do this?
---------------------
I have skimmed [the URL that they asked me to](https://php.net/manual/en/security.cgi-bin.attacks.php).
They list two points:
1. "Interpreters open and execute the file specified as the first argument on the command line." This is true, it's how shebangs work (a file `script.sh` beginning with `#!/bin/sh` is magically transformed to `["/bin/sh", "script.sh"]`). It's how Python and Perl launch. I don't get the exploit path here, unless there's some horrible way to misconfigure Apache to do the wrong thing with scripts.
2. If you put the php interpreter under your web server's document root (where files are served from), and then use it to load up php scripts using the `PATH_INFO` environment variable *which is specified by the end user*, then PHP will dutifully serve up every document on your machine.
If you don't get it, (I didn't either), what they mean is that
some sysadmins honestly want the following URL to work:
http://yourhost/cgi-bin/php-cgi/var/www/yourhost-root/scripts/mything.php
This is such a terrible way to do things that it took me half an hour to even understand what they were describing. It's analagous to putting your shell in `/var/www/cgi-bin` and then asking the browser (which, remember, the end user can make do whatever they please) to pass the path to your shell script in as part of the URL.
These are both just awful, terrible,
hideous ways to set up a web server.
But apparently people do it anyway,
and apparently the PHP developers felt like it was their job
to allow this setup and still try to prevent
some of the security nightmares it entails.
Their solution: introduce dependencies on two Apache-specific
environment variables,
and one of them (`REDIRECT_STATUS`)
isn't even checked further than "is this set to anything at all".
And that, dear reader, is why you must fake out PHP
in order to get it to behave like every other CGI ever written.