basename() 单独不能防目录遍历,因其仅截取末段文件名而不规范路径;必须用realpath()展开真实路径并严格校验是否在白名单根目录内,且需配合id映射等纵深防御。

basename() 单独用不能防目录遍历,realpath() + 白名单前缀校验才是关键防线。
为什么 basename() 不能单独拦住目录遍历
它只截掉路径分隔符前的所有内容,对 ../../../etc/passwd 返回 passwd,但如果你拼成 /var/www/uploads/../../../etc/passwd 再传给 file_get_contents(),PHP 照样去读 /etc/passwd。
- 攻击者根本不需要让
basename()失效——只要上游代码还拿原始输入拼路径,就等于开门 -
basename()不处理 URL 编码绕过(如%2e%2e%2f)、空字节截断(%00)、Unicode 归一化等变体 - Windows 下反斜杠
\也可能被解析为路径分隔符,basename()虽能处理,但拼接逻辑若没统一转义,仍可能出问题
realpath() 必须配合 str_starts_with() 或前缀比对
realpath() 会真正展开 ..、.、符号链接,并返回文件系统中真实的绝对路径;如果路径不存在或越权,它直接返回 false —— 这个返回值必须检查。
- PHP 8.0+ 直接用
str_starts_with($real_target, $root_dir) - PHP 7.x 用
substr($real_target, 0, strlen($root_dir)) === $root_dir,别用strpos(),它对false返回 0 会误判 - 必须在调用
file_exists()或is_readable()前完成校验,否则攻击者可利用竞态条件(race condition)在检查后、读取前替换符号链接 -
$root_dir末尾必须带斜杠(如'/var/www/uploads/'),否则'/var/www/uploadsssh_key'会被错误接受
绝对不要用黑名单过滤 .. 或特殊字符
写 str_replace(['..', '%00'], '', $input) 或正则删点点,是典型防御失效模式。绕过方式太多:
-
.//、....//、%2e%2e%2f、.\.\、..%c0%af(UTF-8 overlong)都逃得掉 - 空字节截断在老 PHP 或 misconfigured CGI 模式下仍可能生效,不能依赖
open_basedir或版本默认禁用 - 任何基于“删掉危险字符”的思路,本质都在和攻击者拼编码知识,你永远慢一步
最安全的替代方案:完全不暴露路径
如果业务允许,彻底放弃让用户传文件名。改用 ID 映射或哈希路径:
- 数据库存
id=123 → filename='report_v2_20260410.pdf',用户只传?id=123 - 上传时重命名为
sha256($original_name . $timestamp . $secret) . '.pdf',磁盘路径固定为/safe/uploads/xxx.pdf - Web 服务器层(Nginx/Apache)配置
location ^~ /uploads/只允许访问已知扩展名,且禁止执行脚本
真实项目里,realpath() 校验是底线,但 ID 映射才是免于反复踩坑的省心做法——毕竟路径校验漏一个字符,就可能丢一台服务器。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











