在 PHP 中防止目录遍历但允许路径

Posted

技术标签:

【中文标题】在 PHP 中防止目录遍历但允许路径【英文标题】:Preventing Directory Traversal in PHP but allowing paths 【发布时间】:2011-05-11 10:43:47 【问题描述】:

我有一个基本路径 /whatever/foo/

$_GET['path'] 应该是相对的。

但是如何在不允许目录遍历的情况下完成此操作(读取目录)?

例如。

/\.\.|\.\./

无法正确过滤。

【问题讨论】:

我希望这个问题完全是学术性的。仅基于您必须询问的事实,我会说您不应该允许基于用户输入的直接文件系统访问。有一些维护良好的框架可以为您提供此功能,而无需尝试自己滚动。不要在不知道自己在做什么的情况下这样做。 【参考方案1】:

嗯,一种选择是比较真实路径:

$basepath = '/foo/bar/baz/';
$realBase = realpath($basepath);

$userpath = $basepath . $_GET['path'];
$realUserPath = realpath($userpath);

if ($realUserPath === false || strpos($realUserPath, $realBase) !== 0) 
    //Directory Traversal!
 else 
    //Good path!

基本上,realpath() 会将提供的路径解析为实际的硬物理路径(解析符号链接、.../// 等)...所以如果真正的用户路径没有从真正的基本路径开始,它正在尝试进行遍历。请注意,realpath 的输出将包含任何“虚拟目录”,例如 ......

【讨论】:

编辑:strpos 已经是多字节安全的。引入 mb 替代方案可能会引入其他漏洞... 符号链接呢?或者如果我们要检查的文件还不存在怎么办? (即在预期路径中创建一个新文件)。 @petah 符号链接将由 realpath 解析到规范路径。对于不存在的文件,我怀疑它是否是一个可解决的问题,我建议不要一开始就这样做(永远不要让用户直接指定新文件)...... 在用户通过 CMS 上传文件和创建目录的意义上,如果没有用户指定它们怎么可能? 写新文件怎么样?如果文件不存在,realpath 似乎返回空。【参考方案2】:

ircmaxell 的回答并不完全正确。我已经在几个 sn-ps 中看到了该解决方案,但它有一个与 realpath() 的输出相关的错误。 realpath() 函数删除了尾随目录分隔符,因此想象两个连续的目录,例如:

/foo/bar/baz/

/foo/bar/baz_baz/

由于realpath() 将删除最后一个目录分隔符,因此如果$_GET['path'] 等于“../baz_baz”,您的方法将返回“好路径”,因为它类似于

strpos("/foo/bar/baz_baz", "/foo/bar/baz")

也许:

$basepath = '/foo/bar/baz/';
$realBase = realpath($basepath);

$userpath = $basepath . $_GET['path'];
$realUserPath = realpath($userpath);

if ($realUserPath === false || strcmp($realUserPath, $realBase) !== 0 || strpos($realUserPath, $realBase . DIRECTORY_SEPARATOR) !== 0) 
    //Directory Traversal!
 else 
    //Good path!

【讨论】:

检查($realUserPath === false || strcmp($realUserPath, $realBase . DIRECTORY_SEPARATOR) !== 0) 也可以。【参考方案3】:

仅检查 ../ 之类的模式是不够的。以“../”为例,哪个 URI 编码为“%2e%2e%2f”。如果您的模式检查发生在解码之前,您将错过此遍历尝试。黑客还可以使用其他一些技巧来绕过模式检查器,尤其是在使用编码字符串时。

正如 ircmaxwell 所建议的那样,通过使用诸如 realpath() 之类的东西将任何路径字符串规范化为其绝对路径,我在阻止这些方面取得了最大的成功。只有这样我才开始通过将遍历攻击与我预定义的基本路径进行匹配来检查它们。

【讨论】:

【参考方案4】:

您可能很想尝试使用正则表达式来删除所有 ../s,但是 php 中内置了一些不错的函数,它们会做得更好:

$page = basename(realpath($_GET));

basename - 从路径中删除所有目录信息,例如../pages/about.php 将变为 about.php

realpath - 返回文件的完整路径,例如about.php 将变为 /home/www/pages/about.php,但前提是文件存在。

结合起来,它们仅返回文件名,但仅在文件存在时才返回。

【讨论】:

我不认为这会阻止遍历!【参考方案5】:

在研究新文件或文件夹的创建时,我认为我可以使用两阶段方法:

首先使用 realpath() 类似函数的自定义实现检查遍历尝试,但它适用于任意路径,而不仅仅是现有文件。有一个很好的起点here。使用urldecode() 以及您认为值得检查的任何其他内容对其进行扩展。

现在使用这种粗略的方法,您可以过滤掉一些遍历尝试,但您可能会错过一些特殊字符、符号链接、转义序列等的黑客组合。但是既然您确定目标文件不存在(检查使用file_exists)没有人可以覆盖任何东西。最坏的情况是有人可以让您的代码在某处创建文件或文件夹,这在大多数情况下可能是可接受的风险,前提是您的代码不允许他们立即写入该文件/文件夹。

最后,路径现在指向一个现有位置,因此您现在可以使用上面建议的方法使用realpath() 进行正确的检查。如果此时事实证明发生了遍历,那么您或多或少仍然是安全的,只要您确保防止任何尝试写入目标路径即可。现在你也可以删除目标文件/目录并说这是一次遍历尝试。

我并不是说它不能被黑客入侵,因为毕竟它仍然可能允许对 FS 进行非法更改,但仍然比仅进行自定义检查要好,因为不能使用 realpath() 和滥用窗口通过在某处创建临时和空文件或文件夹保持打开状态比允许他们将其永久化甚至写入其中要低,因为只有自定义检查可能会错过一些边缘情况。

如果我错了还请指正!

【讨论】:

【参考方案6】:

我写了一个函数来检查遍历:

function isTraversal($basePath, $fileName)

    if (strpos(urldecode($fileName), '..') !== false)
        return true;
    $realBase = realpath($basePath);
    $userPath = $basePath.$fileName;
    $realUserPath = realpath($userPath);
    while ($realUserPath === false)
    
        $userPath = dirname($userPath);
        $realUserPath = realpath($userPath);
    
    return strpos($realUserPath, $realBase) !== 0;

仅此行if (strpos(urldecode($fileName), '..') !== false) 就足以防止遍历,但是,黑客可以通过多种不同方式遍历目录,因此最好确保用户从真正的基本路径开始。

仅仅检查用户从真正的基本路径开始是不够的,因为黑客可以遍历当前目录并发现目录结构。

while 允许代码在 $fileName 不存在时工作。

【讨论】:

【参考方案7】:

我假设您的意思是不允许用户遍历目录是吗?

如果你试图阻止你自己的 PHP 遍历目录,你应该首先让 php 正常工作。

你需要阻止用户的是一个修改过的 .htaccess 文件...

Options -Indexes

(这一切都假设您在谈论用户)

【讨论】:

MainMa 明白我想要达到的目标。 他提出了$_GET,很明显他是在试图阻止黑客的目录遍历攻击,所以请不要说“你应该首先让php正常工作”。 【参考方案8】:

1

为 -Index 块放置一个空 index.htm

2

在启动时过滤 sQS

// Path Traversal Attack
if( strpos($_SERVER["QUERY_STRING"], "../") )
    exit("P.T.A. B-(");

【讨论】:

如果../的位置=== 0,则此操作失败。

以上是关于在 PHP 中防止目录遍历但允许路径的主要内容,如果未能解决你的问题,请参考以下文章

php 遍历文件夹文件问题

php遍历文件夹是先文件夹还是文件

php 循环遍历文件夹下面的所有目录及文件并且每个文件都写入一句话

PHP脚本遍历目录/文件树并作为嵌套UL输出树[关闭]

CentOS6.5下Apache防止目录遍历

允许目录遍历 Apache2