跳转到帖子

背景

目录穿越漏洞属于 Web 安全里比较基础但长期高发的一类问题。它的触发条件通常并不复杂:后端根据用户传入的文件名或路径读取本地文件,但没有对路径进行可靠约束,导致攻击者可以通过 ../、编码变形或路径拼接问题访问预期目录之外的文件。

这类问题在文件下载、日志查看、模板预览、静态资源代理、附件读取等功能中比较常见。本文主要从防守和审计角度整理一次典型目录穿越问题的复现思路、代码审计要点和修复建议,不涉及未授权攻击目标。

建议所有复现都放在本地测试环境、靶场或已授权系统中进行。目录穿越虽然看起来简单,但一旦读取到配置文件、密钥、源码或日志,影响面通常会快速扩大。

问题场景

假设某系统提供一个文件下载接口,用于下载用户上传后的附件。接口大致如下:

GET /download?file=report.pdf

后端逻辑是从固定目录读取文件,例如:

/data/app/uploads/report.pdf

如果开发者直接把用户参数拼接到文件路径中,并交给文件读取函数处理,就可能出现目录穿越风险。

String baseDir = "/data/app/uploads/";
String file = request.getParameter("file");
File target = new File(baseDir + file);
return readFile(target);

这段代码的问题不在于读取文件本身,而在于 file 完全由用户控制,且没有做路径归一化后的边界校验。

技术分析

目录穿越的核心是利用文件系统路径解析规则,跳出业务预期目录。例如在类 Unix 系统中,../ 表示上级目录。如果后端把参数直接拼接为文件路径,就可能得到类似下面的结果:

/data/app/uploads/../../config/application.yml

经过文件系统解析后,实际访问路径可能变成:

/data/config/application.yml

如果该文件存在且进程有读取权限,接口就可能把不该暴露的内容返回给用户。

实际审计时,需要注意目录穿越并不只表现为明文 ../。常见变形包括:

  • URL 编码:..%2f、%2e%2e%2f
  • 双重编码:%252e%252e%252f
  • 混合分隔符:Windows 下的 ..\
  • 绝对路径输入:如 /etc/passwd 或 C:\Windows\win.ini
  • 路径中夹杂无效过滤绕过片段:如简单替换 ../ 后产生新的路径关系

不过在实际修复中,不建议依赖黑名单过滤这些字符串。原因很简单:路径语义最终由操作系统和运行时解析,黑名单很难覆盖各种编码、符号链接、大小写和平台差异。

本地复现环境

为了便于说明,可以用一个最小化的 Python 示例模拟漏洞。以下代码只建议放在本地测试目录中运行。

from flask import Flask, request, send_file
import os

app = Flask(__name__)
BASE_DIR = "/tmp/demo/uploads"

@app.route("/download")
def download():
    filename = request.args.get("file", "")
    path = os.path.join(BASE_DIR, filename)
    return send_file(path)

if __name__ == "__main__":
    app.run(host="127.0.0.1", port=5000, debug=False)

准备测试文件:

mkdir -p /tmp/demo/uploads
mkdir -p /tmp/demo/config
echo "normal file" > /tmp/demo/uploads/report.txt
echo "secret_key=local-test-only" > /tmp/demo/config/app.conf

正常访问:

curl " " Flask 或 send_file 本身,而是传入的路径已经被用户控制且没有做安全边界检查。

代码审计关注点

审计目录穿越漏洞时,我通常会先全局搜索文件读取相关函数,再反向追踪参数来源。不同语言的关键点如下。

语言/框架常见关注函数或场景
Javanew File()、FileInputStream、Files.readAllBytes、ResourceUtils
PHPfile_get_contents、readfile、fopen、模板包含
Pythonopen、send_file、send_from_directory、日志读取接口
Node.jsfs.readFile、fs.createReadStream、静态资源代理
Goos.Open、http.ServeFile、ioutil.ReadFile

重点看三个问题:

  • 文件名或路径是否来自用户输入,例如 query、body、header、cookie、数据库中可被用户写入的字段。
  • 是否直接拼接路径,例如 baseDir + filename 或 path.join(baseDir, filename) 后未校验。
  • 是否只做了字符串黑名单过滤,而没有做规范化路径后的目录边界判断。

有些项目会误以为使用 path.join、Paths.get 就安全了。实际上这些函数只是帮助拼接或解析路径,不等于安全校验。真正关键的是:归一化后的最终路径必须仍然位于允许访问的根目录之下。

推荐修复方式

比较稳妥的修复思路是白名单优先,其次才是路径规范化加边界校验。

方式一:使用文件 ID 映射

最推荐的方式是不要让用户直接传文件路径,而是传业务层的文件 ID。后端根据 ID 查询数据库中的文件记录,再拼接服务端可信文件名。

