【问题标题】:the most reliable way to check upload file is an image检查上传文件的最可靠方法是图像
【发布时间】:2015-02-21 06:26:40
【问题描述】:

我想验证我的上传文件是否为图像。经过搜索,我发现了两种我认为是一种好方法的方法。第一个代码是:

$whitelist_type = array('image/jpeg', 'image/png','image/gif');
$fileinfo = finfo_open(FILEINFO_MIME_TYPE);

if (!in_array(finfo_file($fileinfo, $file['tmp_name']), $whitelist_type)) {
$error[]  = "Uploaded file is not a valid image";
}

和第二个代码:

if (!getimagesize($_FILES['photo']['tmp_name'])) {
$error[]  = "Uploaded file is not a valid image";
}

哪个代码更可靠地检查它是图像,为什么?还是比这更好的方法?谢谢。

【问题讨论】:

    标签: php image file-upload mime-types file-type


    【解决方案1】:

    我使用的最快方法是自定义 PHP 函数,它从文件中读取特定字节。当检查文件非常大(电影、iso 图像等)时,它的运行速度比getimagesize 快得多。

    fastImageGet('image.jpg');         // returns size and image type in array or false if not image
    fastImageGet('image.jpg', 'type'); // returns image type only
    fastImageGet('image.jpg', 'size'); // returns image size only
    
    function fastImageGet($file, $what=null) {
    
        if (!in_array($what, ['size', 'type']))
            $what = null;
    
        // INIT
    
        $pos = 0; $str = null;
    
        if (is_resource($file))
            $fp = $file;
    
        elseif (!@filesize($file))
            return false;
    
        else
            try {
                $fp = fopen($file, 'r', false);
            } catch (\Exception $e) {
                return false;
            }
    
    
        // HELPER FUNCTIONS
    
        $getChars = function($n) use (&$fp, &$pos, &$str) {
            $response = null;
    
            if (($pos + $n - 1) >= strlen($str)) {
                $end = $pos + $n;
    
                while ((strlen($str) < $end) && ($response !== false)) {
                    $need = $end - ftell($fp);
    
                    if (false !== ($response = fread($fp, $need)))
                        $str .= $response;
                    else
                        return false;
                }
            }
    
            $result = substr($str, $pos, $n);
            $pos += $n;
            return $result;
        };
    
        $getByte = function() use ($getChars) {
            $c = $getChars(1);
            $b = unpack('C', $c);
            return reset($b);
        };
    
        $readInt = function ($str) {
            $size = unpack('C*', $str);
            return ($size[1] << 8) + $size[2];
        };
    
    
        // GET TYPE
    
        $t2 = $getChars(2);
    
        if ($t2 === 'BM')
            $type = 'bmp';
        elseif ($t2 === 'GI')
            $type = 'gif';
        elseif ($t2 === chr(0xFF) . chr(0xd8))
            $type = 'jpeg';
        elseif ($t2 === chr(0x89) . 'P')
            $type = 'png';
        else
            $type = false;
    
        if (($type === false) || ($what === 'type')) {
            fclose($fp);
            return $type;
        }
    
    
        // GET SIZE
    
        $pos = 0;
    
        if ($type === 'bmp') {
            $chars = $getChars(29);
            $chars = substr($chars, 14, 14);
            $ctype = unpack('C', $chars);
            $size = (reset($ctype) == 40)
                ? unpack('L*', substr($chars, 4))
                : unpack('L*', substr($chars, 4, 8));
    
        } elseif ($type === 'gif') {
            $chars = $getChars(11);
            $size = unpack('S*', substr($chars, 6, 4));
    
        } elseif ($type === 'jpeg') {
            $state = null;
    
            while (true) {
    
                switch ($state) {
    
                    default:
                        $getChars(2);
                        $state = 'started';
                        break;
    
                    case 'started':
                        $b = $getByte();
                        if ($b === false) {
                            $size = false;
                            break 2;
                        }
                        $state = $b == 0xFF ? 'sof' : 'started';
                        break;
    
                    case 'sof':
                        $b = $getByte();
    
                        if (in_array($b, range(0xE0, 0xEF)))
                            $state = 'skipframe';
    
                        elseif (in_array($b, array_merge(range(0xC0, 0xC3), range(0xC5, 0xC7), range(0xC9, 0xCB), range(0xCD, 0xCF))))
                            $state = 'readsize';
    
                        elseif ($b == 0xFF)
                            $state = 'sof';
    
                        else
                            $state = 'skipframe';
    
                        break;
    
                    case 'skipframe':
                        $skip = $readInt($getChars(2)) - 2;
                        $state = 'doskip';
                        break;
    
                    case 'doskip':
                        $getChars($skip);
                        $state = 'started';
                        break;
    
                    case 'readsize':
                        $c = $getChars(7);
                        $size = [$readInt(substr($c, 5, 2)), $readInt(substr($c, 3, 2))];
                        break 2;
                }
            }
    
        } elseif ($type === 'png') {
            $chars = $getChars(25);
            $size = unpack('N*', substr($chars, 16, 8));
        }
    
    
        // COMPLETE
    
        fclose($fp);
    
        if (is_array($size))
            $size = array_values($size);
    
        return ($what === 'size') ? $size : [$type, $size];
    }
    

    【讨论】:

      【解决方案2】:

      从安全的角度来看,您可能最好将上传的文件假定转换为图像,看看它是否成功,并保留并提供转换后的结果在那里。

      根据您检测到的 MIME 类型,您可以使用 GD library 中的 imagecreatefrom...() 函数之一,例如来自$_FILES 数组,和/或来自exif_imagetype()finfo_file() 等。

      问题是存在一些伪装成有效图像的漏洞利用(在某些情况下有效图像),但也是有效的 JavaScript、Flash 或其他可以运行的代码容器在某些情况下由客户端的浏览器。

      参见例如https://www.defcon.org/images/defcon-15/dc15-presentations/dc-15-schrenk.pdf

      【讨论】:

      • 那么 imagecreatefrom 是否可以阻止似乎是有效图像的 php 或其他脚本并通过上述验证?
      • 我的意思是你应该重新创建图像并保存重新创建的图像而不是上传的文件,因为它可以删除上传中的恶意数据。重新创建还可能会删除 EXIF 数据,例如用户通常不想向其他人公开的地理位置(它会告诉所有人照片的拍摄地点。如果是在家里,人们就会知道您住在哪里)。
      【解决方案3】:

      为什么不使用exif_imagetype

      if (exif_imagetype($file['tmp_name']) != (IMAGETYPE_JPEG || IMAGETYPE_GIF || IMAGETYPE_PNG)) {
          $error[]  = "Uploaded file is not a valid image";
      }
      

      它可能会比其他任何一个都快。 (PHP 4 >= 4.3.0,PHP 5)

      【讨论】:

      • 它更可靠吗?我更喜欢选择安全性而不是性能,因为我将在小型项目中使用它。
      • 我认为它会被证明是最可靠的,因为它使用了 PHP 的内置函数;它还专门读取文件签名以验证类型。这并不是说文件 mime 类型不能被欺骗,但是,没有任何功能可以完全防止这种情况发生。如果您正确处理您的图像,尽管有人能够使用假图像作为攻击媒介的威胁不应该是一个值得关注的主要原因。
      • 也许我应该在我的其他验证旁边使用这个选项,以防万一。我读了一些文章,黑客仍然可以制作有效的文件类型并在其中包含一些代码,我很好奇@Archimedix 回答说我们应该使用 imagecreatefrom...() 转换图像我想知道它是否可以覆盖图像中的代码文件。
      • 如果您允许用户上传自己的文件,无论您使用何种方法确保它们是真正的图像,都无法真正阻止在图像中添加代码(有效与否) (甚至不是imagecreatefrom())。关键是要为它们上传到的文件/目录正确设置权限,并且正确设置 PHP 处理程序。恶意使用图像由来已久,并且有数百种使用方式。
      • This question 提供了一些关于您可能还想通读的常见漏洞的有用信息。
      【解决方案4】:

      finfo_* 库会很好,但它适用于 >= 5.3.0 版本,

      AND getimagesize() GD 库函数返回图像信息WxHsize

      如果图像无效,则getimagesize() 显示警告,因此最好使用finfo_* 函数来验证图像,

      你也可以做跨版本代码,见下面的示例代码

      <?php 
      $file = $_FILES['photo'];
      $whitelist_type = array('image/jpeg', 'image/png','image/gif');
      $error = null;
      if(function_exists('finfo_open')){    //(PHP >= 5.3.0, PECL fileinfo >= 0.1.0)
         $fileinfo = finfo_open(FILEINFO_MIME_TYPE);
      
          if (!in_array(finfo_file($fileinfo, $file['tmp_name']), $whitelist_type)) {
            $error[]  = "Uploaded file is not a valid image";
          }
      }else if(function_exists('mime_content_type')){  //supported (PHP 4 >= 4.3.0, PHP 5)
          if (!in_array(mime_content_type($file['tmp_name']), $whitelist_type)) {
            $error[]  = "Uploaded file is not a valid image";
          }
      }else{
         if (!@getimagesize($file['tmp_name'])) {  //@ - for hide warning when image not valid
            $error[]  = "Uploaded file is not a valid image";
         }
      }
      

      【讨论】:

      • 你能解释一下getimagesize前面的!@有什么用吗?谢谢
      • @ - 当图像无效时隐藏编译器警告
      • }else{ case 可能永远不会使用,但您可以添加安全方面的代码,
      • 不要盲目相信 getimagesize() 因为它可以被绕过。请参阅此处的文档:nullcandy.com/php-image-upload-security-how-not-to-do-it
      猜你喜欢
      • 1970-01-01
      • 2013-10-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-06-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多