Security with PHP

Vincent Danen

March 25, 2008

PHP is a server-side scripting language that is extremely versatile, powerful, and in use by many systems. It's popularity has increased to the point that any web-hosting service worth it's salt provides the capabilities to use it. That being said, PHP can also be dangerous, especially in a web-hosting environment, if configured incorrectly. Because PHP is server-side, and because it can interact with the filesystem of the host environment, it can be used to obtain data that should otherwise remain confidential from the client. For instance, a web hosting company may do appropriate checks to disallow CGI scripts for their clients, or wrap them in suexec so that they have no privilege than the user who owns them. They may limit shell access, or disallow it entirely, and they may chroot FTP users who connect to the site to maintain web pages. But unless they chroot the web server itself, which is almost impossible to do on a client-by-client basis (unless you run one web server per client, run as that client), chances are if PHP is permitted, a client can write a PHP program, upload it, and use it to traverse the filesystem. They may even be able to get access to other client's web data, a very dangerous thing.

Fortunately, PHP allows us to restrict a lot of this if configured properly. Out of the box, a lot of vendors who ship PHP do not do this type of enforcement. A lot of PHP applications do not respect or work with this type of enforcement. Your best bet is to reconfigure PHP for maximal security, and then find software that works with it, or reduce some of these security settings only if you need to, and only if you know the risks involved.

Some of PHP's security is built in and one doesn't need to think about it. For instance, if PHP is compiled as a CGI binary, it will not interpret commandline arguments. This is a good thing. Unfortunately, you can use another type of attack, such as interpretting any file on the file system. One means of avoiding this type of attack is to compile PHP with the enable-force-cgi-redirect --enable-force-cgi-redirect configure directive. This is, of course, only really necessary if you use PHP as a CGI binary. If you use Apache as your webserver, chances are you are using PHP as an Apache module. The other option, if you must use PHP as a CGI binary, is to have the PHP parser outside of the web tree; for instance, install it as /usr/local/bin/php or /usr/bin/php. Don't include it as a binary in the /cgi-bin directory for a given host.

The more popular method of using PHP is as an Apache module, and for the rest of this document we will assume it is installed as such. The benefits to using PHP in this manner is that you can take advantage of Apache's access controls and security features as well. The drawback is the inheritence of the script being run as the same user Apache, typically "nobody" or "apache". This gives the script the same user to run as as everyone else on the system, which could cause issues when database access, file access, etc. are involved.

The following are various points on some methods of securing PHP:

Error Reporting

When PHP encounters an error on a site, it reports the error to the browser. This is great for development, but poor for a live site. It can expose the value of variables, file locations, etc. There are two ways this can be done. The first, and best, is to modify your php.ini (usually found in /etc) as follows:

register_globals Off
log_errors On
display_errors Off
error_log /var/log/php/errors

This will log errors to the file /var/log/php/errors (make sure the /var/log/php directory exists first!) instead of to the client viewing the page. The other option, which is not advisable (because many programmers do not do it, so unless you only use PHP scripts you wrote (impossible in a virtual hosting environment), you may not even want to consider this), is to use the error_reporting() function in PHP. You can set it to error_reporting(E_ALL) during development and error_reporting(E_NONE) during deployment.

Register Globals

In the code above, we turned Register Globals off (register_globals Off). This disables the ability for any user-submitted variable to be injected directly into PHP code. This reduces the amount of variable poisoning that may occur, and internal variables are pretty much isolated from user-submitted data. For instance, assume the URL http://localhost/script.php?user_val=1. With register_globals on the variable $user_val has a value of 1. If $user_val is an internal variable that permits access to secret documents, an attacker can give themself access without providing any credentials.

With register_globals Off this cannot be done. You have to explicitly request the value of a variable; it cannot be injected in the manner above. For instance, in the above example, $user_val is likely an internal variable that gets set depending upon certain conditions. Let's assume one of these conditions is a username that is obtained via a form. You would use something like the following:

<?
if($_REQUEST['username']) {
  $username = $_REQUEST['username'];
} else {
  $username = "";
}
?>

The above could be considered transitional code. For instance, if you make reference to $username throughout your code, changing each instance of $username to $_REQUEST['username'] may be troublesome. Of course, with the above, someone cannot simply manually call a script and inject any variable they want, or change the value of known internal variables.

