mcpField
S
sast-pathtraversal

sast-pathtraversal

github.com/utkusen安全1.3k75 次安装5 天前更新
代码仓库

关于 sast-pathtraversal

用于代码库安全评估的路径遍历检测技能,面向检查文件访问逻辑的开发者。它先查找动态路径文件操作,再分批追踪用户输入并检查路径约束,最后汇总结果。

主要功能

  1. 01查找动态路径的文件读取与服务操作
  2. 02按批次追踪用户输入来源
  3. 03检查规范化、目录限制及文件名白名单
  4. 04合并验证结果并跳过无候选项分析

适用场景

  1. 01检查下载接口是否允许越界读取文件
  2. 02审查文件包含与文件服务逻辑
  3. 03检查 ZIP 或 TAR 解压路径是否受限

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/utkusen/sast-skills --skill sast-pathtraversal

安装后在 Claude Code 中输入/sast-pathtraversal确认已加载。

详细介绍

路径遍历检测

你正在对代码库进行有针对性的安全评估,以查找路径遍历漏洞。此技能采用由子 Agent 执行的三阶段方法:侦察(查找使用动态路径的文件加载接收点)、分批验证(追踪用户输入,并以每批 3 个的方式并行检查缓解措施),以及合并(将各批结果整合为一份报告)。

前提条件:sast/architecture.md 必须存在。如果不存在,请先运行分析技能。


什么是路径遍历

路径遍历(也称为目录遍历)是指将用户提供的输入加入文件路径,并用该路径从文件系统读取、写入或提供文件,却未能将最终路径限制在预期的基础目录内。攻击者可以提供 ../ 之类的序列或其编码形式(%2e%2e%2f、..%2f、%2e%2e/),以逃出预期目录并访问任意文件,例如 /etc/passwd、应用程序源代码、凭据或私钥。

核心模式:未经验证的用户输入到达文件系统操作,且未验证解析后的路径是否仍位于预期基础目录内。

什么情况属于路径遍历
  • 直接从基础目录提供用户请求的文件名,未先规范化并检查最终路径: open(os.path.join(BASE_DIR, user_filename))
  • 从 URL 参数构造文件路径,并将其传递给文件读取函数: fs.readFile(path.join(__dirname, req.query.file), ...)
  • 由用户输入驱动的模板渲染或 include 指令: include($_GET['page'] . '.php')
  • 存档解压(ZipFile、tarfile、zipslip),将条目名称用作输出路径,却未移除其中的 ../ 组成部分
  • 将未经清理的用户控制路径用于 send_file() / send_from_directory() / res.sendFile()
  • 读取路径来源于用户控制的数据库值,且该值存储时未经过清理的文件
什么情况不属于路径遍历

不要将以下情况标记为路径遍历:

  • SSRF:根据用户输入获取远程 URL——这是服务器端请求伪造,属于另一类问题
  • 通过文件写入实现 RCE:将攻击者控制的内容写入任意路径——这与路径遍历相关,但影响类别不同(应标记为 RCE 或文件上传)
  • 静态文件服务:提供的文件路径完全硬编码,不受用户影响
  • 安全的路径拼接后接 realpath 和前缀检查:代码计算 realpath(),并验证其以预期的基础目录开头
  • 拼接前调用 basename():只使用文件名部分会移除路径遍历序列(但请注意,这只能阻止选择目录,并不只是阻止路径遍历)
可防止路径遍历的模式

当你看到以下缓解措施在文件操作之前应用时,代码通常不存在漏洞:

1. realpath / resolve 后进行基础目录前缀检查(最稳健的修复方法)

# Python
import os
BASE = '/var/www/files'
safe_path = os.path.realpath(os.path.join(BASE, user_input))
if not safe_path.startswith(BASE + os.sep):
    raise PermissionError("Path escape detected")
with open(safe_path) as f:
    ...
