如何安全地允许用户上传文件

如果在为自己或客户开发 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字节来回避目录遍历和本地文件泄露攻击。

<?php
/**
 * 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 注入漏洞),并为实际的文件系统存储使用随机文件名。
  • 无需总是从磁盘读取,而是与 Memcache 集成并直接从 RAM 提供流行文件(这通常会导致 90% 的性能提升)。

但是,即使没有这些增强功能,您也可以轻松添加目录级(甚至文件级)访问控制。

如果您遵循此建议,那么恭喜您,您已经避免了大多数困扰接受最终用户上传文件的应用程序的攻击。而且您无需深入研究服务器配置领域即可完成所有这些工作。

现在让我们看看一些不太有效的策略。

保护文件上传脚本的无效策略

将不良文件扩展名列入黑名单

考虑这个片段:

$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

<?php
// 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 上可执行的文件扩展名(例如php3phtml)。

(当我们向 Snort 规则开发人员询问这种微不足道的绕过时,他们说,“99.9% 的攻击者不会考虑这样做。”)

检查 MIME 类型$_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 在他的博客文章“通过电子邮件订购远程文件包含”中所解释的那样。

以下是它的工作原理:

  1. 在目标应用程序的其他地方找到一个本地文件包含漏洞,该漏洞尚未立即用于破坏整个系统。
  2. 在 Web 根目录之外上传包含恶意 shell 代码的文件。
  3. 使用本地文件包含来获取要执行的文件。

对于如何处理这个问题,有两种思想流派:

  1. 修复漏洞,这样一开始就不会发生这种情况。
  2. 对上传的文件进行编码或加密,使它们不能以这种方式直接执行。(纵深防御。)

鉴于许多环境都可以通过 Makan 的攻击加以利用,我们强烈建议只修复该漏洞。编码/加密文件有学术价值,甚至在需要这样做的情况下,但通常在我们描述的场景中是不必要的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值