Some common PHP security pitfalls…
… and how to exploit them.
Chapter One: Image uploading.
Allowing users to upload files to your server needs to be done carefully. Very carefully. If a user manages to upload a (PHP) script, they’ll be able to do pretty much anything with your server, including the database (if any).
Let’s assume for a moment that you only want to allow images to be uploaded. So how can you make sure the file is really an image? Some might say “Hey, there’s $_FILES[‘file’][‘type’], which holds the file’s content type.”
I’ve seen many people (INCLUDING w3schools!) relying on this value for verification. But not only comes this value directly from the CLIENT, it can also be easily modified/faked.
I could upload a PHP file with an image/jpeg content type, and it would pass the verification. Everyone’s site using the code from w3schools is vulnerable.
Don’t EVER rely on this value.
The first thing you should do, is validate the file extension. Only files that are parsed by the server (or the user’s browser) are dangerous to us. And by default, the server only parses certain files with certain extensions, such as .php, .asp, .pl, .py, etc… Depending on what you have installed.
Validating the extension is quite easy:
<?php
// Valid file extensions.
$valid_extensions = array('.jpg', '.jpeg', '.png', '.gif', '.tif', '.tiff');
// Get current file extension
$extension = strpos($filename, '.') !== false
? strrchr($filename, '.')
: 'none';
$extension = strtolower($extension);
if (!in_array($extension, $valid_extensions))
{
// Invalid file... throw error and DO NOT UPLOAD
}
else
{
// Everything is cool... continue.
}
?>
NOTE: Some servers parse .gif files by default. I’m not exactly sure why, but try it out to be on the safe side. Valid and normal looking GIF (as well as other) images can contain PHP code. So you don’t want to have those images parsed.
Secondly, you can use PHP’s image functions to verify if the file is an actual image. They’re not 100% bullet-proof, but it’s a fairly good start.
$type = @exif_imagetype($_FILES['image']['tmp_name']);
if (!$type OR !in_array($type, array(
IMAGETYPE_JPEG,
IMAGETYPE_GIF,
IMAGETYPE_PNG,
IMAGETYPE_TIFF_II,
IMAGETYPE_TIFF_MM
)))
{
// Invalid image... do not upload
}
else
{
// Everything's cool
}
If you don’t have the EXIF extension installed, use getimagesize().
As I said, this is not 100% bullet-proof, meaning this can be exploited as well. But it’s a very good start.
Another option:
Upload the files to a non-public directory. The user needs to access the file using his browser in order to execute it. And this is impossible if the file is outside the public directory.
If you need to display the images at some point to the user, use a PHP script to read the file’s contents. This way they won’t be parsed and can’t cause trouble.
$file = '/path/to/some/file.jpg';
if ($type = @exif_imagetype($file))
{
header('Content-Type: ' . image_type_to_mime_type($type));
readfile($file);
}
else
{
// Error, not an image...
}
Another safe, but “not-so-optimal” option is storing the file in a BLOB field in the database. I wouldn’t suggest doing this if you plan on storing a lot of images, though.
And last, I highly suggest you to read this PDF from Scanit. It’s a good and mind opening read. (It also contains a Perl script which allows custom content types).
Chapter Two: HTTP header redirects.
This is one of the things I’ve seen too many times as well. People have protected areas on their pages which require some kind of authentication. If the users fails to be authenticated, they’re redirected to the log-in page. Usually, there’s nothing wrong with that. But a lot of people seem to forget to EXIT their script after redirecting.
The browser follows the redirect header for your COMFORT. It’s just a function and can be disabled easily. And if you do not exit the script, it’ll continue to run, meaning, the user will be able to see the output even after sending the header.
I’ve exploited this here, for example. And even ImperialBB was vulnerable to this a few years ago, before i reported the issue. I was able to see protected administrator forums.
BAD:
if (empty($_SESSION['userid']))
{
header('Location: login.php');
}
GOOD:
if (empty($_SESSION['userid']))
{
header('Location: login.php');
exit;
}
http://www.php.net/exit
http://www.php.net/header
Chapter Three: Cross site scripting (XSS).
Another thing I see on a daily basis:
<form action="<?php echo $_SERVER['PHP_SELF']; ?>" method="post">
I won’t even lie, I was guilty of doing this too in my early PHP days. Until I learned that I could append JavaScript to the URL in the address bar, and it would be injected directly into the page I was on.
example.com/page.php/<script>alert(‘XSS’);</script>
… the above is a valid URL, and PHP_SELF would contain this bit of JavaScript. Needless to say, this alert() is harmless, but I could inject any code, and this could be potentially harmful.
Some sites use .htaccess to block requests that contain <script> tags. But in some cases, this can be bypassed by adding for example type=”text/javascript” to the tag. If you’re already using this attribute, try again without it. It all depends on how weak the regex they’re using is.
Solution? It’s even easier than any line of code… It’s as simple as leaving the action=”” attribute in BLANK! This will force the browser to submit the page to itself. Exactly as PHP_SELF would do.
<form action="" method="post">
Cross site scripting goes a lot further than that, though. But I’m only going to cover this as of now, ‘cause it’s probably the mistake I see the most. You might want to take a look at this post, if you haven’t already.
Chapter Four: Email header injection.
Another common one:
mail(
'to@address.com',
'Some subject',
'Some message',
"From: {$_POST['email']}"
);
I’ve seen this a lot in contact forms. Assuming $_POST[‘email’] is unfiltered, I would be able to inject CC/BCC headers, and thus, allowing me to send emails to anyone I wanted using your server. Now if I had some spare Viagra, or a certain “enlargement” service, I could make good use of this.
Make always sure the email is valid, and does not contain new lines or carriage returns. Just because it contains an @, it doesn’t mean it’s a valid email (apparently some people think it does).
Here’s a good one:
function is_valid_email($email)
{
return (bool) preg_match(
'~^([^\\x00-\\x20\\x22\\x28\\x29\\x2c\\x2e\\x3a-\\x3c' .
'\\x3e\\x40\\x5b-\\x5d\\x7f-\\xff]+|\\x22([^\\x0d' .
'\\x22\\x5c\\x80-\\xff]|\\x5c[\\x00-\\x7f])*\\x22)' .
'(\\x2e([^\\x00-\\x20\\x22\\x28\\x29\\x2c\\x2e' .
'\\x3a-\\x3c\\x3e\\x40\\x5b-\\x5d\\x7f-\\xff]+|' .
'\\x22([^\\x0d\\x22\\x5c\\x80-\\xff]|\\x5c\\x00' .
'-\\x7f)*\\x22))*\\x40([^\\x00-\\x20\\x22\\x28' .
'\\x29\\x2c\\x2e\\x3a-\\x3c\\x3e\\x40\\x5b-\\x5d' .
'\\x7f-\\xff]+|\\x5b([^\\x0d\\x5b-\\x5d\\x80-\\xff' .
']|\\x5c[\\x00-\\x7f])*\\x5d)(\\x2e([^\\x00-\\x20' .
'\\x22\\x28\\x29\\x2c\\x2e\\x3a-\\x3c\\x3e\\x40' .
'\\x5b-\\x5d\\x7f-\\xff]+|\\x5b([^\\x0d\\x5b-' .
'\\x5d\\x80-\\xff]|\\x5c[\\x00-\\x7f])*\\x5d))*$~',
);
}
(I don’t take credit for that regex. Can’t remember where I got it from.)
To be continued…