PHP Guidelines

PHP is used extensively in the GNOME web infrastructure. And with good reason; it's well adapted to the task of creating web content and familiar to many people. However, like any form of server-side scripting there are security considerations. A poorly thought script can pose dangers both for particular service in question and for other services running on gnome.org. For this reason, the current policy on gnome.org is that any PHP must be reviewed by the sysadmin team before it goes live. This page gives guidelines to be used when creating PHP for use on gnome.org and for reviewing proposed PHP scripts.

A global thing to be aware of is that all PHP scripts on gnome.org are run as the 'apache' user. So there's no way of isolating one PHP application against another or against local users on the machine. Any file (such as a password file) that can be read by the PHP script can be read by another application or by a local user. Any file that can be written to by the PHP script can be written to be a local user. While the goal of the review process and these guidelines is to prevent external attackers from using bugs in one application to attack another application or the system, if your application application has security needs that are more than nomimal, PHP is likely not the right technology. A CGI script running as a special user with SUExec is likely more appropriate.

Validating input

Most security vulnerabilities in web scripts come from insufficiently validated input. Whether you are creating a web page, a SQL command, a filename, or a shell command to be excecuted, if you substitute in unchecked input from the user bad things will happen.

  • Validate all external input. External input includes values from the URL query string ($_GET[]), values from forms ($_POST[]), cookie values ($_COOKIE[]). Any value that comes from the user's machine must not be trusted, even if in normal operation you set that value yourself. The easiest way to make sure that a script is validating all external input is to never directly use the $_GET[], $_POST[], $_COOKIE[] variables, but instead always assign the value (after validation) to a local variable.
  • Specify the characters you want, not the characters that you don't want. Trying to filter out dangerous metacharacters like '<', '|', ';' is very error prone. The set of dangerous metacharacters is different for shell, html, SQL, and so forth, and it's easy to forget one. Instead, specify the characters that you do want. For instance, if a parameter is supposed to be an integer, it should match the regular expression /^[0-9]+$/. (Note that the regular expression is anchored at both the start and the end.)

Quoting

In some cases, requiring a particular syntax isn't possible. If the input is a human-readable text: a name, a comment, an address, even a phone number, then restricting the set of allowed characters is likely to cause problems. (Remember, that just because your name matches /^[A-Za-z ]+$/) doesn't mean that everybody's name will.) In this case, we need to use quoting instead.

  • Quote database parameters with addslashes().
  • Quote web content with htmlspecialchars(). Allowing users to enter HTML that is then inserted into output should be avoided; writing a sanitizer that prevents any possible malicious thing (unclosed tags, javascript, links to images) is close to impossible.
  • Don't try to quote shell command lines. While quoting arbitrary text to insert into a command line isn't that hard, it's quite no-obvious how to do it and there is no PHP builtin that does the right thing. If you need to pass arbitrary input to an external command, use an environment variable or standard input.

External command execution

Running external commands leads to entirely new areas of potential vulnerability. It's best avoided, but sometimes is useful. There are a lot of PHP functions that execute external executables, including: exec(), passthru(), popen(), proc_open(), shell_exec(), system(), and the backtick operator. All uses of these need to be checked carefully.

  • Use fixed command lines with full pathnames to binaries when possible.
  • Sanitize input to the external command as much as possible. Don't assume that the external command has been written with security in mind.
  • Sanitize input inside the external command as much as possible. Don't assume the PHP frontend has been written with security in mind; the external command might be run by a local user or through a whole in someone else's PHP script.

Filesystem access

Reading an unintended file may reveal sensitive information. Even "harmless" information such as program source may provide help to someone trying to figure out how to crack the system. Writing to or deleting an unintended file is obviously worse.

  • Use care when constructing filenames. If your script can be tricked into using a filename with metacharacters like '.' and '/' in it, then you may be accessing completely a file in a completely different directory than you expect.
  • Be careful about race conditions. If you are writing into a directory where you don't have exclusive access (and PHP scripts run unprivileged, so this is always the case), then operations like checking to see if a file exists before writing to it are never safe. Use tmpfile() if you need to create a temporary file. Use the x flag to fopen() when creating a non-temporary file.

Passwords

Hardcoding a database or other password into a script should be avoided because web server misconfiguration can easily allow people to see the text of the PHP script rather than the script itself.

  • Passwords should be outside of the web server space. Instead of specifying the password directly, include a small php fragment outside of the directories that the web server that sets the password. However, see the note at the top of this page: such files are still readable to everybody with login access on the machine, and could be revealed publically by someone else's buggy PHP. (Ask the sysadmin team about creating a password file readable only by apache if you run into this situation. That at least helps the local user situation.)
  • Use readonly database accounts when possible. If your script just needs to read a database, not write it, then log into the database as a user that doesn't have the ability to write to the database.
  • Don't check passwords into publically acessible CVS. Obvious, but easily forgotten.

References