28

I'd like to check if an uploaded file is an image file (e.g png, jpg, jpeg, gif, bmp) or another file. The problem is that I'm using Uploadify to upload the files, which changes the mime type and gives a 'text/octal' or something as the mime type, no matter which file type you upload.

Is there a way to check if the uploaded file is an image apart from checking the file extension using PHP?

Kibbee
  • 65,369
  • 27
  • 142
  • 182
Ali
  • 261,656
  • 265
  • 575
  • 769

7 Answers7

42

My thought about the subject is simple: all uploaded images are evil.

And not only because they can contain malicious codes, but particularly because of meta-tags. I'm aware about crawlers that browse the web to find some protected images using their hidden meta-tags, and then play with their copyright. Perhaps a bit paranoid, but as user-uploaded images are out of control over copyright issues, I take it seriousely into account.

To get rid of those issues, I systematically convert all uploaded images to png using gd. This have a lot of advantages: image is clean from eventual malicious codes and meta tags, I only have one format for all uploaded images, I can adjust the image size to fit with my standard, and... I immediately know if the image is valid or not! If the image can't be opened for conversion (using imagecreatefromstring which doesn't care about image format), then I consider the image as invalid.

A simple implementation could look like this:

function imageUploaded($source, $target)
{
   // check for image size (see @DaveRandom's comment)
   $size = getimagesize($source);
   if ($size === false) {
      throw new Exception("{$source}: Invalid image.");
   }
   if ($size[0] > 2000 || $size[1] > 2000) {
      throw new Exception("{$source}: Too large.");
   }

   // loads it and convert it to png
   $sourceImg = @imagecreatefromstring(@file_get_contents($source));
   if ($sourceImg === false) {
      throw new Exception("{$source}: Invalid image.");
   }
   $width = imagesx($sourceImg);
   $height = imagesy($sourceImg);
   $targetImg = imagecreatetruecolor($width, $height);
   imagecopy($targetImg, $sourceImg, 0, 0, 0, 0, $width, $height);
   imagedestroy($sourceImg);
   imagepng($targetImg, $target);
   imagedestroy($targetImg);
}

To test it:

header('Content-type: image/png');
imageUploaded('http://www.dogsdata.com/wp-content/uploads/2012/03/Companion-Yellow-dog.jpg', 'php://output');

This does not exactly answer your question as this is the same kind of hack than the accepted answer, but I give you my reasons to use it, at least :-)

Alain Tiemblo
  • 36,099
  • 17
  • 121
  • 153
  • I totally agree with @Alain Tiemblo "I systematically convert all uploaded images to png using gd". This is the way to go about security. – PauloASilva Aug 13 '14 at 10:13
  • Indeed, one of my colleague pointed out that this does not work if you're allowing animated gifs. True. – Alain Tiemblo Jan 21 '15 at 13:32
  • I agree with you BUT... Everyone knows that GIFs can be exploited ([example](http://www.phpclasses.org/blog/post/67-PHP-security-exploit-with-GIF-images.html)). If you're required to support GIFs maybe you'll have to invest some time and effort to filter/sanitize the format. – PauloASilva Jan 22 '15 at 16:19
  • 4
    Using `imagecreatefromstring()` directly on arbitrary data is also dangerous, because it's still possible for a DoS-style attack to fill the server's memory with only a few requests. Rather than jumping in an creating a whole image resource (4 bytes per pixel, an RGBa bitmap) from an arbitrary data string, you should first use `getimagesize()` as mentioned in the accepted answer to make sure that the image is in a readable format and the dimensions are sane. – DaveRandom Mar 21 '16 at 10:31
33

You could use getimagesize() which returns zeros for size on non-images.

mauris
  • 42,982
  • 15
  • 99
  • 131
Scott C Wilson
  • 19,102
  • 10
  • 61
  • 83
  • 2
    As far as I know, this is actually the accepted hack to get this done. I'd be interested in hearing better methods if they exist. – Wesley Murch Jun 26 '11 at 13:45
  • The docs say it returns an array with 7 elements, which element do I need to check to see if its an image or not? – Ali Jun 26 '11 at 13:51
  • zero and one for width and height. – Scott C Wilson Jun 26 '11 at 13:53
  • Seems like it just returns false if its not an image. I'm accepting this though i'd still like to know if there are any other options. – Ali Jun 26 '11 at 14:11
  • 2
    `exif_imagetype()` is much faster apparently. – rybo111 Nov 10 '14 at 21:06
  • It will return false if the image type is in mascules – BorisD Jun 04 '16 at 15:24
  • fileinfo() is the best option for validating files upload. using getimagesize() is not the best as the hacker can embed executable php script into an image via image glimp which will then corrupt the file header and all you will hear is boom. – chinazaike Oct 08 '18 at 16:55
5

If Uploadify really changes the mime type - i would consider it a bug. It doesn't make sense at all, because that blocks developers from working with mime-type based functions in PHP:

This is a little helper function which returns the mime-type based on the first 6 bytes of a file.

/**
 * Returns the image mime-type based on the first 6 bytes of a file
 * It defaults to "application/octet-stream".
 * It returns false, if problem with file or empty file.
 *
 * @param string $file 
 * @return string Mime-Type
 */
function isImage($file)
{
    $fh = fopen($file,'rb');
    if ($fh) { 
        $bytes = fread($fh, 6); // read 6 bytes
        fclose($fh);            // close file

        if ($bytes === false) { // bytes there?
            return false;
        }

        // ok, bytes there, lets compare....

        if (substr($bytes,0,3) == "\xff\xd8\xff") { 
            return 'image/jpeg';
        }
        if ($bytes == "\x89PNG\x0d\x0a") { 
            return 'image/png';
        }
        if ($bytes == "GIF87a" or $bytes == "GIF89a") { 
            return 'image/gif';
        }

        return 'application/octet-stream';
    }
    return false;
}
Jens A. Koch
  • 39,862
  • 13
  • 113
  • 141
5

You can verify the image type by checking for magic numbers at the beginning of the file.

For example: Every JPEG file begins with a "FF D8 FF E0" block.

Here is more info on magic numbers

rcode
  • 1,796
  • 18
  • 26
  • 6
    not safe imo, an attacker could "FF D8 FF E0" the first bytes, then add some php code that potentially could be executed – Reacen Aug 14 '13 at 03:17
  • 2
    Reacen, nothing is safe, PHP code can as well be injected into a valid JPG file by making use of the JPG metadata section. – rcode Aug 17 '13 at 06:00
3

Try using exif_imagetype to retrieve the actual type of the image. If the file is too small it will throw an error and if it can't find it it will return false

kaore
  • 1,288
  • 9
  • 14
3

You can check the first few bytes of the file for the magic number to figure out the image format.

Kibbee
  • 65,369
  • 27
  • 142
  • 182
2

Is it not possible to interrogate the file with finfo_file?

$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimetype = finfo_file($finfo, $filename); //should contain mime-type
finfo_close($finfo);

This answer is untested but based on this forum discussion on the Uploadify forums.

I would also point out that finfo should "try to guess the content type and encoding of a file by looking for certain magic byte sequences at specific positions within the file" so in my mind this should still work even though Uploadify has specified the wrong mime type.

JeffJenk
  • 2,575
  • 2
  • 21
  • 28