GET /download?id=12345

数据库中保存类似信息:

id: 12345
owner_id: 10001
storage_name: 7f3a2d9c-9b11-4e1a.pdf
original_name: report.pdf

这样用户无法直接控制磁盘路径,同时还能做权限校验,例如文件归属、租户隔离、下载次数限制等。

方式二:规范化路径并校验边界

如果业务确实需要传文件名,也应只允许文件名,不允许路径分隔符。下面是一个 Python 修复示例:

from flask import Flask, request, abort, send_file
from pathlib import Path

app = Flask(__name__)
BASE_DIR = Path("/tmp/demo/uploads").resolve()

@app.route("/download")
def download():
    filename = request.args.get("file", "")

    # 只接受简单文件名,拒绝包含目录结构的输入
    if filename != Path(filename).name:
        abort(400)

    target = (BASE_DIR / filename).resolve()

    # 确认最终路径仍在 BASE_DIR 内
    if BASE_DIR not in target.parents and target != BASE_DIR:
        abort(403)

    if not target.is_file():
        abort(404)

    return send_file(target)

Java 中可以使用 Path.normalize() 和 startsWith() 做类似处理:

Path baseDir = Paths.get("/data/app/uploads").toRealPath();
String filename = request.getParameter("file");

Path target = baseDir.resolve(filename).normalize();

if (!target.startsWith(baseDir)) {
    throw new SecurityException("invalid path");
}

if (!Files.isRegularFile(target)) {
    throw new FileNotFoundException();
}

byte[] data = Files.readAllBytes(target);

需要注意,存在符号链接时,单纯 normalize() 可能不够。对安全要求较高的场景,建议结合 toRealPath() 获取真实路径后再判断,避免符号链接把访问引到允许目录之外。

测试与验证

修复后建议至少覆盖以下测试用例:

  • 正常文件名:report.txt
  • 上级目录:../config/app.conf
  • 多级上跳:../../../../etc/passwd
  • URL 编码路径:..%2fconfig%2fapp.conf
  • 反斜杠路径:..\config\app.conf
  • 绝对路径:/tmp/demo/config/app.conf
  • 不存在文件:notfound.txt
  • 目录名而非文件:.

可以用 curl 进行本地验证:

curl -i "
curl -i "
curl -i " 200,越界路径返回 400 或 403,不存在文件返回 404。错误响应中不要返回服务器真实路径,避免信息泄露。

日志与应急排查

如果线上发现疑似目录穿越探测,可以从访问日志中检索以下特征:

../
..\
%2e%2e
%252e%252e
/etc/passwd
win.ini
application.yml
.env

Nginx 日志中可先做粗略检索:

grep -Ei "(\.\./|\.\.\\|%2e%2e|%252e%252e|/etc/passwd|win\.ini|\.env)" access.log

排查时建议关注:

  • 是否有成功返回 200 的异常请求。
  • 同一 IP 是否批量尝试不同敏感路径。
  • 接口是否返回了配置、密钥、源码、日志等敏感内容。
  • 应用进程权限是否过大,是否可读取非业务目录。
  • 是否存在后续利用迹象,例如使用泄露配置访问数据库、对象存储或管理后台。

如果确认敏感文件被读取,应按泄露处理:轮换密钥、检查访问凭证、审计数据库和对象存储访问记录,而不是只修一个路径校验。

常见误区

  • 只替换一次 ../:容易被编码、嵌套或平台差异绕过。
  • 只判断字符串是否包含某些敏感文件名:无法覆盖所有敏感路径。
  • 以为上传目录不可执行就安全:目录穿越关注的是读取,不是执行。
  • 忽略符号链接:允许目录内的软链接可能指向目录外。
  • 错误信息返回绝对路径:会降低后续攻击成本,也影响资产暴露面。

总结

目录穿越漏洞的修复原则并不复杂:不要信任用户传入的路径,不要直接拼接文件系统路径,优先使用文件 ID 映射;确需使用文件名时,要做规范化后的边界校验,并结合权限控制和最小化进程权限。

从代码审计角度看,文件读取、文件下载、日志查看、模板预览和静态资源代理都是重点区域。实际项目里,这类漏洞往往不是单点问题,而是和配置泄露、凭证复用、权限过大组合产生更高风险。把路径处理收敛到统一工具函数,并配套单元测试,是比较可持续的治理方式。

代码审计与漏洞定位示意图
代码审计、调用链与关键函数定位示意

0篇意见

推荐意见

没有意见。

游客
抱歉,你的帖子内容包括我们不允许的字词。请编辑你的帖子,删除下面高亮的屏蔽字。
添加意见…

SiteMap HACKHAT © 2026 All rights reserved.