PHP Image Upload Security: How Not to Do It

June 29th, 2012 | Posted by Darwish in Programming

Let’s take a break from talking about games for a brief journey into the world of web development. I’ve done a fair bit of work with PHP and I want to address the issue of secure file uploads.

File uploading is a scary thing for web developers. You’re allowing complete strangers to put whatever they want onto your precious web server. In this article I’ll be dealing entirely with the uploading of images, and how to ensure that what a user is giving you is actually an image.

Part I: The Evil $_FILES["file"]["type"]

Many times, I’ve seen (and—back in my youth—written) code that resembles the following:

$valid_mime_types = array(
    "image/gif",
    "image/png",
    "image/jpeg",
    "image/pjpeg",
);

// Check that the uploaded file is actually an image
// and move it to the right folder if is.
if (in_array($_FILES["file"]["type"], $valid_mime_types)) {
    $destination = "uploads/" . $_FILES["file"]["name"];
    move_uploaded_file($_FILES["file"]["tmp_name"], $destination);
}

The above snippet checks the uploaded file’s MIME type to validate it as an image, then moves the file to the appropriate location if it passes. So what’s the problem? Well, if you read the documentation page on handling file uploads, pay attention to what it has to say about $_FILES["file"]["type"]:

[T]his value is completely under the control of the client and not checked on the PHP side.

The first rule of web security is to never trust user-submitted data. Allowing a file onto your server because the client says it’s an image is like giving a stranger the keys to your house because he says he’s won’t steal anything. Here’s a quick example of a script that could be used to exploit such a vulnerability:

// The destination for our attack:
$host = "127.0.0.1";
$port = 8887;
$page = "/server.php";

// Here we have the file we're uploading (note the content-type):
$payload =
"------ThisIsABoundary
Content-Disposition: form-data; name=\"file\"; filename=\"evil.php\"
Content-Type: image/jpeg

<?php phpinfo();
------ThisIsABoundary--";

// Finally, craft the request and send it.
$content_length = strlen($payload);
$headers = array(
    "POST {$page} HTTP/1.1",
    "Host: {$host}:{$port}",
    "Connection: close",
    "Content-Length: {$content_length}",
    "User-Agent: Evil Robot",
    "Content-Type: multipart/form-data; boundary=----ThisIsABoundary",
);

$request = implode("\r\n", $headers) . "\r\n\r\n" . $payload . "\r\n";

$fp = fsockopen($host, $port, $errno, $errstr)
      or die("ERROR: $errno - $errstr");
fwrite($fp, $request);

The above script crafts a standard HTTP request that uploads a php file named evil.php. If the server relies on $_FILES["file"]["type"]to validate uploads, then it’ll be under the mistaken impression that we’re sending it an image.

Part II: The mod_mime Apache Module and Multiple File Extensions

So what’s the solution, then? Some people use file extension checks, since servers will determine appropriate handlers and content types base on the extension of the file. Something like this will work most of the time:

$valid_file_extensions = array(".jpg", ".jpeg", ".gif", ".png");

$file_extension = strrchr($_FILES["file"]["name"], ".");

// Check that the uploaded file is actually an image
// and move it to the right folder if is.
if (in_array($file_extension, $valid_file_extensions)) {
    $destination = "uploads/" . $_FILES["file"]["name"];
    move_uploaded_file($_FILES["file"]["tmp_name"], $destination);
}

You might be safe with this, depending on your server settings. See, Apache can be configured to interpret multiple file extensions for the same file. While it might be useful for allowing a filename to determine both language and content type at once, it also presents a security vulnerability to developers who are unaware of this feature.

Exploiting the multiple file extension vulnerability doesn’t take much skill. Grab any PHP file, add .jpg to the end of its name, then upload it to the vulnerable server. Then visit the file in your web browser. This will cause Apache to run the script and output the results. Piece of cake.

Part III: The Script Disguised as an Image

People who are wary of  falsified MIME types and extra file extensions often advocate the use of something like getimagesize() to ensure that the uploaded file is actually an image.

