路径穿越防护关键在于用realpath()校验真实路径并检查其是否在合法根目录内,而非简单过滤“..”;必须显式处理realpath()返回false的情况,并结合is_file()和is_readable()双重确认,避免依赖basename()或file_exists()等不安全函数。

用 realpath() 做路径规范化和白名单校验
直接拼接用户输入到文件函数(如 file_get_contents()、include)是路径穿越的根源。关键不是“过滤掉 ..”,而是让系统只认准一个真实、可控的起点。
realpath() 会解析所有 ..、.、符号链接,返回绝对路径;如果越权或路径不存在,它返回 false —— 这个返回值必须显式检查,不能忽略。
- 先定义你的合法根目录(比如
/var/www/uploads/),末尾带斜杠 - 拼接后立即调用
$real_path = realpath($root_dir . $_GET['file']) - 若
$real_path === false,直接拒绝 - 再用
str_starts_with($real_path, $root_dir)(PHP 8.0+)或strpos($real_path, $root_dir) === 0(PHP 7.x)确认它真在根目录下 - 只有这两步都通过,才允许读取或包含
用 basename() 截断再拼接,但别单独依赖它
basename() 能剥离掉所有路径前缀,只留下最后一段文件名,对简单场景(比如只允许访问上传目录下的单层文件)很有效。但它不处理 URL 编码绕过(如 %2e%2e%2f)或空字节截断(filename%00.jpg),所以必须配合其他校验。
- 执行
$safe_name = basename($_GET['file']),得到纯净文件名 - 手动拼回白名单路径:
$full_path = '/var/www/uploads/' . $safe_name - 加一层防护:检查
$safe_name是否为空、是否为'.'或'..' - 必须调用
is_file($full_path)和is_readable($full_path)双重确认,不能只信basename()的结果
避免用 file_exists() 或 is_file() 做路径校验
这两个函数本身不解析符号链接,但它们的底层行为受操作系统影响,且无法区分“路径存在”和“路径合法”。更危险的是,攻击者可能提前在上传目录里建好指向 /etc/passwd 的符号链接,然后传入该链接名 —— file_exists() 会返回 true,而后续的 readfile() 就直接读了敏感文件。
立即学习“PHP免费学习笔记(深入)”;
- 永远不要把
file_exists($_GET['file'])当作安全判断依据 - 如果要用,必须先用
realpath()把路径转成绝对路径,再比对根目录 - 上传场景中,
move_uploaded_file()是唯一安全的移动方式,别用copy()或fopen()+fwrite()
上传文件时,路径和文件名必须彻底解耦
最稳妥的方式,是根本不要让用户控制路径字符串。业务层用 ID 或哈希值查数据库,拿到预存的磁盘路径,用户参数里只传 id=123 或 hash=7f3a9b2d。
- 数据库表存字段:
id、original_name、stored_path(如/var/www/storage/7f3a9b2d_report.pdf) - 接收请求后只查
SELECT stored_path FROM files WHERE id = ?,不拼任何路径 - 即使数据库被拖库,攻击者也拿不到路径规律,无法构造遍历
- 这个方案天然规避了
basename()漏洞、符号链接、编码绕过等所有变体
真正容易被忽略的点是:realpath() 返回 false 时,很多开发者只当“文件不存在”处理,却没意识到这恰恰是越权的信号;还有人把 basename() 当万能药,忘了它对编码和空字节毫无抵抗力。



















