Shared Hosting

Published in PHP Architect on 23 Mar 2004

Welcome to another edition of Security Corner. This month, I have chosen a topic that is a concern for many PHP developers, shared hosting. Through my involvement with the PHPCommunity.org project, my contributions to various mailing lists, and by keeping tabs on PHP blogs and news sites, I have seen this topic brought up in various incarnations. Some people are concerned about hiding their database access credentials, some are concerned about safe_mode being enabled or disabled, and others just want to know what they should be concerned about, if anything.

As a result, I have decided to address these concerns in as much detail as possible, so that you will have a better understanding and appreciation of shared hosting security risks. After reading this article, you can decide if there is anything for you to be concerned about.

Shared Hosting

Since the advent of HTTP/1.1 and the required Host header, shared hosting has become very popular. Without this header, there is no direct way for a web client to identify the domain. It makes a TCP/IP connection to the host identified by the domain, and that's where it sends its reqquest, which can be as simple as the following:

  1. GET /path/to/script.php HTTP/1.0

Notice that the URL presented in the request does not include the domain name. This is because it is unnecessary under the assumption that only one domain is served by a particular server. With HTTP/1.1, Host becomes a required header, so a simple request must be expressed as follows:

  1. GET /path/to/script.php HTTP/1.1
  2. Host: example.org

With this format, a single web server can server many domains, because the client must identify the domain. As a result, a hosting company can host many domains on a single server, and it is not necessary to have a separate public IP for each domain. This yields much more inexpensive hosting and has spurred a tremendous growth in the web itself. Of course, this has been a driving force behind early PHP adoption as well.

The downside to shared hosting is that it incurs some security risks that do not exist in a dedicated server environment. Some of these risks are mitigated by PHP's safe_mode directive, but it doesn't address the root cause of most security risks, and a solid understanding of these risks is necessary to appreciate what safe_mode does and doesn't do. Because of this, I begin by introducing some of the unique risks associated with shared hosting.

Filesystem Security

A true multi-user operating system such as Linux is built upon a fundamentally secure approach to user permissions. When you create a file, you specify a set of permissions for that file. This is achieved by assigning each file both user and group ownership as well as a set of privileges for three groups of people:

The privileges that you can assign each category of user include read, write, and execute (there are some other details, but they are irrelevant to the present discussion). To illustrate this further, consider the following file listing:

  1. -rw-r--r-- 1 chris shiflett 4321 May 21 12:34 security-corner

This file, security-corner, is owned by the user chris and the group shiflett. The permissions are identified as -rw-r--r--, and this can be broken into the leading hyphen (indicating a normal file), and three groups of permissions:

These three sets of permissions correspond directly to the three groups of users: user (chris), group (shiflett), and other.

Linux users are probably familiar with these permissions and how to change them with commands such as chown and chmod. For a more thorough explanation of filesystem security, read Linux Security HOWTO: Files and File System Security.

As a user on a shared host, it is unlikely that you will have read access to many files outside of your own home directory. You certainly shouldn't be able to browse the home directory or document root of other users. However, with a simple PHP script, this can be possible.

Browsing with PHP

For this discussion, assume that the web server is Apache and that it is running as nobody. In order for Apache to be able to serve your web content, that content must be readable by nobody. This includes images, HTML files, and PHP scripts. Thus, if someone could gain the same privileges as nobody on the server, they would at least have access to everyone's web content, even if precautions are taken to prevent access by any other user.

Whenever Apache executes your PHP scripts, it of course does so as nobody. Combine this with Files and File system SecurityPHP's rich set of filesystem functions, and you should begin to realize the risk. To make the risk clearer, I have written a very simplistic filesystem browser in PHP (See Listing 1). This script outputs the current setting for the safe_mode directive (for informational purposes) and allows you to browse the local filesystem. This is an example of the type of script an attacker might write, although several enhancements would likely be added to make malicious actions more convenient.

Listing 1:

  1. <?php
  2.  
  3. echo "<pre>\n";
  4.  
  5. if (ini_get('safe_mode')) {
  6.     echo "[safe_mode enabled]\n\n";
  7. } else {
  8.     echo "[safe_mode disabled]\n\n";
  9. }
  10.  
  11. if (isset($_GET['dir'])) {
  12.     ls($_GET['dir']);
  13. } elseif (isset($_GET['file'])) {
  14.     cat($_GET['file']);
  15. } else {
  16.     ls('/');
  17. }
  18.  
  19. echo "</pre>\n";
  20.  
  21. function ls($dir)
  22. {
  23.     $handle = dir($dir);
  24.     while ($filename = $handle->read()) {
  25.         $size = filesize("$dir$filename");
  26.  
  27.         if (is_dir("$dir$filename")) {
  28.             if (is_readable("$dir$filename")) {
  29.                 $line = str_pad($size, 15);
  30.                 $line .= "<a href="{$_SERVER['PHP_SELF']}?dir=$dir$filename/">$filename/</a>";
  31.             } else {
  32.                 $line = str_pad($size, 15);
  33.                 $line .= "$filename/";
  34.             }
  35.         } else {
  36.             if (is_readable("$dir$filename")) {
  37.                 $line = str_pad($size, 15);
  38.                 $line .= "<a href="{$_SERVER['PHP_SELF']}?file=$dir$filename">$filename</a>";
  39.             } else {
  40.                 $line = str_pad($size, 15);
  41.                 $line .= $filename;
  42.             }
  43.         }
  44.  
  45.         echo "$line\n";
  46.     }
  47.  
  48.     $handle->close();
  49. }
  50.  
  51. function cat($file)
  52. {
  53.     $contents = file_get_content($file);
  54.     echo htmlentities($content, ENT_QUOTES, 'UTF-8');
  55. }
  56.  
  57. ?>

