4. 路径遍历漏洞的规范化与白名单校验

路径遍历漏洞的规范化与白名单校验 学习卡片

漏洞场景:文件下载接口的路径拼接

下面是一段典型的文件下载功能代码,它把用户提交的 filename 参数直接拼接到下载根目录之后,再交给文件读取函数:

import os
from flask import Flask, request, send_file

app = Flask(__name__)
DOWNLOAD_ROOT = "/var/www/app/downloads"

@app.route("/download")
def download():
    filename = request.args.get("filename", "")
    # 危险:未做任何校验,直接拼接
    filepath = os.path.join(DOWNLOAD_ROOT, filename)
    return send_file(filepath)

当请求为 /download?filename=../../../../etc/passwd 时,os.path.join 会保留 .. 片段,最终路径变成 /var/www/app/downloads/../../../../etc/passwd,操作系统解析后实际指向 /etc/passwd,敏感文件被读取并返回给攻击者。这就是路径遍历(Path Traversal)漏洞。

注意:os.path.join 并不会自动消除 ..,它只负责拼接。若第二个参数以 / 开头,还会直接丢弃前面的根目录,例如 filename=/etc/shadow 会得到 /etc/shadow。

攻击路径的解析过程

理解漏洞的关键在于看清“拼接字符串”与“操作系统最终解析路径”之间的差异。下图展示了从用户输入到文件系统实际访问目标的完整链路:

路径遍历:从用户输入到实际文件访问 用户输入 filename ../../../../etc/passwd os.path.join 拼接 /var/www/app/downloads/../../../../etc/passwd 操作系统路径解析 .. 逐级回退 → /etc/passwd 敏感文件被读取 /etc/passwd 内容返回给攻击者 修复:规范化 + 白名单双重校验 ① realpath 规范化 消除 .. 与符号链接 ② 前缀白名单校验 必须位于 DOWNLOAD_ROOT 内 ③ 拒绝非法请求 返回 403,不泄露路径信息

修复实现:规范化与白名单校验

修复的核心思路是:先把用户输入解析为绝对路径并消除所有 .. 和符号链接,再判断该绝对路径是否严格位于允许的下载根目录之下。只有同时满足这两个条件才允许读取文件。

import os
from flask import Flask, request, send_file, abort

app = Flask(__name__)
DOWNLOAD_ROOT = os.path.realpath("/var/www/app/downloads")

def safe_download_path(filename: str) -> str:
    # 1. 拒绝空值和包含空字节的输入
    if not filename or "\x00" in filename:
        abort(400)

    # 2. 拼接后做规范化,消除 .. 和符号链接
    candidate = os.path.realpath(os.path.join(DOWNLOAD_ROOT, filename))

    # 3. 白名单校验:规范化后的路径必须以 DOWNLOAD_ROOT 为前缀
    #    使用 os.path.commonpath 避免前缀字符串误判(如 /downloads_evil)
    if os.path.commonpath([DOWNLOAD_ROOT, candidate]) != DOWNLOAD_ROOT:
        abort(403)

    # 4. 额外确认目标确实是普通文件
    if not os.path.isfile(candidate):
        abort(404)

    return candidate

@app.route("/download")
def download():
    filename = request.args.get("filename", "")
    filepath = safe_download_path(filename)
    return send_file(filepath)

关键校验点说明

校验步骤作用绕过风险
os.path.realpath 将路径转为绝对路径,解析 .. 和符号链接 若在拼接前调用,.. 仍可能逃逸
os.path.commonpath 判断两个路径的公共前缀是否等于根目录 比字符串 startswith 更安全,避免 /downloads_evil 误判
空字节检查 阻止 filename=../../etc/passwd%00.png 类截断攻击 部分旧版语言运行时可能截断,需显式拒绝
os.path.isfile 确保目标是普通文件而非目录或设备文件 防止读取 /dev/zero 等特殊文件
如果业务允许的文件名是固定集合,更推荐直接使用文件名白名单:ALLOWED = {"report.pdf", "manual.zip"},请求参数必须精确匹配集合中的元素,从根本上消除路径拼接。

验证方法

修复后可用以下请求验证防护是否生效:

  • /download?filename=../../../../etc/passwd → 应返回 403,不返回文件内容。
  • /download?filename=/etc/shadow → 应返回 403,因为规范化后不在下载根目录内。
  • /download?filename=report.pdf → 应正常返回文件,确认合法请求未被误伤。
  • /download?filename=..%2f..%2fetc%2fpasswd → URL 解码后仍应被拦截。

验证时重点观察响应状态码和响应体:合法请求返回 200 与文件内容,非法请求返回 403 且响应体中不包含任何系统文件内容或路径信息。