basename()不能防御目录遍历,真正有效的是realpath()结合白名单根目录前缀校验,如strpos($full_path, '/var/www/uploads/') === 0。

PHP中basename()不能防目录遍历,别信老教程
很多旧文章说用 basename() 提取文件名就能防路径穿越,这是错的。它只截掉前面的路径分隔符,对 ../../../etc/passwd 这种输入毫无作用——basename('../../../etc/passwd') 返回的还是 passwd,但上游代码若直接拼进 file_get_contents() 就炸了。
真正要拦住的是「路径解析后仍落在非授权目录外」这个行为,不是单纯看字符串里有没有点和点点。
- 永远不要信任用户传入的原始路径字符串,包括
$_GET['file']、$_POST['path']等 -
basename()只适合清理显示用的文件名,不参与路径拼接逻辑 - 如果必须动态读文件,优先用白名单映射(如
['report' => '/var/www/data/report.pdf']),而非拼路径
用realpath() + 白名单根目录校验最稳妥
核心思路:把用户给的路径转成绝对路径,再检查它是否落在你允许的目录树内。这能对抗符号链接、多层 ..、空字节截断等常见绕过。
示例逻辑:
立即学习“PHP免费学习笔记(深入)”;
if (isset($_GET['file'])) {
$user_path = $_GET['file'];
$full_path = realpath('/var/www/uploads/' . $user_path);
// 注意:realpath返回false说明路径不存在或不可访问
if ($full_path === false || strpos($full_path, '/var/www/uploads/') !== 0) {
die('Access denied');
}
echo file_get_contents($full_path);
}
-
realpath()会解析所有..、.、符号链接,返回真实物理路径 - 必须用
strpos($full_path, $allowed_root) === 0判断前缀,不能用===全等(因为用户可能传./xxx) - 如果
$allowed_root末尾没加/,比如写成'/var/www/uploads',那/var/www/uploads2/evil.txt也会被误放行
遇到open_basedir限制时,realpath()可能失败
当 PHP 配置了 open_basedir,而用户输入的路径指向被限制区域外的位置,realpath() 会返回 false —— 这本身是好事,但容易被当成「文件不存在」处理,反而掩盖了越权尝试。
- 别把
realpath()失败直接当作「文件找不着」返回 404;它更可能是权限拦截信号 - 可加一层日志:记录所有
realpath() === false的原始输入,方便审计可疑请求 - 若业务必须支持软链接或挂载点,考虑用
is_file()+is_readable()替代realpath(),但需配合严格目录前缀判断(见上一节)
上传文件名里的路径字符必须在保存前剥离
用户上传的 $_FILES['file']['name'] 可能是 ../../.env,如果直接用它生成保存路径,就等于开了个后门。
- 永远不要把
$_FILES['file']['name']原样拼进move_uploaded_file()的目标路径 - 用
pathinfo($name, PATHINFO_FILENAME)和pathinfo($name, PATHINFO_EXTENSION)分离名字与扩展名,再拼新文件名 - 强制重命名(如用
uniqid()+ 哈希)比清洗更安全,尤其对头像、附件类场景 - 保存目录权限设为
0750,属主为 web 进程用户,避免其他账户读取
路径校验不是加一层函数调用就完事的事。真实攻击里常混用编码(%2e%2e%2f)、大小写(..\..\windows\win.ini)、空字节(PHPrealpath() 是目前最接近「解题标准答案」的做法,但前提是你的校验逻辑没漏掉开头的斜杠、没忽略配置带来的副作用。



















