如何使用PHP上传安全图片?

5

我知道这个问题有很多相关的提问,但是没有一个能涵盖我想要了解的所有内容。

我想让我的网页用户通过表单上传图片。我希望这个过程是安全的,或者至少尽可能安全。

我对深入了解安全方面并不太了解,但我知道一个不安全的网页会产生的后果。我不能安心地想着我的网页是不安全的,或者因为没有足够的访问量而没有人进入我的网页(我是现实主义者)。

在这一点上,我知道所有关于安全性的检查都必须在服务器端进行,而不是客户端(或两者都可以)。

我知道一个文件可以被欺骗成图像并运行恶意代码,所以我搜索了一些方法来避免这种情况。这是我在将图像存储到服务器之前找到的检查方法:

$_FILES中:

  • $_FILES['file']['name']:检查我上传的文件是否有名称。以此来知道该文件是否存在。
  • $_FILES['file']['error']:检查图像是否有错误。
  • $_FILES['file']['size']:检查图像的大小是否大于0。
  • $_FILES['file']['type']:检查文件类型是否为图像,但不建议使用,因为PHP不会对其进行检查。

通用函数:

但我遇到的问题是我不知道是否应该使用所有这些方法,还是避免或添加其中一些方法(因为其中一些似乎是多余的)。

我的问题是:

  • 我应该使用所有这些方法吗?
  • 我是否需要添加另一种方法以更安全?
  • 过滤文件的顺序是否关键?我的意思是,使用一个过滤器比另一个更好,反之亦然吗?如果是这样的话,应该是什么顺序,为什么?

注意:我不是在寻找个人意见。我尝试收集了我可以得到的所有信息,但是在安全方面谈论其是否正确或不正确我不能确定。如果您可以提供被遗漏的内容的示例,那将是很棒的。

提前致谢!