One of the first places an attacker might want to glance is /etc/passwd. This is achieved by either browsing there from the root directory (where the script begins) or visiting the URL directly (by calling the script with ?file=/etc/passwd).

This gives an attacker a list of users and their home directories. Another file of interest might be httpd.conf. Assuming each user's home directory has a directory called public_html for their respective document roots, an attacker can browse another user's web content by calling the script with ?dir=/home/victim/public_html/.

A security-conscious user will most likely keep sensitive configuration files and the like somewhere outside of document root. For example, perhaps the database username and password are stored in a file called db.inc and included with code similar to the following:

  1. <?php
  2.  
  3. include '../inc/db.inc';
  4.  
  5. ?>

This is wise, but unfortunately an attacker can still view this file by calling the browse.php script with ?file=/home/victim/inc/db.inc. Why does this necessarily work? For the include call to be successful, Apache must have read access to the file. Thus, this script must also have access. In addition, because the user's login credentials are often the same as the database access credentials, this technique might allow an attacker to compromise any account on the server (and launch additional attacks from compromised accounts).

There is also the potential for an attacker to use this same script to gain access to anyone's session data. By just browsing the /tmp directory (?dir=/tmp/), it is possible to read any session that is stored therein. With a few enhancements to the script, it could be even easier to view and/or modify session data from these files. An attacker could visit your application and then modify the associated session to grant administrator access, forge profile information, or anything of the like. And, because the attacker can browse the source to your applications, this doesn't even require guesswork. The attacker knows exactly what session variables your applications use.

Of course, it is much safer to store session data in your own database, but we have just seen how an attacker can gain access to that as well. The safe_mode directive helps prevent these attacks.

Configuration Directives

The safe_mode directive is specifically designed to try to mitigate some of these shared hosting concerns. If you practice running the script from Listing 1 on your own server, you can experiment with enabling safe_mode and observing how much less effective the script becomes.

When safe_mode is enabled, PHP checks to see whether the owner of the script being executed matches that of the file being opened. Thus, a PHP script owned by you cannot open files that are not owned by you. Your PHP scripts are actually more restricted than you are from the shell when safe_mode is enabled, because you likely have read access to files not specifically owned by you. This strict checking can be relaxed somewhat by enabling safe_mode_gid. This directive relaxes the checking to the group instead of the user.

Because safe_mode can cause problems for users who have a legitimate reason to access files owned by another user, there are a few other directives that allow even more flexibility. The safe_mode_include_dir directive can specify one or more directories from which users can include files, regardless of ownership. I encourage you to read http://php.net/features.safe-mode for more information.

A similar PHP directive is open_basedir. This directive allows you to restrict all PHP scripts to only be able to open files within the directories specified by this directive, regardless of whether safe_mode is enabled.

Bypassing safe_mode

Of course, safe_mode only protects against people using PHP to gain access to otherwise restricted data. It does nothing to protect you against someone on your shared server who writes a similar program in another language. For example, why would the Perl interpreter care what's in php.ini? The manual states:

It is architecturally incorrect to try to solve this problem at the PHP level, but since the alternatives at the web server and OS levels aren't very realistic, many people, especially ISP's, use safe mode for now.

Consider the following CGI script written in Bash:

  1. #!/bin/bash
  2.  
  3. echo "Content-Type: text/plain"
  4. echo ""
  5. cat /etc/passwd

This will output the contents of /etc/passwd as long as Apache can read that file. So, we're back to the same dilemma. While the attacker can't use the script in Listing 1 to browse the filesystem when safe_mode is enabled, this doesn't prevent the possibility of similar scripts written in other languages.

What Can You Do?

You probably knew that a shared host was less secure than a dedicated one long before this article. Luckily, there are some solutions to a few of the problems I have presented, but not all. There are basically two main steps that you want to take on a shared host:

How do you keep your database access credentials safe? If another user can potentially have access to any file that we make available to Apache, it seems that there is nowhere to hide them. My favorite solution to this problem is one that is described in the PHP Cookbook by David Sklar and Adam Trachtenberg.

The approach is to use environment variables to store sensitive data (such as your database access credentials). With Apache, you can use the SetEnv directive for this:

  1. SetEnv DB_USER "myuser"
  2. SetEnv DB_PASS "mypass"

Set as many environment variables as you need using this syntax, and save this in a separate file that is not readable by Apache (so that it cannot be read using the techniques described earlier). In httpd.conf, you can include this file as follows:

  1. Include "/path/to/secret-stuff"

Of course, you want to keep these include statements within each user's VirtualHost block, otherwise all users could access the same data.

Because Apache is typically started as root, it is able to include this file while it is reading its configuration. Because each child process handling a request runs as nobody, it can no longer access this file directly, so other users cannot access this information with clever scripts.

Once these environment variables are set, you can access them in the $_SERVER superglobal array. For example:

  1. <?php
  2.  
  3. mysql_connect('localhost', $_SERVER['DB_USER'], $_SERVER['DB_PASS']);
  4.  
  5. ?>

Because this information is stored in $_SERVER, you need to take care that this array is not output in any of your scripts. For example, a call to phpinfo() reveals everything from $_SERVER, so you should ensure that you have no public scripts that execute this function.

Until Next Time...

Hopefully you now understand some of the risks involved with shared hosting and can take some steps to mitigate them. While safe_mode is a nice feature, there is only so much help it can provide in this regard. It should be clear that these risks are actually independent of PHP, and this is why other steps are necessary.

As always, I'd love to hear about your own solutions to these problems. Until next month, be safe.