// Node.js
const BASE = path.resolve('/var/www/files');
const resolved = path.resolve(BASE, req.query.file);
if (!resolved.startsWith(BASE + path.sep)) {
    return res.status(403).send('Forbidden');
}
fs.readFile(resolved, ...);
// Java
Path base = Paths.get("/var/www/files").toRealPath();
Path resolved = base.resolve(userInput).normalize();
if (!resolved.startsWith(base)) {
    throw new SecurityException("Path escape");
}
Files.readAllBytes(resolved);

2. 使用 basename() / path.basename() 移除目录组成部分

# Python — strips all directory parts, only the filename remains
filename = os.path.basename(user_input)
with open(os.path.join(BASE, filename)) as f:
    ...
// PHP
$filename = basename($_GET['file']);
readfile('/var/www/uploads/' . $filename);

3. 允许的文件名或扩展名白名单

ALLOWED = {'report.pdf', 'manual.txt', 'logo.png'}
if user_input not in ALLOWED:
    abort(400)
with open(os.path.join(BASE, user_input)) as f:
    ...

4. 框架提供的安全文件服务

# Flask — send_from_directory validates the path stays within the directory
return send_from_directory('/var/www/files', filename)

# Django — FileResponse with a path that was never user-controlled

易受攻击与安全示例

Python — Flask
# VULNERABLE: user-controlled filename joined without realpath check
@app.route('/download')
def download():
    filename = request.args.get('file')
    filepath = os.path.join('/var/www/files', filename)
    return send_file(filepath)

# SECURE: resolve and verify the path stays within the base directory
@app.route('/download')
def download():
    filename = request.args.get('file')
    base = os.path.realpath('/var/www/files')
    filepath = os.path.realpath(os.path.join(base, filename))
    if not filepath.startswith(base + os.sep):
        abort(403)
    return send_file(filepath)
Python — FastAPI
# VULNERABLE: path parameter used directly in file read
@app.get('/file/{name}')
async def get_file(name: str):
    return FileResponse(f'/app/static/{name}')

# SECURE: basename strips traversal sequences
@app.get('/file/{name}')
async def get_file(name: str):
    safe_name = os.path.basename(name)
    return FileResponse(os.path.join('/app/static', safe_name))
Node.js — Express
// VULNERABLE: req.query.file used directly in readFile
app.get('/file', (req, res) => {
  const filePath = path.join(__dirname, 'uploads', req.query.file);
  fs.readFile(filePath, (err, data) => res.send(data));
});

// SECURE: resolve and check prefix
app.get('/file', (req, res) => {
  const base = path.resolve(__dirname, 'uploads');
  const filePath = path.resolve(base, req.query.file);
  if (!filePath.startsWith(base + path.sep)) {
    return res.status(403).send('Forbidden');
  }
  fs.readFile(filePath, (err, data) => res.send(data));
});
PHP
// VULNERABLE: direct inclusion of user input
<?php
$page = $_GET['page'];
include($page . '.php');

// VULNERABLE: readfile with unsanitized path
$file = $_GET['file'];
readfile('/var/www/uploads/' . $file);

// SECURE: basename strips directory components
$file = basename($_GET['file']);
readfile('/var/www/uploads/' . $file);

// SECURE: realpath + prefix check
$base = realpath('/var/www/uploads');
$path = realpath($base . '/' . $_GET['file']);
if ($path === false || strpos($path, $base . DIRECTORY_SEPARATOR) !== 0) {
    http_response_code(403);
    exit;
}
readfile($path);
Ruby on Rails
# VULNERABLE: params[:file] used directly in file read
def show
  file_path = Rails.root.join('public', 'reports', params[:file])
  send_file file_path
end

# SECURE: basename only
def show
  safe_name = File.basename(params[:file])
  send_file Rails.root.join('public', 'reports', safe_name)