You can also turn register_globals on or off on a virtualhost-by-virtualhost basis. You can do this by modifying your httpd.conf and setting php_admin_value register_globals 0 in your VirtualHost directive for a specific site, or using the Apache .htaccess file and specifying php_flag register_globals 0 (0 for off, 1 for on). If you use the .htaccess route, ensure your VirtualHost has a Directory stanza that enables Overrides (AllowOverride Options (or All)).

Safe Mode

PHP comes with a feature called "Safe Mode" which tries to ease security implications of virtual hosting or shared-server instances. Things like being able to read other user's datafiles and scripts is part of what Safe Mode guards against. There are a few php.ini directives related to Safe Mode:

  • safe_mode (0|1) - whether or not to enable Safe Mode.
  • safe_mode_include_dir (string) - allows to set directories for including files (with include() or require(), etc.). This can be more than one path, separated by colons. This restriction is by prefix, not complete path, unless you add a trailing slash at the end of the path.
  • safe_mode_exec_dir (string) - only allow programs in this directory to be called using system() and other similar functions.
  • safe_mode_allowed_env_vars (string) - allows to define a comma-delimited list of what environment variables can be altered by users; string here is the prefix of the environment variables. By default, PHP only allows users to set environment variables that begin with PHP_ (ie. PHP_FOO=BAR).
  • safe_mode_protected_env_vars (string) - allows to define a comma-delimited list of what environment variables a user cannot change, even if it should be allowed by safe_mode_allowed_env_vars.

Additional Directives

There are two more directives that can be set in php.ini that are very useful:

  • disable_functions (string) - this allows you disable certain functions. It is a comma-delimited list of function names to disable. It is not affected by whether Safe Mode is on or off, and can only be set in php.ini.
  • open_basedir (string) - this limits the files that can be opened by PHP to the specified directory tree, including the file itself. It is also not affected by whether or not Safe Mode is enabled. Like safe_mode_include_dir, the string here is a prefix; to make it absolute, include the trailing slash of the directory path.

open_basedir is extremely useful because it can effectively "jail" scripts from operating on their own directories. It is essential to a web-sharing or virtual hosting system. For instance, assume you had the directory /home/www/somewhere.com/html as the base directory for the website somewhere.com. The user may wish to put files outside of the web tree, such as configuration files, etc. (for added security or to prevent casual exposure). You chroot them, via FTP, to /home/www/somewhere.com and they've created the subdirectory includes/.

open_basedir /home/www/somewhere.com

This will allow the user to include files in html/ (where the PHP scripts would live), and includes/ (where configuration data may live). Unlike safe_mode_include_dir, openbase_dir not only limits included files, but all files; it resolves symlinks and if they point to a file outside of the specified directory tree, it cannot be opened. This will work with system-type functions like fopen() and others; essentially any function that tries to open a file on the local file system. You can make this a comma-delimited list so that you can have more than one directory specified.

Setting PHP Configuration Settings

There are a few ways you can set PHP configuration settings outside of php.ini. php.ini is the global file to cover the entire system, so it should contain sane defaults, however some administrative options can be set in the httpd.conf file or .htaccess files (for Apache).

The php_value (name value) directive will set the directive name to value. The php_flag (name on|off) directive will set a boolean configuration directive, on or off. Both of these directives can be used within .htaccess files.

The administrative equivalents, which can only be set in the httpd.conf files, are php_admin_value and php_admin_flag, which take the same arguments and do the same thing except that they cannot be used within .htaccess files, and they cannot be overriden by .htaccess or virtualhost directives.

A VirtualHost Example

Taking the following information, a reasonable template for an Apache VirtualHost directive may look like this:

<VirtualHost 127.0.0.1>
DocumentRoot /home/www/somewhere.com/html
ServerName somewhere.com
ServerAlias www.somewhere.com
php_admin_value open_basedir /home/www/somewhere.com/
</VirtualHost>

Then you may set something like the following in your php.ini:

safe_mode On
register_globals Off
log_errors On
display_errors Off
error_log /var/log/php/errors

Also see the PHP manual for Functions restricted/disabled by safe mode to see what else is affected when you enable Safe Mode.

References