if (@getimagesize($_FILES["file"]["tmp_name"]) !== false) {
    $destination = "uploads/" . $_FILES["file"]["name"];
    move_uploaded_file($_FILES["file"]["tmp_name"], $destination);
}

Surely, an image can’t be harmful? I mean, look at this kitten: That kitten could never hurt anyone, right? Just count yourself lucky that it’s a white hat kitten.

Now click on the kitten-link  (it opens in a new tab) and see what happens. What you should see is the exact same kitten, but this time, I’m running it as a PHP script. To accomplish this, I took the wonderful jhead tool, and I embedded a comment inside the original kitten image. My comment looked something like this:

<?php blahblahblah(); __halt_compiler();

The __halt_compiler()function call is there so that the image data doesn’t accidentally get interpreted as PHP and throw a parse error. This is why the output stops before outputting the actual image data. If you want to see exactly what I wrote, you can download the original kitten image (right-click, Save Image As…) and open it in your favorite text editor.

Part IV: The End

The above security checks certainly aren’t useless. If you’re expecting an image to be uploaded, then it’s nice to check and see if it’s a valid image. Having many layers of security is always a good thing. But what do we do about the PHP scripts that seem to keep sneaking by our protection?

Our goal here is not only to ensure that the file uploaded is an image, but also to ensure that the server doesn’t run any script handlers. My favorite way to do this is using Apache’s ForceType directive:

ForceType application/octet-stream
<FilesMatch "(?i)\.jpe?g$">
    ForceType image/jpeg
</FilesMatch>
<FilesMatch "(?i)\.gif$">
    ForceType image/gif
</FilesMatch>
<FilesMatch "(?i)\.png$">
    ForceType image/png
</FilesMatch>

This code, placed in the .htaccess file in your upload directory will only allow images to be associated with their default handlers. Everything else will be served as a plain byte stream and no handlers will be run.

I like this more that the “turn PHP off” solution (php_flag engine off), because it turns all script handlers off at once, in case your server also serves perl, python or whatever. Of course, you can always do both just to be on the safe side.

An even better solution is to place the files outside of the web directory, so that they will never be served at all. Then you need to write a script that takes a request for the file, retrieves the appropriate file from the filesystem, and outputs it with the correct headers. Of course, outputting files based on user input comes with its own set of security vulnerabilities, but that’s a story for another day.

Last but not least, always be sure to rename uploaded files. Choosing a random name makes it that much harder for an attacker to fool you, and it ensures that nobody overwrites your .htaccess or .user.ini files, neither of which be a good thing.

There are many resources out there on the web that have a lot to say about security. If you’re interested in reading more, check out OWASP and more specifically the OWASP Cheat Sheet Page.

You can follow any responses to this entry through the RSS 2.0 You can leave a response, or trackback.

