路径遍历攻击防御实战:从原理到 Web 应用安全加固

适用场景

本教程适用于 Web 应用开发者和安全运维人员,需要在代码层和服务器层防御路径遍历(Path Traversal,又称目录穿越)攻击的场景。路径遍历漏洞允许攻击者突破 Web 根目录限制,读取服务器上的任意文件(如 /etc/passwd、配置文件、数据库凭据),是 OWASP Top 10 中常见的严重漏洞。

前置条件

  • Web 服务器运行中(Nginx/Apache/OpenResty)
  • 目标应用使用 PHP/Python/Java/Node.js 中的一种
  • 存在文件读取或下载功能(如图片预览、附件下载、日志导出)
  • 文件操作函数的路径参数来自用户输入(GET/POST 参数、Cookie、请求头)

原理说明

路径遍历的原理是利用文件路径中的特殊字符序列绕过应用的文件访问限制。攻击者注入 ../..\ 来向上跳转目录层级,最终访问到 Web 应用预期之外的文件。常见攻击手法包括:

  • 基本遍历../../../etc/passwd
  • URL 编码绕过%2e%2e%2f(即 ../ 的 URL 编码)
  • 双重编码绕过%252e%252e%252f(即 %2e%2e%2f 的二次编码)
  • Unicode 绕过..%c0%af(IIS 旧版本特有的 UTF-8 编码绕过)
  • 空字节截断../../../etc/passwd%00.jpg(旧版 PHP < 5.3.4)

防御的核心原则是:绝不相信用户输入的文件路径,对所有文件操作实施严格的路径白名单验证。

操作步骤

步骤一:在 Nginx 层禁止关键文件访问

即使应用层存在漏洞,Nginx 层的前置拦截可以作为第一道防线:

# 在 server block 中添加
location ~* \.(env|git|svn|swp|bak|sql|log)$ {
    deny all;
    return 403;
}

location ~* /\.(git|svn|hg|idea) {
    deny all;
    return 403;
}

# 禁止访问系统关键文件
location ~* (etc/passwd|etc/shadow|etc/hosts|boot\.ini|win\.ini) {
    deny all;
    return 403;
}

# 限制 PHP 执行目录(防止上传目录的 webshell 执行)
location ~ /uploads/.*\.php$ {
    deny all;
    return 403;
}

将配置添加到 /etc/nginx/conf.d/security.conf,然后 nginx -t && systemctl reload nginx

步骤二:PHP 应用层——规范化后验证基础路径

// 错误做法:直接拼接用户输入
$file = $_GET['file'];
$content = file_get_contents('/var/www/downloads/' . $file);  // ❌ 危险!

// 正确做法:规范化路径后检查前缀
function safeReadFile(string $userInput, string $baseDir): string {
    // 1. 移除空字节和路径遍历序列
    $clean = str_replace(['\0', '%00', "\0"], '', $userInput);

    // 2. 拒绝包含 ../ 或 ..\ 的输入
    if (strpos($clean, '../') !== false || strpos($clean, '..\\') !== false) {
        throw new \Exception('Invalid path');
    }

    // 3. 规范化绝对路径(解析所有 .. 和 .)
    $baseReal = realpath($baseDir);
    $targetReal = realpath($baseDir . '/' . $clean);

    // 4. 关键验证:目标路径必须以基础路径开头
    if ($targetReal === false || strpos($targetReal, $baseReal) !== 0) {
        throw new \Exception('Path traversal detected');
    }

    if (!is_file($targetReal) || !is_readable($targetReal)) {
        throw new \Exception('File not found or not readable');
    }

    return file_get_contents($targetReal);
}

// 使用示例
try {
    $content = safeReadFile($_GET['file'], '/var/www/downloads');
    echo htmlspecialchars($content);
} catch (\Exception $e) {
    http_response_code(403);
    echo 'Access denied';
}

核心逻辑:先用 realpath() 将用户输入 + 基础路径解析为绝对路径,然后验证解析结果是否以基础路径的 realpath 开头。这能防御所有 ../ 变种。

步骤三:Python 应用层——使用 os.path.abspath 验证

import os

BASE_DIR = '/var/www/downloads'

def safe_read_file(user_path: str) -> bytes:
    # 移除路径遍历序列
    user_path = user_path.replace('\0', '').replace('%00', '')

    # 拒绝显式的路径遍历
    if '../' in user_path or '..\\' in user_path:
        raise PermissionError('Invalid path')

    # 构建并规范化完整路径
    full_path = os.path.abspath(os.path.join(BASE_DIR, user_path))

    # 验证目标路径在基础目录内
    if not full_path.startswith(os.path.abspath(BASE_DIR) + os.sep):
        raise PermissionError('Path traversal detected')

    # 验证文件存在且不越界
    if not os.path.isfile(full_path):
        raise FileNotFoundError('File not found')

    with open(full_path, 'rb') as f:
        return f.read()

Python 方案同样遵循规范化 + 前缀检查的原则。os.path.abspath 会自动解析 ..,确保任何路径遍历序列都被展开为实际路径。

