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

41

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

$_GET['path']应该相对于它。

但是,如何在不允许目录遍历的情况下实现这一点(读取目录)?

例如。

/\.\.|\.\./

无法正确过滤。


我希望这个问题完全是学术性的。仅从你提问的事实来看,我会说你不应该基于用户输入允许直接文件系统访问。有一些维护良好的框架可用,可以为您提供此功能,而无需尝试自己编写。如果不确切知道自己在做什么,请勿这样做。 - JSON
8个回答

129

好的,一个选项是比较实际路径:

$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 的输出不会有任何 "虚拟目录",例如 ... ...


1
符号链接怎么办?或者我们想要检查的文件还不存在怎么办?(例如,在潜在路径中创建一个新文件)。 - Petah
3
符号链接将通过realpath解析为规范路径。对于不存在的文件,我怀疑这是一个可解决的问题,建议一开始就不要这样做(永远不要允许用户直接指定新文件)... - ircmaxell
1
在用户通过CMS上传文件并创建目录的情况下,如果没有用户指定,这该如何实现? - Petah
1
我看到了一个可能的改进。虽然这是安全的,但恶意用户可以了解父结构。例如,这将解析为一个好的路径:"?path=../../bar/baz/"。 - marcovtwout
4
写作需要新文件怎么办?如果文件不存在,realpath 函数返回空值。请注意,这里只是翻译,并未做出解释或提供其他内容。 - Sufendy
显示剩余4条评论

18

ircmaxell的答案并不完全正确。我在几个片段中都看到了这个解决方案,但它有一个与realpath()的输出相关的bug。 realpath()函数会删除尾部的目录分隔符,因此请想象两个连续的目录:


/foo/bar/baz/

/foo/bar/baz_baz/

由于 realpath() 会删除最后一个目录分隔符,因此如果$_GET['path']等于"../baz_baz",那么您的方法将返回"good path",因为它看起来像是

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!
}

1
只需检查($realUserPath === false || strcmp($realUserPath, $realBase . DIRECTORY_SEPARATOR) !== 0)也可以起作用。 - Goozak

6

仅检查类似../等模式是不够的。以"../"为例,它的URI编码为"%2e%2e%2f"。如果您的模式检查发生在解码之前,您将错过这种遍历尝试。黑客可以使用某些其他技巧来规避模式检查,特别是在使用编码字符串时。

我成功地阻止了大多数此类攻击,方法是使用类似realpath()的东西将任何路径字符串规范化为其绝对路径。然后,我开始通过将它们与我预定义的基本路径匹配来检查遍历攻击。


1

你可能会尝试使用正则表达式来删除所有的../,但是 PHP 中有一些很好的函数可以更好地完成这项工作:

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

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

realpath - 返回文件的完整路径,例如about.php将成为/home/www/pages/about.php,但仅当文件存在时。

组合使用它们只返回文件名,但仅当文件存在时。


1
我认为这并不能防止遍历! - ESP32

0

在创建新文件或文件夹时,我发现可以使用两阶段方法:

首先,使用一个类似于realpath()的自定义实现来检查遍历尝试,但可以处理任意路径而不仅仅是现有文件。可以从这里 here入手,再加上urldecode()和其他你认为值得检查的内容。

现在,使用这种简单的方法可以过滤掉一些遍历尝试,但可能会错过一些特殊字符组合、符号链接、转义序列等的骇客攻击。但由于你确定目标文件不存在(使用file_exists进行检查),因此没有人可以覆盖任何东西。最坏的情况是,有人可以获取你的代码并创建一个文件或文件夹,这在大多数情况下可能是可以接受的风险,前提是你的代码不允许他们立即写入该文件/文件夹。

最后,因此路径现在指向一个现有位置,因此您现在可以使用上面建议的方法利用realpath()进行适当的检查。如果此时发现发生了遍历,则仍然相对安全,只要确保防止任何尝试写入目标路径。此外,现在您可以删除目标文件/目录并说它是一次遍历尝试。

我并不是说它不能被黑客攻击,因为毕竟它仍然可能允许对FS进行非法更改,但仍然比仅执行自定义检查要好,这些检查无法利用realpath(),并且通过在某个地方创建临时和空文件或文件夹留下的滥用窗口较小,而允许他们将其变成永久性并甚至写入其中,就像只进行自定义检查可能会错过一些边缘情况一样。

如果我错了,请纠正我!


0
我已经编写了一个检查遍历的函数:
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 不存在时工作。


0
在我的版本中,我用 $_SERVER['REQUEST_URI'] 替换了 $_GET['file'],从服务器变量中获取请求的 URI。然后,我使用 parse_url() 函数和 PHP_URL_PATH 常量来提取 URI 中的路径组件,不包括任何查询参数。
脚本的其余部分执行路径规范化,使用基本目录验证文件路径,检查文件存在性并向用户提供文件服务。
通过使用 $_SERVER['REQUEST_URI'],即使文件参数没有明确设置为查询参数,也可以处理 URL 中的文件路径提取。
$baseDirectory = '/path/to/file/'; // Define the base directory where the file(s) is/are located

if ($_SERVER['HTTP_HOST'] == 'localhost') { 
    $baseDirectory = 'C:\xampp\htdocs'; // For localhost use
}

// Check if the 'REQUEST_URI' is set in the server variables
if (!isset($_SERVER['REQUEST_URI'])) {
    $this->logger->logEvent('REQUEST_URI NOT SET: '.__LINE__.' '.__FILE__);
    die('Invalid file request');
}

// Get the requested URI from the server variables
$requestedUri = $_SERVER['REQUEST_URI'];

// Extract the file path from the requested URI
$requestedFile = parse_url($requestedUri, PHP_URL_PATH);

// Normalize the file path to remove any relative components
$requestedFile = realpath($baseDirectory . $requestedFile);

// Check if the normalized file path starts with the base directory
if (strpos($requestedFile, $baseDirectory) !== 0) {
    $this->logger->logEvent('Directory Traversal: '.$requestedUri);
    die('Invalid file path');
}

// Check if the requested file exists
if (!file_exists($requestedFile)) {
    $this->logger->logEvent($requestedFile. ' not found: '.__LINE__.' '.__FILE__);
    die('File not found');
}
// Serve the file to the user

-3

1

将一个空的index.htm放在-Index块中

2

按开始筛选 sQS

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

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

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