导读:本期聚焦于小伙伴创作的《PHP中如何用scandir实现安全的目录遍历避免路径泄露》,敬请观看详情。把用户传入的参数直接拼进scandir函数,往往会让攻击者用../跳出预期文件夹,读取到配置或密码文件。安全的做法是对输入做白名单校验,再用realpath解析绝对路径并确认是否仍处于根目录内。本文对比了裸用scandir与加防护两种写法,指出open_basedir限制虽能兜底但不够灵活。通过递归封装,可以把只列单层目录的scandir扩展成整棵树的遍历,同时过滤掉.与..避免重复进入父级。掌握这些细节,才能在提供文件浏览功能时不留后门。

在PHP开发里,scandir是一个用来读取目录条目的基础函数,它返回包含文件和子目录名的数组。不过如果只是简单地把外部参数传给它,很容易引发目录遍历漏洞,导致服务器上的敏感文件被非法列出或读取。真正安全的目录遍历,需要在调用scandir前后都做严格的路径控制。

PHP中如何用scandir实现安全的目录遍历避免路径泄露

一、scandir的基本用法与隐患

scandir函数的签名非常直白,传入一个目录路径,就能拿到该路径下的所有项。下面是一段最常见、但也最危险的写法:

<?php
// 危险示例:直接使用用户输入
$dir = $_GET['path'];
$files = scandir($dir);
foreach ($files as $f) {
    echo $f . "<br>";
}
?>

这段代码把URL中的path参数原样交给scandir。攻击者在浏览器里输入?path=../../etc,就可以让PHP去扫描系统目录,从而泄露服务器结构。更严重的是,如果后续代码还允许下载这些文件,后果就不只是遍历而是任意文件读取了。

很多新手以为scandir只能列当前目录,其实它完全依赖传入的字符串。只要字符串在操作系统看来是合法路径,它就会照做。因此,限制路径范围的责任完全在开发者自己,而不是函数本身。

二、安全的目录遍历实现思路

要避免路径穿越,核心思路是:先定义允许访问的根目录,再把用户传入的相对名拼进去,最后用realpath拿到规范化绝对路径,确认它确实以根目录开头。下面给出一种封装函数:

<?php
define('BASE_DIR', '/var/www/uploads/');

function safe_scandir($sub_path) {
    // 禁止包含斜杠和上级引用,仅允许纯目录名
    if (preg_match('/[/\\.]/', $sub_path)) {
        return false;
    }
    $target = realpath(BASE_DIR . $sub_path);
    if ($target === false) {
        return false;
    }
    // 确认目标仍在基目录内
    if (strpos($target, realpath(BASE_DIR)) !== 0) {
        return false;
    }
    return scandir($target);
}

$list = safe_scandir($_GET['folder']);
if ($list === false) {
    echo '非法访问';
} else {
    foreach ($list as $name) {
        if ($name === '.' || $name === '..') continue;
        echo htmlspecialchars($name) . "<br>";
    }
}
?>

上面的代码首先用正则拒绝了一切带斜杠、反斜杠和点的输入,这样用户根本没法传入../。接着把输入拼到基目录后,用realpath展开为真实绝对路径,再判断是不是从基目录开始。哪怕系统层做了软链,strpos检查也能挡住越界。

这种写法的优点是逻辑清晰、不依赖服务器配置;缺点是要为每个业务单独定义基目录。相比直接靠php.ini里的open_basedir做全局限制,它更灵活,也能在共享主机之外给出细粒度控制。

三、用scandir实现递归目录树遍历

scandir本身只列一层,如果要遍历整个子目录树,可以在安全封装的基础上做递归。注意每次进入子目录都要复用前面的校验逻辑,不能中途放松。

<?php
function scan_tree($sub_path, $depth = 0) {
    $list = safe_scandir($sub_path);
    if ($list === false) return;
    foreach ($list as $name) {
        if ($name === '.' || $name === '..') continue;
        echo str_repeat('  ', $depth) . htmlspecialchars($name) . "<br>";
        // 拼出下级相对路径继续递归
        $next = $sub_path === '' ? $name : $sub_path . '/' . $name;
        scan_tree($next, $depth + 1);
    }
}
scan_tree('');
?>

这里把空字符串作为起始,表示从BASE_DIR根开始。每遇到一个条目,先打印缩进名称;若它其实是目录,则把名字拼到当前相对路径后再次调用自己。由于safe_scandir已经拦掉了点号与斜杠,递归时不会出现无限返回父目录的死循环。

从性能角度看,递归深度大时可能占用较多栈空间,但对于一般的文件管理后台足够用。若目录极多,可改为显式栈或队列来避免递归过深。无论如何,过滤掉.与..是保证遍历正确的关键一步。

四、常见误区与补充建议

有人觉得在scandir之后用basename()处理一下返回项就安全了,这是概念混淆。basename只处理文件名本身,不改变你传给scandir的路径,穿越发生在读取之前,而非之后。还有人以为把open_basedir设好就不需要写代码防护,但open_basedir在某些扩展或命令行模式下并不生效,不能作为唯一防线。

另外,scandir返回的数组包含.和..两项,若在前端展示时忘了跳过,会让用户看到父目录链接,甚至造成重复遍历。推荐统一在循环里用条件continue掉这两项,保持界面干净也避免逻辑错误。

最后提醒,任何用到scandir的地方,输出文件名到页面都要经htmlspecialchars处理,防止文件名里夹带HTML标签造成注入。把路径校验、递归封装和输出转义三件事做全,目录遍历功能才能既好用又安全。

PHPscandir目录遍历修改时间:2026-08-07 23:54:35

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。