我正在使用Fileinfo扩展来获取MIME类型并将其与白名单进行检查。如果一切正常,我会重新创建图像以删除任何不必要的元数据(我使用Imagick,但它也适用于GD(直到特定限制)。 - Charlotte Dunois
很高兴我能分享一些技巧。我有一个模块化的清洁剂/消毒剂/验证器类系统/例程,用于保护用户输入,但我从未实现我的FileSanitizer和FileValidator类。但是,你的问题和我的回答涵盖了我打算做的基础知识。 - Anthony Rutledge
@CharlotteDunois 谢谢您的评论!我对您在评论中提到的内容很感兴趣,因为我以前没有听说过。您所说的“Fileinfo”扩展和MIME是什么意思?我搜索了一下 MIME,我的理解是它是为电子邮件传输而创建的,但现在它用于所有网络传输,并设置一个带有正在传输的文件类型的标头,对吗?白名单可以是一个简单的数组,在其中存储不同的允许类型,并将其与 MIME 类型进行比较检查,对吗? - Francisco Romero
3个回答

2
回答你的问题:
1. 你不需要使用所有这些方法,你使用哪些方法将基于个人意见。也就是说,有不止一种完全安全的方法来做到这一点,所以如果你得到多个不同的答案,不要感到惊讶。
2. 请参见下面的示例,了解您可能遗漏的其他检查。
3. 是的,顺序绝对很重要。
根据您的应用程序,任何安全上传的逻辑应该如下所示:
用户是否已登录?(可选)
// make sure user is logged in
if (!$user->loggedIn()) {
    // redirect
}

用户是否具有权限?(可选)
// make sure user has permission
if (!$user->isAllowed()) {
    // redirect
}

表单已提交吗?

// make sure form was submitted
if ($_SERVER['REQUEST_METHOD'] == 'POST') {

表单输入是否有效?
// validate CSRF token
// ...

// make sure there were no form errors
if ($_FILES['file']['error'] == UPLOAD_ERR_OK) {

// make sure the file size is good
if ($_FILES['file']['size'] <= MAX_FILE_UPLOAD) {

// make sure we have a valid image type
$type = exif_imagetype($_FILES['file']['tmp_name']);
if ($type !== false) {

// make sure we check the type against a whitelist
if (in_array(ltrim(image_type_to_extension($type), '.'), array('jpeg', 'jpg', 'png'))) {

即使经过验证,也不要相信用户输入的内容。
// give the file a unique name
$hash = hash_file('sha1', $_FILES['file']['tmp_name']);
$ext = image_type_to_extension($type);
$fname = $hash . $ext;

保存文件(或选择使用库重新创建文件以剥离元数据),但绝不要保存在公共可访问的目录中

$upload_path = '/path/to/private/folder';
move_uploaded_file($_FILES['file']['tmp_name'], "$upload_path/$fname");

以上步骤是完全安全和合理的,当然,您的应用程序或服务器的其他部分可能存在漏洞风险。


@Error404,哈希值只是给文件一个唯一名称的示例。如果你最终得到了两个相同的哈希值,那么可以安全地假设这两张图片是重复的。 - mister martin
那么我猜想,在创建了这个哈希之后,我需要与文件夹内的文件进行比较,对吗? - Francisco Romero
还有一个问题:如何知道我必须遵循的顺序是什么?我猜在你的情况下,你按顺序安排了步骤,但是一般来说,应该先做什么,后做什么呢?再次感谢! - Francisco Romero
@Error404是因为$type是由exif_imagetype返回的整数,而不是实际的扩展名。image_type_to_extension函数可以找到实际的扩展名。 - mister martin
按照顺序进行,在逻辑上合理的情况下进行操作。例如,在确认表单已提交或无误之前检查文件大小是错误的操作。 - mister martin
显示剩余4条评论

1
如果你得到足够的回复,你可能会得到一个好答案!:-)

操作系统

确保你有一个专门用于文件的卷。或者,至少在目录上设置配额。如果是Linux / Unix,请确保你有足够的inode等。一堆小文件和几个巨大的文件一样致命。有一个专门的上传目录。在php.ini中设置临时文件应该去哪里。也要确保你的文件权限是安全的(chmod)。必要时使用Linux ACLs来微调权限。测试,测试,测试。

PHP

将此处找到的知识合并到你的上传文件处理算法中PHP手册:POST方法上传。对MAX_FILE_SIZE这一点要保持谨慎。

  1. 确保您知道您的最大上传文件大小。相应地设置它。可能会有其他与文件相关的设置。在处理 $_FILES 超全局变量之前,请确保锁定这些设置。

  2. 不要直接使用上传的文件,并且根本不使用 name 属性来为文件命名。适当使用 is_uploaded_file()move_uploaded_file()

  3. 适当使用 tmp_name

  4. 注意文件名中的空字节!是的,您仍然需要过滤验证任何表示用户输入的字符串(特别是如果您打算以任何方式使用它)。

  5. 首先,检查文件是否存在。

  6. 其次,检查文件大小(以字节为单位)。
如果第5或第6步失败,验证过程应该结束。为了实现强大的例程,应考虑在某个时候您可能希望一次上传多个文件(PHP手册:上传多个文件)。在这种情况下,$_FILES超全局变量可能看起来并非您所期望。有关更多详细信息,请参见上面的链接。
GD
您已经了解如何使用这些函数打开提交的文件(不使用用户提交的名称)。只需想出一系列逻辑连续步骤即可。我没有这些步骤,但是如果元数据可能是一个问题,那么在基本文件存在和大小方面之后尝试GD的高级操作似乎很重要。不过我也可能错了。

非常感谢您的回答!我有一些问题:1. 当您说“在php.ini中设置临时文件应该放在哪里”时,您是指更改默认的tmp_file,对吗? 2. 当您说“适当使用is_uploaded_file()和move_uploaded_file()。”时,您的“适当使用”是什么意思?我应该怎么做? 3. 您所说的GD是什么意思?提前致谢! - Francisco Romero
  1. 是的,在php.ini中搜索,你会找到上传文件应该放在哪里的设置。
  2. 你必须阅读手册。这是我能给出的最好建议。只是不要使用用户提供的文件名。或者,彻底过滤和验证它。
  3. GD序列并非一成不变。你需要进行研究。同样,我没有那些步骤。Niel的答案是一个GD序列,但我不能证明它的完整性。
- Anthony Rutledge
真糟糕,我的 Stack Overflow 账户被禁言了。自从我拥有大约400分以来,我就没办法再提出问题了!但是,只要我能帮得上忙,我很乐意提供帮助。他们告诉我如果我获得足够的积分,我就可以走出这个罚款箱了。 - Anthony Rutledge
再次感谢您的回复!当我提到“GD”时,我想问的是字母“G”和“D”在互联网上的搜索意义。至于“penalty box”,我从未听说过。 - Francisco Romero

1
我会在明显的图像上传中采取以下步骤: 1)使用is_uploaded_file()确保您没有被欺骗去处理完全不同的东西
if(!is_uploaded_file($yourfile))
     return false;

2) 使用exif_imagetype()检查MIME类型,并阻止任何不想要的内容

   $allowed_images = array(IMAGETYPE_BMP, IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_PNG);

    $uType = exif_imagetype($yourfile);
    if(!in_array($uType, $allowed_images))
    {
        unlink($yourfile);
        return false;
    }

3) 使用Imagick重新制作图片并删除所有注释和元数据:

    $image = new Imagick($yourfile);
    $image->resizeImage($image->getImageWidth(), $image->getImageHeight(), Imagick::FILTER_CATROM, 1);
    $image->stripImage();         // remove all comments and similar metadata

4) 将替换图像写入文件系统并删除原始文件:

$image->writeImage("/path/to/new/image");
unlink($yourfile);

将此图像上传到S3。
 // your S3 code here

6) 在数据库或其他地方记录图像的S3 URL。

 // your database code here

7) 删除替换图像。

 unlink("/path/to/new/image");

我需要学习“文件图像”算法部分,以便为我的Cleaner / Filter / Validator类提供支持。我已经在其他地方看到了类似的步骤。我一定会记住你的答案。--安东尼 - Anthony Rutledge
非常感谢您的回答!太好了!我有一些问题:1. 我已经搜索了关于 MIME 类型的信息,我的理解是它是所有 Web 交易中包含发送文件类型的头部,对吗? 2. 当你说 S3 的时候,你是什么意思? 3. 您没有使用我在问题中提到的大多数方法。我应该使用它们吗?还是应该避免使用它们?提前致谢! - Francisco Romero
  1. 我检查MIME类型以减少将垃圾数据输入ImageMagick的机会。我想上传一些带有正确MIME类型标头的恶意内容是可能的,但重新制作没有元数据和注释的图像应该可以防止这种情况发生。
  2. 我应该更好地解释一下。我打算将所有静态资源(如图像)存储在不是我的Web服务器的平台上。我喜欢使用AWS S3服务。这有两个好处:首先,我的Web服务器只需要担心动态文件,其次,如果上传是恶意的,则移动到无法执行它的平台。
- Neil Mukerji
我认为通过使用Imagick重新创建图像,您建议的额外检查变得多余了,但是测试这些也不会有害(除了性能)。希望这可以帮到您! - Neil Mukerji
谢谢你!你的信息非常有帮助,但如果你能在你的回答中添加关于S3的信息就更好了。我认为在那里突出显示这些信息会很有帮助。谢谢 :) - Francisco Romero

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接