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(

// 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 = "";
$port = 8887;
$page = "/server.php";

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

<?php phpinfo();

// 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("rn", $headers) . "rnrn" . $payload . "rn";

$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 "(?i).gif$">
    ForceType image/gif
<FilesMatch "(?i).png$">
    ForceType image/png

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.

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


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


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

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

  • annonymous says:

    do you think this can inject JavaScript inside image, and while rendering image we can make browser to execute JavaScript ?

  • Bob says:

    With all this talk about images and security, why don’t you do something about all the comment spammers who have posted garbage comments? They compromise credibiity.

    • Hoffman says:

      Sorry about that. We hadn’t maintained this site in a few years and hadn’t noticed that spam comments started getting through. I removed them now. Thanks for the notice.

  • Vaughn says:

    I know this is a old post, but still a interesting read.

    I am currently resampling/renaming images, is there any standard/best practice for PDFs? I have looked high and low with out anything for safely storing uploaded PDFs.

  • lagwag0n says:

    Won’t this work too if you drop it in the folder where people will be uploading? create an .htaccess and put in:

    AddHandler cgi-script .php .pl .py .jsp .asp .htm .shtml .sh .cgi
    Options -ExecCGI

    This will block any scripts from running even if hidden in images.

Leave a Reply

Your email address will not be published.