end
Java — Spring
// VULNERABLE: path variable used directly to read file
@GetMapping("/file/{name}")
public ResponseEntity<Resource> getFile(@PathVariable String name) throws IOException {
    Path filePath = Paths.get("/var/www/files").resolve(name);
    Resource resource = new UrlResource(filePath.toUri());
    return ResponseEntity.ok(resource);
}

// SECURE: normalize and check prefix
@GetMapping("/file/{name}")
public ResponseEntity<Resource> getFile(@PathVariable String name) throws IOException {
    Path base = Paths.get("/var/www/files").toRealPath();
    Path resolved = base.resolve(name).normalize();
    if (!resolved.startsWith(base)) {
        return ResponseEntity.status(403).build();
    }
    Resource resource = new UrlResource(resolved.toUri());
    return ResponseEntity.ok(resource);
}
Go
// VULNERABLE: query param joined directly to base directory
func fileHandler(w http.ResponseWriter, r *http.Request) {
    name := r.URL.Query().Get("file")
    http.ServeFile(w, r, filepath.Join("/var/www/files", name))
}

// SECURE: filepath.Clean + prefix check
func fileHandler(w http.ResponseWriter, r *http.Request) {
    name := r.URL.Query().Get("file")
    base := "/var/www/files"
    clean := filepath.Join(base, filepath.Clean("/"+name))
    if !strings.HasPrefix(clean, base+string(os.PathSeparator)) {
        http.Error(w, "Forbidden", http.StatusForbidden)
        return
    }
    http.ServeFile(w, r, clean)
}
存档解压(ZipSlip)
# VULNERABLE: ZipSlip — zip entry names can contain ../
import zipfile
with zipfile.ZipFile(user_zip) as zf:
    zf.extractall('/var/www/uploads')

# SECURE: validate each entry path stays within the target directory
import zipfile, os
base = os.path.realpath('/var/www/uploads')
with zipfile.ZipFile(user_zip) as zf:
    for member in zf.namelist():
        target = os.path.realpath(os.path.join(base, member))
        if not target.startswith(base + os.sep):
            raise ValueError(f"ZipSlip detected: {member}")
    zf.extractall(base)

执行

此技能使用子 Agent 分三个阶段运行。将 sast/architecture.md 的内容作为上下文传递给所有子 Agent。

阶段 1:查找使用动态路径的文件加载接收点

启动一个子 Agent,并向其提供以下指示:

目标:找出代码库中所有使用动态构造路径打开、读取、提供或解压文件的位置——也就是说,路径(或其组成部分)存储在变量中,而不是完全硬编码的字符串。将结果写入 sast/pathtraversal-recon.md。

背景:你会获得项目的架构摘要。利用它了解技术栈、Web 框架、文件提供模式,以及任何文件上传或下载功能。

搜索目标——包含动态路径组件的文件加载接收点:

查找路径参数中包含变量的任何文件读取/提供函数调用(无论变量来自何处)。本阶段不追踪用户输入——那是第 2 阶段的任务。只需找出所有动态文件访问模式。

  1. 路径包含变量的直接文件打开/读取调用:
    • Python:open(var)、open(os.path.join(..., var))、pathlib.Path(var).read_text()、pathlib.Path(var).read_bytes()
    • Node.js:fs.readFile(var, ...)、fs.readFileSync(var)、fs.createReadStream(var)
    • PHP:file_get_contents(var)、fopen(var, ...)、readfile(var)、include(var)、require(var)、include_once(var)、require_once(var)
    • Ruby:File.read(var)、File.open(var)、IO.read(var)、IO.binread(var)
    • Java:new FileInputStream(var)、new File(var)、Files.readAllBytes(Paths.get(var))、Files.newInputStream(path)
    • Go:os.Open(var)、os.ReadFile(var)、ioutil.ReadFile(var)、os.OpenFile(var, ...)
    • C#:File.ReadAllText(var)、File.ReadAllBytes(var)、new FileStream(var, ...)、System.IO.File.Open(var, ...)
  2. 路径包含变量的框架文件提供调用:
    • Flask:send_file(var)、send_from_directory(base, var)
    • FastAPI/Starlette:FileResponse(var)
    • Django:FileResponse(open(var, 'rb'))、对已打开文件使用 StreamingHttpResponse
    • Express:res.sendFile(var)、res.download(var)、使用动态根目录的 express.static
    • Spring:new UrlResource(path.toUri())、ResourceLoader.getResource(var)、ClassPathResource(var)
    • Rails:send_file var、render file: var
    • Go:http.ServeFile(w, r, var)、http.ServeContent(w, r, var, ...)
  3. 至少有一个组件为变量的路径构造函数:
    • os.path.join(BASE, var)、os.path.join(var1, var2)
    • path.join(__dirname, var)、path.resolve(base, var)
    • Paths.get(base).resolve(var)
    • filepath.Join(base, var)
    • 用作路径的字符串拼接:BASE + var、f"{BASE}/{var}"、`${base}/${var}`
  4. 使用用户提供的归档文件进行解压(ZipSlip 模式):
    • Python:zipfile.ZipFile.extractall(...)、tarfile.TarFile.extractall(...)
    • Java:将 ZipEntry.getName() 用作输出路径
    • Node.js:unzipper、adm-zip、node-tar 的解压调用
    • Go:未验证条目名称就进行解压的 archive/zip 或 archive/tar

跳过的内容(这些没有动态路径组件——不要标记):

  • 完全硬编码的字符串字面量文件路径,不含变量部分
  • 完全从服务器端配置/环境变量派生,且不含用户提供组件的路径(例如,open(settings.LOG_FILE),其中 LOG_FILE 是配置值)
  • 根目录硬编码的框架内置静态文件中间件(例如,根目录固定的 express.static('public'))

输出格式——写入 sast/pathtraversal-recon.md:

# 路径遍历侦察:[项目名称]

## 摘要
发现 [N] 个使用动态构造路径访问文件的位置。

## 文件加载接收点

### 1. [描述性名称——例如,“下载端点中的动态 readFile”]
- **文件**:`path/to/file.ext`(第 X-Y 行)
- **函数/端点**:[函数名称或路由]
- **接收点**:[open / fs.readFile / send_file / include / FileInputStream / 等]
- **路径构造**:[os.path.join / path.join / 字符串拼接 / f-string / 等]
- **动态变量**:`var_name` —— [简要说明其可能代表什么,例如,“看起来是来自请求的文件名”或“来源未知”]
- **代码片段**:

[路径构造 + 文件操作调用]


[每个接收点重复以上内容]
阶段 1 后:检查候选项,然后再继续

阶段 1 完成后,读取 sast/pathtraversal-recon.md。如果侦察结果发现零个文件加载接收点(摘要显示“Found 0”,或“File-Loading Sinks”部分为空或不存在),则完全跳过阶段 2 和阶段 3。请改为将以下内容写入 sast/pathtraversal-results.md,然后删除 sast/pathtraversal-recon.md,并停止:

# Path Traversal Analysis Results

No vulnerabilities found.

只有在阶段 1 发现至少一个文件加载接收点时,才继续阶段 2。

阶段 2:验证——追踪污点并检查缓解措施(分批处理)

阶段 1 完成后,读取 sast/pathtraversal-recon.md,并将文件加载接收点拆分为每批最多 3 个接收点。并行启动每批一个子 Agent。每个子 Agent 只分析分配给自己的接收点,并将结果写入各自的批次文件。

