必须用 realpath() + 白名单根目录前缀校验防目录遍历,因 basename() 无法处理 url 编码、unicode 全角点及符号链接绕过;realpath() 需检查返回 false、严格前缀匹配且根目录末尾带斜杠。

直接结论:仅用 basename() 或简单字符串替换 ../ 不足以防目录遍历,必须配合 realpath() + 白名单根目录前缀校验,且要检查 realpath() 返回值是否为 false。
为什么 basename() 单独用不安全
攻击者传 ../../etc/passwd,basename() 确实能截成 passwd;但若传的是 %2e%2e%2fetc%2fpasswd(URL 编码的 ../etc/passwd),basename() 默认不 decode,会原样返回,拼接后仍是越权路径。更隐蔽的是 Unicode 全角点(如 ../etc/passwd)或符号链接绕过 —— 这些都逃得过 basename()。
实操建议:
- 接收参数后立刻
urldecode()再做basename(),或统一用rawurldecode()处理所有 GET/POST 输入 -
basename()后仍需白名单校验文件名:只允许[a-zA-Z0-9_-],拒绝点号、斜杠、空字节 - 别信
$_FILES['file']['name']里的任何路径成分,它本就不该含路径
realpath() 校验必须做的三件事
realpath() 是防遍历的核心,但它不是“一调就安全”的银弹 —— 它的行为和返回值容易被忽略。
实操建议:
- 必须检查返回值是否为
false:realpath('/nonexistent/../etc/passwd')返回false,不检查就会跳过后续校验 - 比对根目录要用严格前缀匹配:
strpos($real_path, $root_dir) === 0(PHP 7.x)或str_starts_with($real_path, $root_dir)(PHP 8.0+),不能用==或=== false判定起始位置 -
$root_dir必须是真实绝对路径,且末尾带斜杠:/var/www/uploads/,否则/var/www/uploadssomething会被误判为合法
上传文件时的特殊处理要点
上传场景下,move_uploaded_file() 虽有基础防护,但目标路径若构造不当,依然可被遍历利用。
实操建议:
- 永远用
move_uploaded_file($_FILES['file']['tmp_name'], $safe_target),绝不用copy()或file_put_contents() -
$safe_target构造顺序必须是:basename()→ 白名单校验 → 拼到根目录 →realpath()→ 前缀校验 → 才调用move_uploaded_file() - 上传目录在 Web 服务器上应禁用 PHP 解析(如 Nginx 配置
location ~ \.php$ { deny all; }),防止上传 Webshell 后被执行
容易被忽略的符号链接陷阱
realpath() 会解析符号链接,这本是优点,但也带来新风险:攻击者可能提前在授权目录下创建指向系统关键路径的软链,让 realpath() “合法地”跳出白名单范围。
实操建议:
- 若业务允许符号链接,需额外用
is_link($real_path)检查,发现即拒绝 - 更稳妥的做法是:上传/读取前先
chdir($root_dir),再用相对路径操作,避免依赖外部路径解析逻辑 - Web 服务器层也应限制符号链接访问(如 Apache 的
Options -FollowSymLinks)
真正麻烦的不是写几行校验代码,而是每个文件操作入口都要重复这套逻辑 —— 尤其是路由分发、图片代理、下载接口这些看似简单的脚本,最容易漏掉 realpath() === false 这一行判断。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