步骤四:Java 应用层——使用 CanonicalPath

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class FileSecurity {
    private static final String BASE_DIR = "/var/www/downloads";

    public static byte[] safeReadFile(String userPath) throws IOException {
        // 拒绝显式路径遍历
        if (userPath.contains("../") || userPath.contains("..\\")
            || userPath.contains("\0") || userPath.contains("%00")) {
            throw new SecurityException("Invalid path");
        }

        // 构建路径并获取规范路径
        Path basePath = Paths.get(BASE_DIR).toRealPath();
        Path targetPath = Paths.get(BASE_DIR, userPath).normalize().toRealPath();

        // 验证规范路径以基础路径开头
        if (!targetPath.startsWith(basePath)) {
            throw new SecurityException("Path traversal detected");
        }

        return Files.readAllBytes(targetPath);
    }
}

Java 的 toRealPath()normalize() 会自动解析 .. 和符号链接,是最安全的选择。

步骤五:Nginx 层限制文件下载目录

# 方式一:alias + 目录限制
location /downloads/ {
    alias /var/www/downloads/;
    # 禁止通过 ./.. 跳出
    if ($uri ~* '\.\.') {
        return 403;
    }
}

# 方式二:X-Accel-Redirect 内部重定向(推荐)
# 由 PHP/Python 验证后发起内部重定向
location /protected-files/ {
    internal;  # 只能内部访问,禁止直接外部请求
    alias /var/www/downloads/;
    add_header Content-Disposition 'attachment';
}

X-Accel-Redirect 方案是 Nginx 的最佳实践:应用层只负责权限验证,验证通过后设置 X-Accel-Redirect 请求头,由 Nginx 内部发送文件。这样文件路径永不暴露给用户,且静态文件由 Nginx 直接处理(性能更高)。

步骤六:WAF 层拦截路径遍历攻击

# Nginx + ngx_http_lua_module (OpenResty)
# 在 server 或 http block 中

# 方式一:正则拦截(简单有效)
if ($args ~* "(\.\./|\.\.\\|\.\.%2f|%2e%2e%2f|%252e%252e%252f)") {
    return 403;
}

# 方式二:map 方式(更高效,无 if 块限制)
map $args $block_path_traversal {
    default 0;
    ~*(\.\./|\.\.\\|\.\.%2f|%2e%2e%2f|%252e%252e%252f) 1;
    ~*(etc/passwd|etc/shadow|boot\.ini|win\.ini|windows/win\.ini) 1;
}

server {
    if ($block_path_traversal = 1) {
        return 403;
    }
    # ... 其余配置
}

配置验证

# 基本路径遍历测试
curl -v "https://www.wafai.cn/download?file=../../../etc/passwd"
# 预期:403 Forbidden

# URL 编码绕过测试
curl -v "https://www.wafai.cn/download?file=%2e%2e%2f%2e%2e%2f%2e%2e%2fetc/passwd"
# 预期:403 Forbidden

# 双重编码测试
curl -v "https://www.wafai.cn/download?file=%252e%252e%252f%252e%252e%252fetc/passwd"
# 预期:403 Forbidden(WAF 层和应用层都要拦截)

# 正常文件下载验证
curl -v "https://www.wafai.cn/download?file=report_2026.pdf"
# 预期:正常返回文件内容或下载

常见问题

Q1:为什么用 realpath() 比正则过滤更可靠?

正则过滤难以穷举所有编码变种。攻击者可能使用双重 URL 编码、Unicode 编码、畸形路径(如 ....// 在某些系统中会被解析为 ../)等手段绕过。而 realpath() 将路径交给文件系统去解析,无论攻击者如何编码 ../,规范化后都能被前缀检查拦住。正则应作为WAF 层的辅助防御。

Q2:Windows 服务器如何配置?

Windows 上路径遍历同样使用 ..\,但防御原理相同:

  • IIS 中使用 Request.MapPath() 获取物理路径后做前缀检查
  • ASP.NET 中 Path.GetFullPath() 替代 realpath
  • 注意 Windows 大小写不敏感,C:\WindowsC:\windows 等效
  • 防范 ::$DATA 备用数据流绕过(IIS 6 遗留问题)

Q3:如何防御符号链接(symlink)绕过?

即使路径不包含 ../,攻击者可能通过上传一个指向 /etc 的符号链接文件来绕过。防御方法:

  • 使用 realpath()toRealPath() 解析符号链接为实际路径
  • 在上传时检查文件类型(is_link()lstat()),拒绝符号链接文件
  • 在 Linux 上使用 openat2() 系统调用(内核 5.6+)的 RESOLVE_NO_SYMLINKS 标志

总结

路径遍历漏洞的防御需要多层防护:Nginx 层用正则拦截请求、应用层用规范化路径验证、WAF 层作为补充拦截编码变种。核心原则是”先规范化,再验证前缀”,而非依赖黑名单过滤。建议将所有文件下载功能统一为安全函数,配合 X-Accel-Redirect 内部重定向,实现最优的防护效果。