本文目录导读:

防止PHP项目目录遍历漏洞的核心原则是:永远不要直接使用用户输入来拼接文件路径。
目录遍历漏洞(Path Traversal)允许攻击者通过注入 等字符,访问 Web 根目录之外的文件,/etc/passwd 或应用源码。
以下是具体的防御措施,按推荐优先级排列:
不要使用用户输入拼接路径(最根本)
最安全的做法是:不允许用户直接指定文件名或路径。
-
使用 ID 或 Key,而非文件名:
- 错误做法:
download.php?file=report.pdf - 正确做法:
download.php?id=123,在服务器端维护一个映射表(如数据库),将id=123映射到真实路径/var/data/report.pdf。// 安全示例:从数据库获取路径 $allowedFiles = [ 1 => '/var/data/report_2023.pdf', 2 => '/var/data/invoice_001.pdf' ];
$id = $_GET['id'] ?? null; if (isset($allowedFiles[$id])) { $filePath = $allowedFiles[$id]; // 输出文件... } else { exit('Invalid file ID.'); }
- 错误做法:
如果必须使用用户输入,必须进行严格的过滤和校验
如果业务逻辑确实需要用户传入文件名,必须执行以下步骤:
1 禁止或过滤危险字符
使用正则表达式或函数移除或拒绝包含 、、 等路径分隔符的输入。
<?php
// 只允许字母、数字、下划线、点(用于扩展名)
$fileName = $_GET['file'] ?? '';
// 方法一:正则匹配(推荐)
if (!preg_match('/^[a-zA-Z0-9_\.\-]+$/', $fileName)) {
exit('Invalid file name format.');
}
// 方法二:移除危险字符
$fileName = str_replace(['..', './', '../', '/', '\\'], '', $fileName);
// 注意:这种方法不安全!攻击者可以绕过,"....//" 或 "..../"
// 因此更推荐白名单正则。
2 使用 basename() 函数
basename() 可以剥离路径中的所有目录部分,只保留文件名。但请注意:这不能防止文件被包含,只能防止访问上级目录。
<?php $unsafePath = $_GET['file']; // 用户输入: ../../../etc/passwd $safePath = basename($unsafePath); // 结果: passwd // 现在只能访问当前目录下的名为 "passwd" 的文件,而不是 /etc/passwd
3 使用 realpath() 锁定路径范围
realpath() 可以解析路径中的所有 和 ,返回绝对路径,然后检查该路径是否以允许的目录前缀开头。
<?php
$baseDir = '/var/www/html/files/'; // 允许访问的根目录
$userInput = $_GET['file'] ?? '';
// 必须加上真实路径
$fullPath = $baseDir . $userInput;
// 关键:解析符号链接和相对路径
$realPath = realpath($fullPath);
// 检查路径是否有效,并且是否在允许的目录内
if ($realPath === false || strpos($realPath, realpath($baseDir)) !== 0) {
exit('Access denied.');
}
// 安全了!$realPath 一定是 $baseDir 的子路径
readfile($realPath);
缺点:如果文件不存在,realpath() 会返回 false,可以使用 pathinfo() 和 dirname() 组合来处理不存在的文件。
针对 include/require 的特别防御(代码包含)
目录遍历漏洞在 include 和 require 语句中尤其危险,可能导致远程文件包含(RFI) 或本地文件包含(LFI)。
// 极度危险! include($_GET['page']);
防御方法:
-
白名单制:这是唯一可靠的方法。
<?php $allowedPages = ['home', 'about', 'contact']; $page = $_GET['page'] ?? 'home'; if (in_array($page, $allowedPages)) { include(__DIR__ . '/pages/' . $page . '.php'); } else { include(__DIR__ . '/pages/404.php'); } -
禁用远程文件包含:在
php.ini中设置allow_url_include = Off。 -
禁用
allow_url_fopen:如果不需要,可以设置为Off。
服务器配置层面的防御
1 禁用不必要的文件函数
在 php.ini 或 disable_functions 中禁用可能会被利用的函数:
disable_functions = exec,passthru,shell_exec,system,popen,proc_open,curl_exec,curl_multi_exec,parse_ini_file,show_source,file_get_contents,fopen,fread,fgets
注意:这会影响到正常功能,需要根据项目需求谨慎设置。
2 配置 open_basedir
open_basedir 是 PHP 最有效的目录限制配置之一,它会限制 PHP 脚本只能访问指定目录及其子目录。
open_basedir = "/var/www/html:/tmp"
- 设置后,任何试图访问
/etc/passwd的操作都会被 PHP 直接拒绝。 - 优点:全局生效,无需修改代码。
- 缺点:可能会影响某些需要访问系统临时目录的第三方库(可以添加
/tmp例外)。
3 Web 服务器配置
- Apache:使用
.htaccess或虚拟主机配置:<Directory /var/www/html/files> Options -Indexes AllowOverride None <FilesMatch "\.(php|php5|phtml)$"> Order Deny,Allow Deny from all </FilesMatch> </Directory> - Nginx:使用
alias指令时需特别小心,不要在alias末尾加斜杠。
最佳实践清单
| 场景 | 推荐做法 | 权重 |
|---|---|---|
| 通用原则 | 永远不要信任用户输入。 | 必备 |
| 文件下载/读取 | 使用 ID 映射 + 白名单。 | 首选 |
| 绝对路径锁定 | 使用 realpath() + strpos() 检查路径前缀。 |
很强 |
| 文件名过滤 | 使用 白名单正则(如 [a-zA-Z0-9_\-]+)。 |
基础 |
| 包含文件 | 仅使用 白名单制。 | 唯一 |
| 服务器配置 | 开启 open_basedir。 |
强烈推荐 |
| 防御性编码 | 所有文件操作函数前,先进行路径校验。 | 习惯 |
一句话记住:与其试图过滤掉 ,不如根本不接受路径,转而使用 ID 或 Key,这是最彻底的解决方案。