通过php实现强制下载并过滤用户输入路径,核心在于使用 basename() 函数剥离路径中的目录信息,仅保留文件名,从而防止目录遍历攻击。具体做法是获取用户提交的 $_get['file'] 参数,将其传入 basename() 进行清洗,然后结合安全配置的根目录构建完整文件路径。若文件存在且可读,则设置 content-disposition: attachment 响应头并使用 readfile() 输出内容;否则返回错误提示。此方法确保即使用户输入 ../../etc/passwd,最终访问的也仅为名为 passwd 的文件(若存在于当前目录),而非系统敏感文件。

basename() 如何过滤路径分隔符
basename() 函数会移除字符串末尾的路径组件,只返回文件名部分。在PHP中,它默认识别 / 和 \ 作为路径分隔符。例如,输入 /var/www/html/../../etc/passwd,经过 basename() 处理后变为 passwd。这有效阻断了利用 ../ 向上跳转目录的攻击尝试。需要注意的是,basename() 并不验证文件是否存在或是否属于允许下载的目录,因此必须配合白名单或固定根目录使用。

<?php
// 定义允许下载的根目录
$download_root = '/var/www/downloads/';
// 获取用户输入的文件名
$user_input = $_GET['file'] ?? '';
// 过滤路径,只保留文件名
$safe_filename = basename($user_input);
// 构建完整路径
$full_path = $download_root . $safe_filename;
// 检查文件是否存在且为普通文件
if (file_exists($full_path) && is_file($full_path)) {
// 设置响应头,强制下载
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . $safe_filename . '"');
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: ' . filesize($full_path));
// 读取并输出文件
readfile($full_path);
exit;
} else {
http_response_code(404);
echo 'File not found or invalid.';
}
?>
上述代码首先定义了 $download_root 为 /var/www/downloads/。当用户请求 ?file=../../etc/passwd 时,basename() 提取出 passwd,拼接后路径为 /var/www/downloads/passwd。只有该目录下确实存在 passwd 文件时才会下载,否则返回404。这确保了即使文件名被篡改,也无法访问根目录之外的文件。
立即学习“PHP免费学习笔记(深入)”;
如何验证文件是否在允许目录内
仅使用 basename() 还不够,因为如果 $download_root 配置不当或存在符号链接,仍可能越权。最佳实践是使用 realpath() 解析物理路径,并验证其是否以 $download_root 开头。这样可以处理符号链接和相对路径的歧义。例如,若 $download_root 指向 /var/www/downloads,而用户试图通过符号链接访问 /etc,realpath() 会解析到实际位置,从而被前缀检查拦截。

<?php
$download_root = realpath('/var/www/downloads/');
$user_input = $_GET['file'] ?? '';
$safe_filename = basename($user_input);
$full_path = $download_root . DIRECTORY_SEPARATOR . $safe_filename;
// 解析真实路径
$resolved_path = realpath($full_path);
// 验证解析后的路径是否在允许的根目录下
if ($resolved_path !== false && strpos($resolved_path, $download_root) === 0) {
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . $safe_filename . '"');
header('Content-Length: ' . filesize($resolved_path));
readfile($resolved_path);
exit;
} else {
http_response_code(403);
echo 'Access denied.';
}
?>
在此改进版本中,realpath() 将 $full_path 转换为绝对物理路径。接着检查 $resolved_path 是否以 $download_root 开头。如果用户输入包含符号链接指向外部目录,$resolved_path 将不包含 $download_root 前缀,从而触发403禁止访问。这提供了比单纯 basename() 更深层的安全保障,确保所有下载操作严格限制在指定目录树内。
当用户输入包含特殊字符如点号时会发生什么
如果用户输入 .. 或 ...,basename() 会将其视为文件名的一部分,而不是路径组件。例如,输入 .. 经 basename() 处理后仍为 ..。此时,程序会尝试下载名为 .. 的文件。由于文件系统通常不存在名为 .. 的文件,file_exists() 将返回 false,导致404错误。同样,输入 ....txt 会被当作合法文件名,若该文件存在于下载目录,则会被下载。这表明 basename() 不自动过滤非法文件名,需额外校验文件名合法性或使用白名单机制。



















