Writing More Secure PHP Programs

by James D. Keeline James@Keeline.com


PHP is a remarkably powerful open-source server-side scripting language. In very little code it is possible to do things which would be much more complex to do in other similar languages like Perl, ASP, JSP, ColdFusion, or C. However, the desire to hack together a quick solution can lead to insecure web applications which can place your data and, in some cases, your server at risk. This presentation will address some of these issues as they relate to PHP and offer some suggestions to get you thinking in a direction which will provide more security.

PHP is neither inherintly secure nor insecure. It is the responsibility of the programmer of a web application, the database administrator and the system administrator to ensure that security is not compromised at several levels as described in Mark Nenadov's article Developing Secure Web Applications.

  1. Operating System/Web Server Layer (Red Hat Linux with Apache)
  2. General Application Layer (issues common to any web application)
  3. Specific Application Layer (issues that relate directly to a given application)
  4. Data Layer (issues related to the storage of data for your application)

Because there are many ways to run PHP, I will limit this discussion to PHP 4.x as a module to the Apache web server 1.3.x running on the Red Hat Linux 7.x operating system. If you are running PHP as a CGI or Command Line application or you choose to run on Windows, your mileage may vary.

Security Philosophy

There are several basic tenets which will help you protect your web apps and your server.

Keep all software up to date.
In the past year there have been several updates to PHP to close security holes related to file uploads and heap errors. Make sure you are running 4.2.2 or later. Additionally, your operating system, web server, SSL, and database should be checked to ensure that all relevant bugfix/security updates are applied.
Don't trust user input. Ever.
In previous versions of PHP, any form variable named would automagically be created as a variable and the value from the input element would be placed in the variable. Current versions of PHP have register_globals in the off position. This solves some problems but creates others.
Initialize all variables and use Super Globals to retrieve values. Careful use of extract() can be helpful when getting large numbers of variables from a form. Validate all data.
Restrict functions and which environmental variables may be changed.
Use disable_functions to prohibit use of exec(), system(), passthru(), and backtick operators.
Consider blocking use of eval() function unless absolutely necessary.
Restrict access to filesystem.
Use safe_mode or open_basedir to limit which files and directories may be accessed with PHP programs.
Protect your database connections.
If you are not using database abstraction, place the database connection statements in a file which is outside of the web space and protected from prying eyes.
Create database users who only have minimum specific rights to tables required for a given application (ie select-only user or insert-only user).
Handle includes and file uploads responsibly.
Make use of is_uploaded_file() and move_uploaded_file() functions when handling file uploads. Restrict size of upload along with max_execution_time and memory_limit for scripts.
Validate MIME type of uploaded file before moving it to web-accessible space. Consider using different, unpredictable, filenames for files in web space or handler programs to prevent direct access.
Don't reveal that you are using PHP.
Security through obscurity can be weak and should not be used alone but it can mislead people who would wish to misuse your web applications.

Keep all software up to date.

The minimum version of PHP which should be run is 4.2.2. Version 4.3.0pre2 was released on Oct 27, 2002. For updated information on this, see the php.net site. Red Hat Linux 8.0 uses 4.2.2. You can locate RPM files at rpmfind.net. It is a good idea to check the MD5 key signatures for all RPMs downloaded from a network to ensure that you have not received a modified version from the trusted version (see man rpm for details).

When running a Red Hat Linux system, use the up2date program to be alerted to software updates related to security and bug fixes. For details on how to install this program, see the tutorial on yolinux.com for Linux System Administration under up2date. This will help you track all of the distribution packages on your system, including PHP, Apache, MySQL, OpenSSL, and others which might require upgrades to keep up with security issues.

Don't trust user input. Ever.

