本文转自nullcandy上Darwish与2012年6月29日的英文文章PHP Image Upload Security: How Not to Do It (仅做记录保存中文翻译内容)

让我们从谈论游戏中休息一下,以短暂的时间进入Web开发领域。我已经在PHP上做了很长时间工作,并且想解决安全文件上传的问题。
对于Web开发人员而言,文件上传是一件令人恐惧的事情。您允许完全陌生的人将他们想要的任何东西放到您宝贵的Web服务器上。

在本文中,我将完全处理图像的上传,以及如何确保用户提供给您的实际上是图像。


第一部分: 邪恶本恶 $_FILES[“file”][“type”]


很多次,我都看过(并且在我年轻的时候就写过)类似于以下内容的代码:

$valid_mime_types = array(
    "image/gif",
    "image/png",
    "image/jpeg",
    "image/pjpeg",
);
 
//检查上传的文件是一个图像
// 然后移动到需要的目录
if (in_array($_FILES["file"]["type"], $valid_mime_types)) {
    $destination = "uploads/" . $_FILES["file"]["name"];
    move_uploaded_file($_FILES["file"]["tmp_name"], $destination);
}

上面的代码片段检查了上传文件的MIME类型,以将其验证为图像,如果通过,则将文件移动到适当的位置。所以有什么问题?好吧,如果您阅读了有关处理文件上载的文档页面,请注意该内容$_FILES[“file”][“type”]

它的值完全在客户端控制下,且不在PHP端进行检查。

PHP文档

Web安全的第一条规则是永远不要信任用户提交的数据。

不要因为客户端说这是一个图像就允许这类文件进入您的服务器,就像给陌生人提供您房子的钥匙一样,因为他说他不会偷任何东西。这是一个可用于利用此漏洞的脚本的快速示例:

// The destination for our attack:
$host = "127.0.0.1";
$port = 8887;
$page = "/server.php";
 
// Here we have the file we're uploading (note the content-type):
$payload =
"------ThisIsABoundary
Content-Disposition: form-data; name="file"; filename="evil.php"
Content-Type: image/jpeg
 
<?php phpinfo();
------ThisIsABoundary--";
 
// 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);

上面的脚本会生成一个标准的HTTP请求,该请求会上传一个后缀名为php文件的evil.php文件。如果服务器只依赖于$_FILES[“file”][“type”]验证上传,那么就会误以为我们正在向其发送图像。


第二部分:mod_mimeApache模块和多个文件扩展


那么,这又有什么解决方案呢?(译者注:那么这又回产生什么问题呢?)有些人使用文件扩展名检查,因为服务器将根据文件扩展名确定适当的处理程序和内容类型。这样的事情在大多数情况下都会起作用:

$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);
}

您可能会对此感到安全,这取决于您的服务器设置。请参阅,可以将Apache配置为解释同一文件的多个文件扩展名。尽管允许文件名同时确定语言和内容类型可能很有用,但它也对不了解此功能的开发人员产生了一个安全漏洞。

利用多文件扩展名漏洞并不需要太多技巧。抓取任何PHP文件,将.jpg其添加到其名称的末尾,然后将其上传到易受攻击的服务器。然后在您的Web浏览器中访问该文件,这将使Apache运行脚本并输出结果,小菜一碟不是么。

第三部分:伪装成图像的脚本

对于已经了解这两种漏洞伪造的MIME类型额外的文件扩展名的人经常建议使用诸如getimagesize()函数确保上传的文件确实是图像之类的东西,如下代码所示。

if (@getimagesize($_FILES["file"]["tmp_name"]) !== false) {
    $destination = "uploads/" . $_FILES["file"]["name"];
    move_uploaded_file($_FILES["file"]["tmp_name"], $destination);
}

当然,只要是图像就不会有害对吗?我的意思是,看看这只小猫:

那只小猫永远不会伤害任何人,对吗?看到这你应该感到庆幸,这只是一只白帽子小猫图片。

现在,单击“小猫图片”(它会在新选项卡中打开),然后看看会发生什么。您预想中应该看到的是完全相同的小猫,但是这次,这里将其作为PHP脚本运行。为此,我采用了非常棒的jhead 工具,并在原始小猫图像中嵌入了注释。我的注释看起来像这样:

<?php blahblahblah(); __halt_compiler();

该__halt_compiler()函数的调用是为了将剩下的图形数据不解释为PHP脚本和不抛出一个脚本解析错误。

这就是为什么脚本输出在输出实际图像数据之前停止的原因。如果您想确切地看到我写的内容,可以下载原始小猫图像(单击鼠标右键,将图像另存为…),然后在您喜欢的文本编辑器中将其打开查看。

第四部分:最后

上面的安全检查当然不是没有用的,如果您希望上传图片,那么最好检查一下是否为真的图片。拥有再多层的安全保护都不为过,这永远是一件好事(译者注:虽然说可能性能有点折扣之类的)。

但是对于需要我们隐蔽的保护我们的脚本,我们该怎么办呢?(译者注:如何不在脚本里面而是在服务器接到请求时就进行安全防护的措施是怎么做的呢?)

我们的目标不仅是确保上传的文件是图像,而且还确保服务器不运行任何脚本处理程序。我最喜欢的方法是使用Apache的ForceType指令:

ForceType application/octet-stream
<FilesMatch "(?i).jpe?g$">
    ForceType image/jpeg
</FilesMatch>
<FilesMatch "(?i).gif$">
    ForceType image/gif
</FilesMatch>
<FilesMatch "(?i).png$">
    ForceType image/png
</FilesMatch>

将此代码放在.htaccess您的上传目录中的文件中,将仅允许将图像与其默认处理程序相关联。其他所有内容将用作纯字节流,并且不会运行任何处理程序。

我更喜欢“关闭PHP”解决方案(php_flag engine off),因为它可以一次关闭所有脚本处理程序,以防您的服务器还提供perl,python或其他服务。当然,为了安全起见,您总是可以同时做这两个事情。

更好的解决方案是将文件放在Web目录之外,这样就永远不会再提供可执行的代码文件了。然后,您需要编写一个脚本,该脚本接受该文件的请求,从文件系统中检索适当的文件,并使用正确的标头将其输出。当然,基于用户输入而输出文件具有其自身的一组安全漏洞,但这已经是另一回事了。

最后而且很重要的事情,一定要确保重命名上传的文件。选择一个随机名称会使攻击者更难于入侵您的服务器,并且可以确保没有人通过覆盖您的文件.htaccess或.user.ini文件来实现突破。

网络上有很多关于安全性的资源,如果您有兴趣阅读更多内容,请查阅OWASP,尤其是OWASP备忘单页面。

 

本文由机翻+人工翻译,如果有语句不通的地方请留言。本文的主要目的是保存中文翻译,方便查找文章内容。若侵权请联系我删除谢谢。

最后修改日期:2021年3月26日

作者

留言

撰写回覆或留言

发布留言必须填写的电子邮件地址不会公开。