On the Security of PHP, Part 2

Tuesday Nov 13th 2001 by Jordan Dimov

We conclude our look at securing PHP code with some advice on programming guidelines, user-input filtering, and configuration settings. Upon finishing this lesson, you should be alert to the major liabilities of working with PHP.

Review Part 1.

Secure Programming Guidelines

The way to secure PHP scripts is through a carefully selected combination of configuration settings and safe programming practices. Based on the vulnerabilities that we have studied so far, we will now set forth to establish some rules that can help avoid dangerous situations.

Using PHP Safe Mode

PHP can be set up so that it executes scripts in a restricted environment to decrease the amount of damage that can be inflicted by insecure programs. This modus operandi is called "safe mode". The configuration directive safe_mode in php.ini turns safe mode on and off. The safe_mode_exec_dir directive specifies a directory from which scripts can be loaded. PHP will not execute a script if it is not in this directory. Furthermore, PHP will not let a script call another program that is not in this directory. This way, even if there is a security hole in the script that allows attackers to run arbitrary commands on the script, they will be limited to whatever is in the safe mode executable directory.

As a general rule, if it is not absolutely necessary for scripts to be able to alter a variable, protect it.

To prevent tampering with environment variables, PHP safe mode makes use of another php.ini configuration setting that restricts the user's ability to modify them. The field safe_mode_allowed_env_vars contains a list of prefixes that identify the names of the environment variables the user is allowed to change. Thus, any environment variable whose name begins with something not listed in safe_mode_allowed_env_vars cannot be altered from within a PHP script. The default list consists of the prefix "PHP_" only. As we have seen, some of the PHP_ variables also contain sensitive information, so this restriction does not always solve the problem completely.

Another cognate configuration setting is safe_mode_protected_env_vars. The list given to this directive specifies names of environment variables that the user is not allowed to modify. The protected variables cannot be altered even if they are also present in the safe_mode_allowed_env_vars list. By default, the only protected variable is $LD_LIBRARY_PATH.

For increased safety, it would be best to use both settings as complementary, placing as many "endangered" environment variables in the directive safe_mode_protected_env_vars as possible. As a general rule, if it is not absolutely necessary for scripts to be able to alter a variable, protect it. By all means, try to protect $PATH and friends. If this is not an acceptable solution, regard all unprotected variables with distrust and handle them with care.

So safe mode is good as a concept, but it is not without its problems. In PHP3, for example, popen() does not follow the general rule of passing everything through EsacpeShellCmd(). Thus, it is possible to execute external programs outside of the directory specified in safe_mode_exec_dir:

 $fp = popen("ls -l /opt/bin/; /usr/bin/id", "r");
 echo "$fp<br>\n";
 while ($line = fgets($fp, 1024)):
  printf ("%s<br>\n", $line);

This will print out a listing of the /opt/bin directory and will execute /usr/bin/id to show the UID and GID of the user running the Web server.

It has even been argued that due to such errors of omission in safe mode, hosts should not rely on it for security but rather use the CGI version of PHP in combination with chroot(). As we have seen, this is hardly a better alternative. It is, however, a good idea not to rely on safe mode exclusively, but to use it as reinforcement for already secure code.

Include Files

The Web server knows that a file is a PHP file by looking at the .php extension. Thus when you request a PHP page from the server, it first lets PHP interpret that file and then shows the results. If the server does not recognize the file extension, it will normally just show the contents of the file.

Sometimes it happens that a PHP script needs to include other files as part of itself. A lot of programmers have a tendency to name those include files with a .inc extension. The problem here is that if the server is not aware that those files are actually parts of PHP scripts, it will just show the code to whoever requested it. This gives attackers the opportunity to study the code for security holes all they want and to see any hard-coded data that may be secret.

There are several ways to prevent this. One way is to name all include files with a .php extension (or .php3, or whatever the server associates with PHP) so that the server will interpret them instead of showing them. Another possible solution is to associate .inc files with PHP. Yet another solution would be to prevent all .inc files from being displayed. In Apache, the last can be achieved by something like this:

 <Files ~ "\.inc$">
  Order allow, deny
  Deny from all

This must be a section in the httpd.conf file.

Probably the safest thing to do, however, would be not to put .inc files under DocumentRoot at all. Put them somewhere else and change include_path in php.ini. This way only your PHP scripts will be able to get to them and the Web server won't even see them.

Filtering User Input

We have seen that unanticipated user input can cause problems. Most dangerous are metacharacters that have special meaning to the shell, or to a database, or any other external program. Filtering user input consists of stripping off any such special characters from all data that comes from outside. This is a fairly efficient way of preventing some misbehavior on the part of the user.