分批流程(由你作为编排者执行,而非子 Agent):

  1. 读取 sast/pathtraversal-recon.md,并统计编号的接收点部分(### 1.、### 2. 等)。
  2. 将它们分成每批最多 3 个。例如,8 个接收点 → 3 批(1-3、4-6、7-8)。
  3. 从侦察文件中提取每批对应接收点部分的完整文本。
  4. 并行启动所有批次子 Agent,每个子 Agent 只提供分配给它的接收点。
  5. 每个子 Agent 将结果写入 sast/pathtraversal-batch-N.md,其中 N 为从 1 开始的批次编号。
  6. 从 sast/architecture.md 中确定项目的主要语言/框架,并从上文的“易受攻击与安全示例”部分中仅选择匹配的示例(并将“防止路径遍历的模式”/“不属于路径遍历的情况”作为参考)。例如,如果项目使用 Node.js/Express,则包含“Node.js — Express”代码块。在下方标注为 [TECH-STACK EXAMPLES] 的位置,将这些选定的示例纳入每个子 Agent 的指令中。

向每个批次子 Agent 提供以下指令(替换为该批次对应的值):

目标:对于每个指定的文件加载汇,判断用户提供的值是否会到达动态路径变量,以及是否有任何缓解措施可防止路径逃逸出预期的基础目录。我们的目标是查找路径遍历漏洞。将结果写入 sast/pathtraversal-batch-[N].md。

分配给你的汇(来自侦察阶段):

[在此粘贴分配给你的汇部分的完整文本,并保留原始编号]

背景:你将获得项目的架构摘要。利用它了解请求入口、中间件以及数据在应用中的流向。

路径遍历参考——需要检查的内容:

用户提供的输入被纳入文件系统路径,却未将解析后的路径限制在预期的基础目录内(包括 ZipSlip 风格的归档解压)。不要将 SSRF、纯 RCE/文件写入类问题、完全硬编码的路径,或安全的 realpath/resolve + 基础路径前缀检查判定为路径遍历(如有需要,请参阅主技能文档中的“路径遍历不包括什么”和“可防止路径遍历的模式”章节)。

对于每个汇,执行两项检查:

检查 A——路径变量是否受用户控制?

向上追踪动态变量的来源:

  1. 直接用户输入——变量直接从请求来源赋值:
    • HTTP 查询参数:request.GET.get(...)、req.query.x、params[:x]、$_GET['x']、c.Query("x")
    • 路径参数:request.path_params['name']、req.params.name、params[:name]、c.Param("name")
    • 请求正文/表单字段:request.POST.get(...)、req.body.x、params[:x]、$_POST['x']
    • HTTP 标头:request.headers.get(...)、req.headers['x']
    • Cookie:request.COOKIES.get(...)、req.cookies.x
    • 多部分文件名:file.filename、req.file.originalname、$_FILES['file']['name']
  2. 间接用户输入——变量经由转换、中间赋值或函数调用从用户输入派生。追踪完整链路:
    • 变量由辅助函数赋值 → 检查该函数的源代码
    • 变量作为参数传入 → 检查所有调用点
    • 变量从数据库值读取,而该值最初由用户输入存储
  3. 服务端/硬编码值——变量来自配置、环境变量、硬编码常量,或不受用户影响的服务端逻辑——此汇无法通过路径遍历利用。

检查 B——是否通过有效缓解措施防止路径逃逸?

即使用户输入到达了路径,以下缓解措施也能防止路径遍历。检查它们是否在文件操作之前正确应用:

  • realpath / os.path.realpath() + 基础目录前缀检查:解析符号链接和 .. 序列,然后验证结果是否以预期基础目录开头。这是最可靠的修复方式。
    • os.path.realpath(path).startswith(BASE + os.sep)——有效 ✓
    • os.path.realpath(path).startswith(BASE) 不带尾部分隔符——可能可绕过,因为 BASE 可能是另一个目录名的前缀 ✗
  • path.resolve() + startsWith(base + sep)(Node.js)——有效 ✓
  • Paths.get(...).normalize() + startsWith(base)(Java)——仅当 base 也通过 toRealPath() 获取时才有效 ✓
  • filepath.Clean() + strings.HasPrefix(clean, base+sep)(Go)——有效 ✓
  • basename() / path.basename() / File.basename()——移除所有目录部分;可有效防止路径遍历,但会阻止访问子目录
  • 允许的文件名白名单——如果白名单严格,并且输入在使用前会与白名单进行比较,则完全有效
  • 框架的 send_from_directory(Flask)——Flask 的 send_from_directory 内部会调用 safe_join,在发生路径遍历时抛出错误;有效 ✓

不足以防止问题的缓解措施:

  • 使用简单的 replace('../', '') 移除 ../——可通过 ....// 或 URL 编码绕过
  • 检查输入是否以 / 开头——无法防止相对路径遍历
  • 单独使用 os.path.join,不配合 realpath——os.path.join('/base', '../etc/passwd') 仍会生成 /etc/passwd
  • 对输入只进行一次 URL 解码——攻击者可进行双重编码:%252e%252e%252f → %2e%2e%2f → ../
  • 类型验证(例如检查扩展名是否为 .pdf),但不进行路径逃逸检查——在较旧系统上,攻击者可使用 ../../etc/passwd%00.pdf(空字节),或构造路径使其以正确的扩展名结尾

适用于本项目技术栈的漏洞与安全示例:

[TECH-STACK EXAMPLES]

分类:

  • 存在漏洞:明确证实用户输入到达路径变量,且在文件操作前未实施有效缓解措施。
  • 可能存在漏洞:用户输入可能到达路径变量(间接流),或存在薄弱/不完整的缓解措施(例如 replace('../', ''),前缀检查不含尾部分隔符)。
  • 不存在漏洞:路径变量仅由服务端控制,或已正确应用有效缓解措施(realpath + 前缀检查、basename、严格白名单、安全的框架辅助函数)。
  • 需要人工审查:无法有把握地确定变量来源(经过不透明辅助函数或复杂的条件流程),或缓解逻辑不常见,难以通过静态分析评估。

输出格式——写入 sast/pathtraversal-batch-[N].md:

# Path Traversal Batch [N] Results

## Findings

### [VULNERABLE] Descriptive name
- **File**: `path/to/file.ext` (lines X-Y)
- **Endpoint / function**: [route or function name]
- **Issue**: [e.g., "HTTP query param `file` flows directly into os.path.join without realpath check"]
- **Taint trace**: [Step-by-step from entry point to the file operation]
- **Missing mitigation**: [What check is absent]
- **Impact**: Read arbitrary files accessible to the process user, including `/etc/passwd`, application config, source code, private keys.
- **Remediation**: [Specific fix]
- **Dynamic Test**:

[curl command or payload to confirm; show traversal and encoded variants as appropriate]


### [LIKELY VULNERABLE] Descriptive name
- **File**: `path/to/file.ext` (lines X-Y)
- **Endpoint / function**: [route or function name]
- **Issue**: [e.g., "Variable likely sourced from user input via helper" or "Weak mitigation: strips ../ but bypassable with ....//"]
- **Taint trace**: [Best-effort trace with the uncertain step identified]
- **Concern**: [Why it remains a risk despite partial mitigation]
- **Remediation**: [Apply realpath + prefix check or basename before joining]
- **Dynamic Test**:

[payloads to attempt bypass of the partial mitigation]


### [NOT VULNERABLE] Descriptive name
- **File**: `path/to/file.ext` (lines X-Y)
- **Endpoint / function**: [route or function name]
- **Reason**: [e.g., "Path is derived entirely from server-side config" or "os.path.realpath() + prefix check correctly applied"]

### [NEEDS MANUAL REVIEW] Descriptive name
- **File**: `path/to/file.ext` (lines X-Y)
- **Endpoint / function**: [route or function name]
- **Uncertainty**: [Why the variable's origin or mitigation could not be determined]
- **Suggestion**: [What to trace manually]
阶段 3:合并 — 汇总批次结果

在所有阶段 2 批次子 Agent 完成后,读取每个 sast/pathtraversal-batch-*.md 文件,并将它们合并到单个 sast/pathtraversal-results.md 中。由你(编排者)直接完成此操作——无需子 Agent。

合并流程:

  1. 读取所有 sast/pathtraversal-batch-1.md、sast/pathtraversal-batch-2.md……文件。
  2. 收集每个批次文件中的所有发现并合并为一个列表,同时保留原始分类和所有详细字段。
  3. 汇总所有批次中的数量,供执行摘要使用。
  4. 使用以下格式将合并后的报告写入 sast/pathtraversal-results.md:
# Path Traversal Analysis Results: [Project Name]

## Executive Summary
- Sinks analyzed: [total across all batches]
- Vulnerable: [N]
- Likely Vulnerable: [N]
- Not Vulnerable: [N]
- Needs Manual Review: [N]

## Findings

[All findings from all batches, grouped by classification:
 VULNERABLE first, then LIKELY VULNERABLE, then NEEDS MANUAL REVIEW, then NOT VULNERABLE.
 Preserve every field from the batch results exactly as written.]
  1. 写入 sast/pathtraversal-results.md 后,删除所有中间批次文件(sast/pathtraversal-batch-*.md)。

重要提醒

  • 读取 sast/architecture.md,并将其内容作为上下文传递给所有子 Agent。
  • 阶段 2 必须在阶段 1 完成后运行——它依赖侦察输出。
  • 阶段 3 必须在所有阶段 2 批次完成后运行——它依赖所有批次输出。
  • 每个子 Agent 处理 3 个 sink。如果总共只有 1-3 个 sink,则使用单个子 Agent。如果有 10 个,则使用 4 个子 Agent(3+3+3+1)。
  • 并行启动所有批次子 Agent——不要按顺序运行。
  • 每个批次子 Agent 只接收侦察文件中分配给它的 sink 文本,而不是整个侦察文件。这样可以让每个子 Agent 的上下文保持精简且聚焦。
  • 阶段 1 纯粹是结构性分析:标记路径包含动态部分的任何文件加载 sink,无论其来源如何。阶段 1 不要尝试追踪用户输入——这是阶段 2 的任务。
  • 阶段 2 是污点分析和缓解措施审查:对阶段 1 发现的每个 sink,(a)追踪路径变量的来源;(b)检查是否有有效的缓解措施可防止路径逃出预期目录。
  • os.path.join 和 path.join 本身不能防止路径遍历——os.path.join('/base', '../etc/passwd') 会解析为 /etc/passwd。只有 realpath 加前缀检查才能防止这种情况。
  • 编码后的路径遍历变体(%2e%2e%2f、%252e%252e%252f、..%2f、%2e%2e/)会绕过简单的字符串匹配过滤器;只有文件系统级解析(realpath)才能可靠地处理它们。
  • Flask 中的 send_from_directory 本身是安全的(内部会调用 safe_join)——除非用户输入也被用作基目录参数,否则不要将其标记出来。
  • 归档提取(ZipSlip)是路径遍历的一种变体:zip/tar 条目名称中可能包含 ../ 序列。对于任何使用条目名称作为输出路径、却未对每个条目进行验证的提取操作,都要标记出来。
  • 可能发生二阶路径遍历:从用户输入写入数据库的文件名,之后可能在代码库的其他位置用于读取文件。将从数据库读取的路径值视为可能受污点影响,并追踪其写入位置。
  • 如有疑问,将其分类为“需要人工审查”,而不是“未发现漏洞”。在安全评估中,漏报比误报更糟。
  • 清理中间文件:写入最终的 sast/pathtraversal-results.md 后,删除 sast/pathtraversal-recon.md 和所有 sast/pathtraversal-batch-*.md 文件。

评论

支持 Markdown