Safe Image File Upload
Hello all!
With the great feedback from our kind Master (hehe) I wanted to chip in too and give some pointers that developers should take into account
when working on an upload system. This post is aimed to image files.
A normal implementation of an image file upload could look like this:
<form name="myForm" action="imageupload1.php" method="post" enctype="multipart/form-data"> Choose your image: <input type="file" name="myimage"> <br> <input type="submit">
Thats ok, thats probably standard by now, some developers like to put another input hidden with certain values like the username or the maximum upload size, thats extra and will not be covered here.
What we will talk about here is the process that happens in the server side and how we can make it more secure, we will look at some of the risks and how to prevent them.
First and foremost, we have to realize that an attacker may not use that form above, in other words they do not have to use an html page to use a form:
PHP’s CURL
CURL in php is an implemented library that allows a php program to connect to almost any server using almost any protocol (http, https, ftp, http post…). The power it embraces allows an attacker to use it to submit a form without even having a graphical interface.
PERL
Perl is another programming language, widely used in the dark side for attacks to innocent sites. For more information look for libwwwperl which is a sort of curl for perl.
enough hints, there are more ways but the important thing is that you understand that they do not need to visit your website to attack it, they just need to have one look at the form to know what to attack.
So, having that html snippet above a naive implementation of imageupload1.php could look like:
<?php
$uploaddir = "uploadfolder/";
$filename = basename($_FILES["myimage"]["name"]);
if( image_upload_file($_FILES["myimage"]["tmp_name"], $uploaddir.$filename))
{
echo "file uploaded";
}
else
{
echo "file was not uploaded";
}
?>
This is the most basic implementation of an image upload script, as you can see there is no validation done. But how can an attacker profit from this lack of defense? Too easy! they can upload the following php script for example:
<?php system($_GET["command"]); ?>
The system command allows a command to be executed directly by the php engine, so with this simple script an attacker could issue any command they want as part of the url:
http://mysite.com/imageupload1.php?command=mycommand
How to protect from this:
One way we can think of is using the file type for verification:
if($_FILES["myimage"]["type"] != "image/gif" )
{
echo "no can do";
exit;
}
$uploaddir = "uploadfolder/";
$filename = basename($_FILES["myimage"]["name"]);
if( image_upload_file($_FILES["myimage"]["tmp_name"], $uploaddir.$filename))
{
echo "file uploaded";
}
else
{
echo "file was not uploaded";
}
We have added a check for the file type there but im afraid this can also be overridden, here is perl script that would do:
#!/usr/bin/perl # use LWP; use HTTP::Request::Common; $ua = LWP::UserAgent->new; $res = $ua->request(POST "http://mysite.com/imageuplaod1.php", Content_Type => "form-data", Content => [myimage => "evilscript.php", "evilscript.php", Content-Type => "image/gif"], ], ); print $res->as_string();
Unfortunately that piece of perl code would change the mime type identified by the server side script and would validate ergo it didnt work so well.
So, instead of trusting the client side to provide correct information we need to validate on the server side. Php has a very nice function that we can use here, its called getimagesize(), it doesnt only get the image size as one may think, it also gathers many other informative details about an image file. Note: Ive tried it on non image files and it simply doesnt work
$imageInfo = getimagesize($_FILES["myimage"]["tmp_name"); // note that we need to use the temporal name since it has not yet been moved
if($imageInfo["mime"] != "image/gif")
{
echo "we only accept gif images";
exit;
}
// ... and the normal uploading code
Now even if the bad guys try changing the mime type, our script will check if it really is an image file so we’re one step closer to a “secure” system (may I remind you that there is no perfect security system).
One could think “alright!! this now is safe enough”, however there is still one more tip to point out here, and its based on one feature of a powerful image editor, gimp. There are other image editors that may be capable of doing this too but I’ve only worked with gimp so thats what I can talk about, gimp allows an image file have comments, and those comments can also be php code, so an image file may result in an image file AND a php script, pretty scary huh? well here is a screenshot I took to show you how it may look:
So as scary as it may be, it can happen :/
So how on God’s name can we stop this? one approach could be to convert the image on the server side, if we only allow gif images then we can create a new image as pgn and in the process it may lose its comments, Ive only heard of this to work so cant really account for it.
Another approach would be to check for the file extension, give n the file name we can check its extension and have a black list of php like files to avoid, if you notice the image above the file name is a php file, which got uploaded using all the security tips here provided, but the browser interprets it given its file extension.
So, the last line of defense is commonly named as Indirect File Access, make no mistake, you should still implement all the security features you can.
This approach tells us that we should only allow access via a php script, so our users cant fetch it directly, this approach teaches that after all the validations have passed we have to assign a new name ot the uploading file and store the real name in a database, so there is a conversion in the process:
User:Requests Image -> Script:Gets request -> Script:Reads Database -> Script: Looks for file -> User:Gets image
so a simple:
header("Content-Type: " . $imageFile["mime_type"]);
readfile($uploadDir.$imageFile["name"]);
would suffice to grant a rather safe way to present an image to the user.
Last safety tip is, since you are now using a script to provide indirect access to the image files you should store the images outside the public folder, if your public folder is /home/myaccount/public_html/ then store it in /home/myaccount/images/ since a normal user shouldnt have access to that folder.
Conclusion
There are many ways to allow image uploads but most are not secure enough to a determined attacker. With the techniques here offered we can at the very least make it very hard for a knowledgable attacker to actually take advantage of our sites.
There still are other aspects to consider, DoS, Performance… but granted a rather safe system we can at least be sure we’re doing all we can to avoid any problems.
A final tip: backup often, I can never stress this enough, before you implement any technique backup, test and if it works backup again!



Ummm… Is this V2 related or just a general discussion of programing techniques? Maybe we can have a “PHPFox School” category and move it there?
Just an idea as I found your post very useful but unrelated to the development of V2.
Yes, I like the school idea and considering you have some good tutorials I think this would be awsome to add this cat and move this there and continue teaching us oh great one
[Reply]
Great Tut. Purefan. I added a new “PHP Tutorial” category and placed it there.
Keep em coming!
[Reply]
Great tutorial, I neva thought uploading image file could be this scary. I will take this into account the next time I think about writing a file upload script. Keep up the good tutorials.
[Reply]
Nice,
until now I just checked the mime-type and the file extension and changed the filename. I was wondering, is there a similar way for movie type files? Is there an equivalent of getimagesize for mpeg/avi?
[Reply]
Maybe I missed something, but there were a few code errors above. Here’s modified code from above that seems to be working. I hope this helps and doesn’t defeat the purpose of the code above:
[Reply]
Maybe I missed something, but there were a few code errors above. Here’s modified code from above that seems to be working. I hope this helps and doesn’t defeat the purpose of the code above:
%3c%3fphp++%0d%0a%09%24imageInfo+%3d+getimagesize(%24_FILES%5b%22myimage%22%5d%5b%22tmp_name%22%5d)%3b+%2f%2f+note+that+we+need+to+use+the+temporal+name+since+it+has+not+yet+been+moved++%0d%0a%09if(%24imageInfo%5b%22mime%22%5d+!%3d+%22image%2fgif%22)++%0d%0a%09%7b++%0d%0a%09echo+%22we+only+accept+gif+images%22%3b++%0d%0a%09exit%3b++%0d%0a%09%7d++%0d%0a+++%0d%0a+++if(%24_FILES%5b%22fileName%22%5d%5b%22type%22%5d+!%3d+%22image%2fgif%22+)++%0d%0a+++%7b++%0d%0a+++echo+%22no+can+do%22%3b++%0d%0a+++exit%3b++%0d%0a+++%7d++%0d%0a+++++%0d%0a+++%24uploaddir+%3d+%22uploadfolder%2f%22%3b++%0d%0a+++%24filename+%3d+basename(%24_FILES%5b%22myimage%22%5d%5b%22name%22%5d)%3b++%0d%0a+++if(+move_uploaded_file(%24_FILES%5b%22myimage%22%5d%5b%22tmp_name%22%5d%2c+%24uploaddir.%24filename))++%0d%0a+++%7b++%0d%0a+++echo+%22file+uploaded%22%3b++%0d%0a+++%7d++%0d%0a+++else++%0d%0a+++%7b++%0d%0a+++echo+%22file+was+not+uploaded%22%3b++%0d%0a+++%7d++%0d%0a+++++%0d%0a+++%3f%3e
[Reply]
Ugh, nevermind. I guess you can’t copy code examples on here.
In gist of what I did, I changed image_upload_file to move_uploaded_file.
[Reply]