Input can be filtered by running it through a regular expression that removes or escapes a set of metacharacters you provide. To simplify this process, PHP provides two functions that do this kind of thing. From the PHP manual:

 " EsacpeShellCmd() escapes any characters in a string that
   might be used to trick a shell command into executing
   arbitrary commands. This function should be used to make 
   sure that any data coming from user input is escaped 
   before this data is passed to the exec() or system() 
   functions, or to the backtick operator. "

The second function encloses the whole string in single quotes before you pass it as an argument to a program. The shell does not assign any special meaning to characters inside single quotes, so a pipe symbol for instance will just be passed on as part of the argument, instead of actually piping the output.

 " EscapeShellArg() adds single quotes around a string and
   quotes/escapes any existing single quotes allowing you 
   to pass a string directly to a shell function and having 
   it be treated as a single safe argument. This function 
   should be used to escape individual arguments to shell
   functions coming from user input. "

While those functions do their best to escape known bad symbols, there will always be some chance that they missed something. This is because different shells on different systems have their own different ideas of which characters to consider special, and an extensive list of all these characters will almost surely be either incomplete or too restrictive.

Functions that access the configuration options should not be fed with user input, even if it has been filtered.

Often it is better to construct your own regular expressions to filter the script's input. Instead of filtering out a large number of special characters, however, filter in only characters that are valid for that particular input. If the user is entering their name for example, only allow them to provide alphabetic characters. For an e-mail address field, only let in alphanumeric characters, underscores, dashes, dots, and the @ sign. Since the script has some information about the expected input anyway, use it to construct a good input filter. This will also solve the database interaction problem better than EscapeShellCmd() can.

Other Configuration Settings

Many other settings affect the behavior of PHP and it can be difficult to achieve the perfect balance between functionality and security. In this section, we will consider some of the configuration settings that are most closely related to the security of PHP scripts and will discuss their significance.

Almost all of these settings are found in the PHP configuration file php.ini (or php3.ini for PHP3.) This file is read and processed every time the PHP interpreter is invoked. Depending on how PHP is installed, this may be only once (if PHP is installed as a module of the Web server), or once for every script execution (if PHP is installed as a CGI application.) The values of some settings specified in the configuration file can be accessed and even changed at runtime from the script using ini_get() and ini_set(), respectively. This means that security settings can be adjusted on a per-script basis, but it also means that carelessly designed scripts have the potential to bypass their own security restrictions. Thus, functions that access the configuration options should not be fed with user input, even if it has been filtered.

In addition, for Apache module installations of PHP, the configuration settings can also be altered from httpd.conf and from .htaccess files local to each directory in which scripts reside. This can be used to give groups of scripts different restrictions and privileges, but again some of those settings can be changed from within the script.

Two configuration options directly control PHP's safe mode, which was discussed earlier -- safe_mode, which turns safe mode on or off, and the directive safe_mode_exec_dir, which specifies a directory from which PHP scripts are allowed to call external programs when safe mode is on. Another setting related to safe mode is doc_root -- PHP will not serve files that are outside this directory while in safe mode.

The directive open_basedir specifies the root of a directory tree outside of which scripts are not allowed to open files. That is, a script can not use fopen(), for instance, on a file that is not under that directory tree. By default, open_basedir is empty and PHP allows files to be opened anywhere (provided that the script has appropriate access permissions.)

When running as an Apache module, the directive enable_dl instructs PHP whether or not to enable dynamic loading of PHP modules with dl(). Dynamic loading is enabled by default, but when in safe mode, PHP will not allow the use of dl() anyway, because it is possible to bypass open_basedir and other restrictions with dynamically loaded modules.

Another restrictive directive is disable_functions. This lists comma-separated names of functions that PHP will just ignore. Putting dl() on this list is another way to forbid dynamic loading. It is probably good to put phpinfo() there, because it gives out so much information about the script and the host. For even tighter security, you can disable mail(), system(), and friends, even include(). Of course this also limits the functionality of the script quite severely. Generally, it is a good idea to disable functions that have the potential to do damage and that the scripts can do without.

By default, PHP makes all environment and server variables, all cookies, and all GET and POST variables globally accessible by name. This helps novice PHP programmers greatly -- they do not have to figure out how to retrieve this external data. But it is a dangerous practice in that important settings can accidentally be changed very easily and transparently. Worse yet, there isn't much to stop an attacker from submitting a form field with the same tag name as that of a variable containing some file name, for instance. The configuration directive register_globals controls this aspect of PHP's behavior, and it seems appropriate to turn it off. The default setting for this directive is on.

About the Author

Jordan Dimov is a software security consultant.

Mobile Site | Full Site
Copyright 2017 © QuinStreet Inc. All Rights Reserved