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。攻击路径的解析过程
理解漏洞的关键在于看清“拼接字符串”与“操作系统最终解析路径”之间的差异。下图展示了从用户输入到文件系统实际访问目标的完整链路:
修复实现:规范化与白名单校验
修复的核心思路是:先把用户输入解析为绝对路径并消除所有 .. 和符号链接,再判断该绝对路径是否严格位于允许的下载根目录之下。只有同时满足这两个条件才允许读取文件。
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 且响应体中不包含任何系统文件内容或路径信息。