The importance of this cannot be overemphasized. A variable within a PHP script must be considered suspect unless it was explicitly set in the program. All input from users should be validated to ensure that it conforms to expected values. A simple PHP program might ask for a password and then check it with the following code:

       if ($password)
        { $valid_user = confirm_password($password); }
       # ....
       if ($valid_user == 1)
        { # Display data that an authorized user may see }
        { # Display login form }

In versions of PHP where a value in /etc/php.ini called register_globals is set to on then it is possible to circumvent the confirm_password() function by simply placing a get variable in the URL (script.php?valid_user=1). The results are obvious. This could be fixed if the value of $valid_user was set to 0 prior to calling the confirm_password() function.

We still have a problem, however, because we don't really know where the value for $password came from. Was it sent via POST (the method used in the form) or was it sent as part of the GET request in the URL by some miscreant? There are several "super global" arrays in PHP which can be used to extract values from particular input methods. Beginning in PHP 3, these had long names like $HTTP_POST_VARS. However, starting in PHP 4.1.0, shorter names, like $_POST were added to encourage their use. Below is a table with the old and new names and the variables they normally include.

Old NameNew NameDescription
$HTTP_GET_VARS $_GET Values from HTML forms using GET method or from variables created in URLs (script.php?var=value). Limited in length to the size of a URL which will be passed and processed.
$HTTP_POST_VARS $_POST Values from HTML forms using POST method which are stored in body of HTTP request rather than URL. These can be any length.
$HTTP_POST_FILES $_FILES Variables set when a file is uploaded from an HTML form via method described in RFC 1867 which uses method of POST and enclding type of multipart/form-data.
$HTTP_COOKIE_VARS $_COOKIE Variables retrieved from client which are stored as cookies in the visitor's web browser.
$HTTP_ENV_VARS $_ENV Variables retrieved from the server, also known as environmental variables, with the PATH variable used to determine where executable programs may be found, along with several others./td>
$HTTP_SERVER_VARS $_SERVER Variables retrieved from the server, often including environmental variables, PHP variables, and information about the visitor which were passed in the HTTP headers.
$HTTP_SESSION_VARS $_SESSION Variables retrieved from the server (either from the /tmp directory or a database) which are associated with a particular web visitor.
-- $_REQUEST Variables from any input method (GET, POST, COOKIE). Do not rely on this!
-- $_GLOBALS Includes all of the variables in the super globals listed above.

The longer variable names are considered depricated and may be removed at some future date. However, if your server is using an older version of PHP (shame on you), you can use the following line to create any of the shorter array names from the long ones.


Some security articles suggested values for variables_order in the php.ini file to set a specific sequence of which variables are set or to ignore certain input methods, such as GET, for automatic instantiation. Most of the articles closed that it would be better to turn register_globals off entirely for security.

Recent versions of PHP have register_globals in the php.ini file set to off which means that variables will not automatically be created and populated with values which cannot be trusted. However, even if you have installed a newer version of PHP, say from RPMs, the php.ini file with the more secure settings will not be installed on top of your existing php.ini file. Instead, in the case of RPMs, it is likely saved as /etc/php.ini.rpmsave. Under these circumstances it is a good idea to perform a comparison (diff /etc/php.ini /etc/php.ini.rpmsave) of the two files and try to understand the reason for changes.

With register_globals in the off setting, a new set of problems arises because many old programs and old tutorials will no longer work. The first reaction was to add simple commands to cause a script to act as if register_globals was on. An example of this is placing extract($_GLOBALS) at the top of the script. This is equivalent to the follwing code snippet:

     foreach($_GLOBALS as $key=>$value)
      { ${$key}=$value; }

where each variable in the $_GLOBAL associative array is is created and populated. However, this quickly and effectively negates the added security in favor of convenience. However, there are ways to have improved security using extract() which can limit where the data can come from and in certain cases actually improve security.


This first example accepts code only from the POST method and can stop easy-to-implement exploits as part of the URL. However, it is possible to create custom HTML forms or use the cURL and other libraries to send data via POST. In the long run, it won't stop a determined intruder. A better way is to use additional features of the extract() function:

     $fname=""; $lname=""; $email=""; $comment="";
     extract($_POST, EXTR_IF_EXISTS);

This works on PHP 4.2.0 and later versions. It will only instantiate variables from the POST array which already exist in the script. By setting them equal to empty values, we can set not only their initial value (an important security step as seen above) but also specify which variables may be received from the POST array. Naturally, this should be at the top of the program, above other variables which might be "hijacked" by an intruder.

Be sure to evaluate the values of all user input with regular expressions or other means to ensure that they hold values in the expected ranges. If you plan to use user input as arguments for functions which execute system commands, look into escapeshellarg() and escapeshellcmd() to filter out efforts to run additional system commands beyond what was intended in your script.

Restrict functions and which environmental variables may be changed.

As with your own internal and form variables, the variables from the $_SERVER array. Many of these can be rewritten simply by making a variable assignment. This could have disasterous effects if a value for the include_path or (initially set in php.ini) or $LD_LIBRARY PATH were altered to cause a PHP program to include() or require() code from a remote site (not running PHP) rather than a local file from a designated directory. This can be achieved with ini_set("include_path","<value>"); or the ini_alter() function. Current versions of PHP protect $LD_LIBRARY PATH in the php.ini with the following line:

     safe_mode_protected_env_vars = LD_LIBRARY_PATH

but only if safe_mode is in the on position (more on that in the next section). There is a complimentary parameter called safe_mode_allowed_env_vars which is discussed in Jordan Dimov's On the Security of PHP, Part 2

It is also possible to block the use of certain functions which are considered dangerous or that they reveal too much information about the server. The php.ini value called disable_functions in a comma-separated list. Below are some functions which are candidates for inclusion:

exec(), shell_exec(), system(), passthru(), popen(), proc_open() These functions execute command-line instructions which the web server user has permissions to run.
dl() Dynamically loads a PHP module.
phpinfo() Displays a wealth of information about how PHP is configured, the PHP modules available, and many values associated with the web server. This includes version numbers of software which may be vulnerable.

There are likely other examples of functions which you may wish to block. This can be done either in the php.ini or the Apache configuration file (httpd.conf) using the php_value Apache directive. Unfortunately, the disable_functions value cannot be changed in a VirtualHost container. This would be helpful if you trusted some domains more than others. For details of where a given value can be changed, see http://www.php.net/manual/en/function.ini-set.php.

Restrict access to filesystem.

Normally, a PHP script has access to the complete filesystem and all of the resources on the server. Any file which the web server user ("apache" or "nobody" on a Red Hat system) is available to PHP. This can allow a script to display files which must be readable by all users on a system, such as /etc/passwd, as web page. If the server was not using shadow passwords (where the password hashes are stored in /etc/shadow -- only the root user may read the file).

One way to limit the risk of this is to enable safe_mode in php.ini which restricts PHP scripts to files owned by the web server user. There are several additional parameters which may be used in conjunction with safe_mode. Details on this may be seen in http://www.php.net/manual/en/printwn/features.safe-mode.php.

However, this may not be a complete solution. Another avenue to consider is open_basedir which limits the PHP script to files in and below the directory specified. Like disable_functions, this can only be handled in php.ini or httpd.conf but not in a VirtualHost container. According to Rasmus Lerdorf's book, Programming PHP, you can use the Apache directive php_admin_value in a VirtualHost container (rather than php_value) to turn on safe_mode directives.

To restrict PHP scripts to the same user that owns the script, PHP is run on some servers in CGI (Common Gateway Interface) mode under Apache's suExec restrictions. suExec involves twenty security checks which must all pass successfully before a script can run. If it passes, a copy of the PHP interpreter is launched. This process can cause a noticable delay in execution for the same reasons it does in Perl scripts.

Protect your database connections.

Databases are of increasing importance to web sites. Many people create a database user to access data for the script and use the following SQL statement to assign permissions to a database:

     GRANT ALL ON db_name.* TO db_user@localhost IDENTIFIED BY 'password';

This gives db_user all types of rights to all of the tables in the database db_name. This is not wise. For many pages with public access, the only permission required is SELECT or INSERT. Only certain pages will need DELETE, DROP, ALTER, or UPDATE for example. Create separate users with the minimum permissions needed for a given task.

Also consider the security benefits of placing the database server on a separate machine with appropriate kernel-level firewall rules. This may have some performance disadvantages but the advantages may be more important.

Handle includes and file uploads responsibly.

PHP programers can create scripts which allow users to upload files. It is very important to use the new is_uploaded_file() and move_uploaded_file() functions to make sure that PHP is not fooled into working with internal files when it should be limited to files uploaded via POST. Before moving a file, check the MIME type to ensure that the file is the type expected. For example, if you are creating an image gallery, you would not expect PHP scripts to be uploaded.

Further, you may want to create a handler script to process image data or use different filenames in the web space from those which are uploaded. A database may be used to associate the names of the files.

Don't reveal that you are using PHP.

Security through obscurity is not very secure but it might prevent you from being a constant target among the casual intruders.

You can reconfigure Apache to process files with extensions other than .php, .php4, .php3, .phtml. Some users use other scripting extensions (.jsp, .asp, .cf, .pl, .cgi) or even .html as the extension for PHP scripts. Also you may wish to change expose_php to off.