必须用is_link()检查符号链接并跳过,禁用is_dir()/scandir()直接遍历;realpath()校验需防open_basedir干扰并记录异常;白名单比对须用str_starts_with()或substr(),root_dir末尾带斜杠;优先使用recursivedirectoryiterator并关闭follow_symlinks;web服务器层需禁用符号链接解析。

递归遍历前必须用 is_link() 检查符号链接
直接调用 is_dir() 或 scandir() 会无差别进入符号链接指向的目标目录,攻击者可借此跳出白名单范围。必须在每次进入子项前显式判断:is_link($fullPath) 返回 true 就该跳过或拒绝。
-
is_link()不跟随链接,只判断当前路径是否为符号链接本身 - 别依赖
file_exists()或is_file()做判断——它们默认跟随链接,等于主动帮攻击者跳转 - 若业务允许部分软链(如挂载的上传卷),需额外用
readlink()获取目标路径,并用realpath()+ 白名单前缀校验(见下节)
realpath() 校验路径时要防 open_basedir 干扰
realpath() 在遇到 open_basedir 限制时会返回 false,但这不等于路径不存在——很可能是越权尝试被 PHP 内核拦截了。直接当成 404 处理,反而掩盖攻击行为。
- 所有
realpath()返回false的请求,必须记录原始输入到审计日志,不能静默忽略 - 白名单比对必须用
str_starts_with($real_target, $root_dir)(PHP 8.0+)或substr($real_target, 0, strlen($root_dir)) === $root_dir(PHP 7.x),禁用strpos()——它对 false 返回 0,导致误放行 -
$root_dir末尾必须带斜杠,比如'/var/www/uploads/',否则'/var/www/uploadsssh_key'会被错误接受
用 RecursiveDirectoryIterator 替代手写递归更安全
原生迭代器自带 follow_symlinks = false 控制,比手动过滤 .、.. 和 is_link() 更可靠,且天然规避因拼接错误导致的路径失效问题。
- 初始化时传入
RecursiveDirectoryIterator::SKIP_DOTS可自动跳过 . 和 .. - 构造
RecursiveIteratorIterator时设置RecursiveIteratorIterator::CATCH_GET_CHILD,让异常(如权限不足)不中断整个遍历 - 注意:
RecursiveDirectoryIterator默认仍会解析符号链接,必须显式关闭:new RecursiveDirectoryIterator($path, FilesystemIterator::FOLLOW_SYMLINKS | FilesystemIterator::SKIP_DOTS)→ 改为去掉FOLLOW_SYMLINKS
Web 服务层也要禁用符号链接解析
PHP 层校验再严,如果 Nginx 或 Apache 允许解析符号链接,攻击者仍可能通过 URL 直接访问软链目标。这是最后一道防线,不能只靠 PHP。
- Nginx 配置中加
disable_symlinks on from=/www/wwwroot/example.com;,放在server或location块内 - Apache 启用
Options -FollowSymLinks并禁用AllowOverride,防止 .htaccess 覆盖 - Linux 内核级可考虑挂载时加
nosymfollow(5.12+ kernel),但兼容性有限,优先走 Web 服务配置
open_basedir 对 realpath() 的干扰,还有人以为 basename() 能防遍历——它连路径都没展开,根本拦不住 ../../../etc/passwd 这种输入。php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











