如果在为自己或客户开发 Web 应用程序的过程中,您发现自己编写代码以允许用户上传文件,那么您就进入了一个全新的复杂世界,一个简单的错误可能导致远程攻击安全漏洞。
幸运的是,您可以做出一个简单的设计决策来阻止与处理文件上传相关的最常见漏洞:
如果您的网站是example.com
并且当访问者在他们的浏览器中访问该网站时,位于的脚本/home/example/public_html/index.php
被执行,那么您不应该存储用户上传的文件/home/example/public_html/
或其任何子目录。相反,一个好的候选人应该是/home/example/uploaded/
.
由于您的文件安全地超出了可直接访问的范围(因此直接作为代码执行),您就免去了编写复杂的黑名单、白名单、推断文件真正 MIME 类型的繁琐尝试(不要相信提供的那种)$_FILES
; 攻击者可以将其更改为他们想要的任何内容),并且笨拙地尝试使用 PHP 的 GD 扩展名处理不受信任的图像文件(出于安全目的,不应依赖它)。
仅仅因为您的文件存储在文档根目录之外并不意味着您不能让您的用户访问它们。例如,您可以将文件转发到无法执行动态内容的静态内容服务器(没有 mod_php 的 Apache 服务器)或第三方服务(例如 Cloudinary)。这仍然满足不存储在 Web 服务器的文档根目录中的要求。
如果您不能单独存储文件,请将它们存储在本地并使用一个简单的代理脚本,该脚本允许对上传的文件进行只读访问(同时保证文件永远不会被直接执行)。与使用单独的服务器来提供静态用户内容相比,此解决方案会带来性能损失。
例如,此脚本假定您将用户提供的文件存储在用户提供的文件名中(当然还要检查冲突)。realpath()
它通过单独检查和检查每个目录名称并剥离NUL
字节来回避目录遍历和本地文件泄露攻击。
- /**
- * This is an example of an image proxy script. It assumes an .htaccess or nginx rewrite e.g.
- * files/.* -> /proxy_script.php?path=$1
- */
- require "../vendor/autoload.php";
-
- if (empty($_GET['path'])) {
- header('HTTP/1.1 404 Not Found');
- exit;
- }
-
- // We're going to iterate over $dirs
- $dirs = explode('/', $_GET['path']);
-
- // We start with $path set to the basepath
- $path = BASEPATH;
-
- // For the FileInfo functions:
- $fi = new finfo(FILEINFO_MIME, '/usr/share/file/magic');
-
- // Bad filenames that should trigger an alert and terminate the script
- $bad_files = [
- '..', '.git', '.htaccess', '.svn', 'composer.json', 'composer.lock', 'framework_config.yaml'
- ];
-
- // Let's iterate through directories
- while (!empty($dirs)) {
- // PHP has a bad history of handling NUL bytes. Just strip them.
- $piece = str_replace("\0", '', array_shift($dirs));
- if (empty($piece)) {
- continue;
- }
- if (in_array($piece, $bad_files)) {
- // We don't want these requests to succeeed.
- Framework::logger()->alert('File proxy - blacklist violation');
- header('HTTP/1.1 404 Not Found');
- exit;
- }
- if (is_dir($path . DIRECTORY_SEPARATOR . $piece)) {
- $realpath = realpath($path . DIRECTORY_SEPARATOR . $piece);
- if (strpos($realpath, $path) !== 0) {
- Framework::logger()->alert(
- 'Directory traversal attempt that somehow bypassed ".." blacklist.'
- );
- header('HTTP/1.1 404 Not Found');
- exit;
- }
- }
- $path .= DIRECTORY_SEPARATOR . $piece;
- }
- // If the file exists and is within BASEPATH (i.e. not a successful LFI)
- $realpath = realpath($path);
- if (file_exists($realpath) && strpos($realpath, BASEPATH) === 0) {
- $type = finfo_file($fi, $file);
- header("Content-Type: ".$type);
- readfile($realpath);
- exit;
- }
- // Are you still here?
- header('HTTP/1.1 404 Not Found');
当然,有很多方法可以改善这一点。举两个:
/home/example/uploaded/some/directories/user_provided.file
,而是将所有相关元数据存储在数据库记录中(同时注意防止 SQL 注入漏洞),并为实际的文件系统存储使用随机文件名。但是,即使没有这些增强功能,您也可以轻松添加目录级(甚至文件级)访问控制。
如果您遵循此建议,那么恭喜您,您已经避免了大多数困扰接受最终用户上传文件的应用程序的攻击。而且您无需深入研究服务器配置领域即可完成所有这些工作。
现在让我们看看一些不太有效的策略。
考虑这个片段:
- $block_extensions = ['php', 'pl', 'cgi'];
- $ext = preg_replace('/.+?\.(.+)$/', '$1', $_FILES['file']['name']);
- if (in_array($ext, $block_extensions)) {
- move_uploaded_file( /* ... */ );
- }
这种方法的问题与困扰任何黑名单策略的问题相同:它允许任何不知道是坏的东西。概念证明:将以下脚本另存为0day.phtml
,将其与您的表单一起上传,然后访问upload_dir/0day.phtml?cmd=whoami
:
- // If you are reading this, the code did not execute:
- echo shell_exec($_GET['cmd']), PHP_EOL;
如果您在 Apache Web 服务器上运行 PHP,那么您的浏览器应该显示类似www-user
. 此外,攻击者可以创造性地上传他们自己的恶意 .htaccess 文件。
这似乎很明显,但即使是网络安全专家也会忽略它。
例如,Snort 规则 1-27667尝试阻止利用CVE-2013-5576(允许攻击者通过在文件名上附加额外内容来上传任意 PHP 脚本.
)的尝试只会阻止.php.
文件上传尝试,但不会阻止任何其他恶意默认情况下在 Apache 上可执行的文件扩展名(例如php3
或phtml
)。
(当我们向 Snort 规则开发人员询问这种微不足道的绕过时,他们说,“99.9% 的攻击者不会考虑这样做。”)
$_FILES
考虑这个片段:
- $allowed_types = ['image/jpg', 'image/png', 'image/jpeg', 'image/gif'];
- if (in_array($_FILES['file']['type'], $allowed_types)) {
- move_uploaded_file( /* ... */ );
- }
这似乎是个好主意,但如果有人想上传恶意文件(例如,PHP 反向 shell 脚本),他们需要做的就是上传文件并告诉服务器它的 MIME 类型是image/gif
. 游戏结束。
getimagesize()
验证文件是否为图像在开发需要照片的 Web 应用程序时,一些开发人员认为他们可以通过使用 GD 扩展和图像处理功能来巧妙地击败攻击者,以保证用户提供的文件实际上是图像。不幸的是,这不是万无一失的。
正如 Benjamin Watson 的一篇出色的博客文章所展示的,您可以上传有效的 JPEG 图像,但仍会在其 EXIF 评论中隐藏恶意负载。
即使您将所有用户的文件保存在文档根目录之外,如果您的应用程序中存在其他基于文件系统的漏洞,您的上传表单仍然可能成为应用程序的攻击媒介。但是,您的电子邮件服务器也可以,正如 Keith Makan 在他的博客文章“通过电子邮件订购远程文件包含”中所解释的那样。
以下是它的工作原理:
对于如何处理这个问题,有两种思想流派:
鉴于许多环境都可以通过 Makan 的攻击加以利用,我们强烈建议只修复该漏洞。编码/加密文件有学术价值,甚至在需要这样做的情况下,但通常在我们描述的场景中是不必要的。