应使用 basename() 提取文件名并配合 realpath() 和路径前缀校验,确保用户输入不突破白名单目录;禁用用户控制的扩展名和目录结构,web 服务器层需协同设防。

用 basename() 提取文件名,别信用户给的完整路径
用户传过来的 filename=../../etc/passwd,你直接拼到 file_get_contents('/var/www/uploads/' . $_GET['filename']) 里,等于主动打开服务器大门。核心原则是:**永远不把原始输入当路径一部分用**。
最简单有效的做法,是用 basename() 强制只保留最后一级文件名:
$user_input = '../../etc/passwd'; $safe_name = basename($user_input); // 结果是 'passwd',不是 '../etc/passwd' // 再拼接白名单目录 $full_path = '/var/www/uploads/' . $safe_name;
-
basename()会剥离所有斜杠和上级路径,只留合法文件名,对 Windows 和 Linux 都有效 - 注意它不处理空字节
%00或 Unicode 归一化绕过,所以必须配合后续校验 - 如果业务允许中文或特殊符号,
basename()本身没问题,但你要确保后续存储/读取也支持
校验路径是否落在预期目录内,用 realpath() + str_starts_with()
光截文件名不够——攻击者可能上传一个叫 .htaccess 的文件,再通过其他方式触发解析;或者你本意只读 /data/docs/,但用户构造 filename=normal.pdf%00.jpg 绕过字符串匹配。
正确做法是:拼出完整路径 → 解析真实绝对路径 → 检查是否在白名单根目录下:
$root_dir = '/var/www/uploads/';
$user_file = $_GET['filename'] ?? '';
$target = $root_dir . $user_file;
$real_target = realpath($target);
if ($real_target === false || !str_starts_with($real_target, $root_dir)) {
http_response_code(403);
exit('Access denied');
}
-
realpath()会自动展开..、.、符号链接,并返回真实路径;如果路径不存在或越权,返回false -
str_starts_with()是 PHP 8.0+ 原生函数;PHP 7.x 可用substr($real_target, 0, strlen($root_dir)) === $root_dir - 这个检查必须在
open_basedir之外做,因为realpath()失败时不会抛异常,而是静默返回false
禁止用户控制扩展名或目录结构,用白名单而非黑名单
有人写 if (strpos($file, '..') !== false || strpos($file, "\0") !== false) —— 这种黑名单校验注定失败。攻击向量太多:%2e%2e、....//、.\.\、././、UTF-8 双字节点等。
- 扩展名必须由你决定,比如固定为
.pdf,不要从用户输入里取filename=report.php.jpg - 目录层级必须硬编码,例如只允许
/uploads/reports/和/uploads/avatars/,不接受用户传dir=reports动态拼接 - 如果真要支持多目录,用映射表:
$dirs = ['invoice' => '/data/invoices/', 'log' => '/data/logs/'];,再校验键是否存在 -
pathinfo()提取扩展名时,务必配合realpath()后再调用,否则可能被空字节截断影响判断
Web 服务器层也要设防,别全指望 PHP
PHP 层漏了,还有 Nginx/Apache 拦一下。这不是“多此一举”,而是纵深防御的实际需要。
- Nginx 中加
location ~ \.\.\/ { return 403; },能挡掉明显含../的请求,但不能替代 PHP 层校验 - Apache 的
mod_security规则可以拦截常见路径穿越模式,但规则更新滞后,且可能误伤正常请求 - 关键配置如
open_basedir要设得够紧,比如open_basedir = "/var/www/uploads:/tmp",但注意它不影响realpath()行为,只是限制 PHP 文件操作范围 - 上传目录务必禁止执行权限,Nginx 配置里加
location ~ \.(php|sh|pl|py)$ { deny all; }
真正麻烦的是那些看似合法、实则经过多次编码或利用函数行为差异的路径,比如 urldecode(rawurldecode(...)) 后才暴露真实路径。这种地方,realpath() 是目前最可靠的守门人,但前提是它前面没被绕过。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











