PHP项目如何防止目录遍历漏洞

wen PHP项目 32

本文目录导读:

PHP项目如何防止目录遍历漏洞

  1. 不要使用用户输入拼接路径(最根本)
  2. 如果必须使用用户输入,必须进行严格的过滤和校验
  3. 针对 include/require 的特别防御(代码包含)
  4. 服务器配置层面的防御
  5. 最佳实践清单

防止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 的特别防御(代码包含)

目录遍历漏洞在 includerequire 语句中尤其危险,可能导致远程文件包含(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.inidisable_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 强烈推荐
防御性编码 所有文件操作函数前,先进行路径校验。 习惯

一句话记住:与其试图过滤掉 ,不如根本不接受路径,转而使用 IDKey,这是最彻底的解决方案。

抱歉,评论功能暂时关闭!