25 Responses

  • Mat Landers says:

    Hey, this is the first article i’ve read on this site. Fantastic! I will certainly be using this to implement file uploads properly on my website.

    Cheers,
    Mat

  • Rusty says:

    So the moral of the story is don’t rely totally on PHP.

  • Anonymous says:

    Very good article!!! :)

    ;)

  • My favorite method for handling non-image files involves:
    1. Random file names
    2. Files stored outside of the webroot, and
    3. AES-256-CBC encryption (part of the key is pseudorandomly generated and stored in the database, the rest is part of the site configuration).

    Even if they can read the file and know what its name is, it’s encrypted so good luck getting anything to execute.

  • leOm says:

    Hi there!

    I’d love to use your protection method but there is a problem when I use this in my .htaccess:


    ForceType application/octet-stream

    ForceType image/jpeg

    ForceType image/gif

    ForceType image/png

    If I put the code into .htaccess I get a “500 Internal Server Error” when I try to open a legit .jpg file.

    It should open the file if it is a legit image, right?

    Do you have a clue what I could do?

    Thanks a lot!

  • bluelightzero says:

    This is just an idea, but to be on the safe side the php script could make a clean image file based on the upload.

    make a new image, draw teh uploaded image onto that ne image save the new image.

    Would this clear any extra headers/ injections? as the new image should only be given pure pixel data.

  • Josh says:

    I’m building an imgur.com type site for myself and IRC buddies and found your article. Lots of really good points, many of which I was completely unaware of. The information you’ve provided has shown itself to be incredibly invaluable especially for what I’m working on. Bookmark! :)

  • Roy M J says:

    Very nice article.. Ive been searching for image uploading scripts and everything that i got was direct uploading ones which do not have any check whatsoever or gives insight into the issues that are associated with this. Extremely useful article. Cheers

  • KappiCraig says:

    Superb article,

    Thanks for the informative content and heads up on dodgy files. I can see myself using this.

  • Anonymous says:

    vvvvvvvvvvvvvvvvv

  • What’s up, just wanted to say, I loved this
    article. It was practical. Keep on posting!

  • Heya i’m for the primary time here. I came across this board and I find It truly useful & it helped me out a lot.
    I am hoping to present one thing again and aid others
    like you helped me.

    Also visit my page :: amway reviews

  • I got this website from my friend who shared with me regarding this
    webb page and at the moment this time I am browsing this website and reading very
    informative content here.

  • Hey I know this is off topic but I was wondering if you knew of any widgets I could add to my blog that automatically tweet my newest twitter updates.
    I’ve been looking for a plug-in like this for quite some time and was
    hoping maybe you would have some experience with something like this.

    Please let me know if you run into anything.

    I truly enjoy reading your blog and I look forward to your new updates.

    Feel free to visit my web page … Roof Replacement Arlington

  • I believe that is among the such a lot significant information
    for me. And i’m satisfied reading your article. But want to commentary on some basic issues, The site style
    is perfect, the articles is in reality great : D. Good process, cheers

    Here is my homepage :: clash of clans triche

  • For most recent news you have to pay a quick visit
    web and on the web I found this web page as a most excellent
    web site for most recent updates.

  • Jeff says:

    I’ve seen one technique that the author claims to be the most secure way of dealing with images. The author converts every uploaded image to a .PNG image type using the imagecreatefromstring PHP function. He claims that any non-image will fail conversion and PNG files cannot have embedded code (such as in your kitten example) or meta tags. Is this actually correct to your knowledge, and can you see any further security issues with this technique? I’d assume if one did not want PNG formats, they would be able to safely convert the file back to JPG, GIF, etc.

  • Velma says:

    I read a lot of interesting posts here. Probably you
    spend a lot of time writing, i know how to save you a lot of work, there is an online tool
    that creates readable, google friendly articles in minutes, just type in google – laranitas free content source

  • When you’ve got much more and a lot more scouring the web specifics about whatsapp
    hacken higher dallas location, it’s not whatsapp hacken whatsapp hacken constantly surprising numerous
    new enterprise organisations are utilising whatsapp hacken the greater
    dallas area to assist you to surge the whatsapp hacken greater dallas area online organization more.

    Maar om iemands whatsapp te hacken heb je wel de Whatsapp hack nodig
    die ik ook heb.

  • ravi says:

    hi this is very good information. i am new to PHP apart from the above security options you can also use getimagesize() it will only accept images and while uploading the image rename it by using mt_rand (100000,99999), if the executable file is evil.jpg.php uploaded to your server mt_rand will change the file name ex: 123456. so the fill will not execute. i am not very good in php but it may be useful to sombody. thank you.

  • Nick says:

    Very nice post. I simply stumbled upon your weblog and wished too mention that I have really enjoyed surfing around your blog posts.
    In any case I will be subscribing on your feed and I’m hoping you write once
    more very soon!

    My web blokg :: Home (Nick)

  • Fernando says:

    Wite tiles are an excellent choice for roofing repaairs and replacements.
    Paul roofing contractor can get you more information about the large selection of siding options to choose from.
    After you have invenfed the final figures, make sure that your present andd future business prospects will not find it tooo diffjcult to
    service the home renovation loan.

  • Pingback: dua xe audi q7

  • Pingback: xe audi q5 cu



Leave a Reply

Your email address will not be published.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>