下载接口 /download.php?file=../../wp-config.php 返回 200,响应体里能看到 DB_NAME 和表前缀。换成 ../../../etc/passwd 一样能读。第一次遇到会往复杂处想,其实漏掉的多是个小前提。

$base = realpath('/var/www/files');
$file = realpath($base . '/' . basename($_GET['file']));
if ($file === false || strpos($file, $base) !== 0) { http_response_code(403); exit; }
readfile($file);

接口把用户传的路径直接拼到基目录后面,没做 realpath 归一化,也没校验结果是否还在允许目录内。两点斜杠一路往上就跳出了限制。这一点先确认清楚,后面就不用反复试。

「目录遍历」相关的验证我通常做两遍,改完一次、隔天再看一次,确认不是临时生效。把这次用到的命令存进笔记,比记住结论有用,换个环境还能照着走。

改完隔一天再看一次,当场正常不代表能稳住。测试环境和线上的差别,往往就在版本号和那几行被忽略的配置上。

线上出问题先对时间点,跟发布、备份、任务执行的时刻排一排,能省掉一半功夫。

具体做法是,先对参数做 basename,把路径分隔符全部去掉;再用 realpath 拼出绝对路径,确认前缀等于允许目录才放行;下载目录放到 web 根之外,nginx 只做内部转发。

改完照着核一遍,带 ../ 的请求返回 403,正常文件名仍然能下载。

作者 9823189