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. 03ZIP・TAR 展開時のパス制限を調査

インストール

コマンドをコピーしてターミナルで実行します。

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

インストール後、Claude Code で/sast-pathtraversalを入力して読み込まれたか確認します。

詳細

パストラバーサルの検出

コードベース内のパストラバーサルの脆弱性を見つけることに特化したセキュリティ評価を実施します。このスキルでは、サブエージェントを使用した3段階のアプローチを採用します。偵察(動的なパスを使用するファイル読み込みのシンクを見つける)、バッチ検証(ユーザー入力を追跡し、緩和策を3件ずつの並列バッチで確認する)、統合(バッチの結果を1つのレポートにまとめる)です。

前提条件: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($_GET['page'] . '.php')
  • エントリー名から ../ の要素を取り除かずに出力パスとして使うアーカイブ展開(ZipFile、tarfile、zipslip)
  • サニタイズされていないユーザー制御のパスを指定して send_file()/send_from_directory()/res.sendFile() を使う
  • サニタイズされずに保存された、ユーザー制御のデータベース値からパスを取得したファイルを読み取る
パストラバーサルに該当しないもの

以下をパストラバーサルとして報告しないでください。

  • SSRF:ユーザー入力からリモート URL を取得すること。これは Server-Side Request Forgery であり、別の脆弱性クラスです
  • ファイル書き込みによる 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)

実行

このスキルはサブエージェントを使い、3段階で実行します。sast/architecture.md の内容をコンテキストとしてすべてのサブエージェントに渡してください。

フェーズ 1:動的なパスを使用するファイル読み込みのシンクを見つける

以下の指示を使ってサブエージェントを起動してください。

目標:コードベース内で、動的に構築されたパス(パス全体またはその一部が、完全にハードコードされた文字列ではなく変数に格納されているもの)を使って、ファイルを開く、読み込む、配信する、または展開するすべての箇所を見つけてください。結果を sast/pathtraversal-recon.md に書き出してください。

コンテキスト:プロジェクトのアーキテクチャ概要が提供されます。それを使って、技術スタック、ウェブフレームワーク、ファイル配信のパターン、ファイルのアップロードやダウンロード機能を把握してください。

検索対象 — 動的なパス要素を含むファイル読み込みシンク:

ファイル読み込み/配信関数のパス引数に変数が含まれている呼び出しはすべて検出してください(変数の由来は問いません)。この段階ではユーザー入力を追跡しません。それはフェーズ 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. 少なくとも 1 つの要素が変数であるパス構築関数:
    • 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 の展開

除外対象(動的なパス要素がないため、検出対象にしないでください):

  • 変数部分のない、完全にハードコードされた文字列リテラルのファイルパス
  • ユーザー提供の要素を含まず、サーバー側の設定/環境変数のみから導出されたパス(例:設定値である LOG_FILE を使う open(settings.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 を読みます。調査で ファイル読み込みシンクが 0 件見つかった場合(概要に「Found 0」と記載されている、または「File-Loading Sinks」セクションが空もしくは存在しない場合)は、フェーズ 2 とフェーズ 3 を完全にスキップします。その代わりに、次の内容を sast/pathtraversal-results.md に書き込んだ後、sast/pathtraversal-recon.md を削除して終了します。

# Path Traversal Analysis Results

No vulnerabilities found.

フェーズ 1 でファイル読み込みシンクが 1 件以上見つかった場合に限り、フェーズ 2 に進みます。

フェーズ 2:検証 — 汚染を追跡し、緩和策を確認する(バッチ処理)

フェーズ 1 の完了後、sast/pathtraversal-recon.md を読み、ファイル読み込みシンクを 最大 3 件ずつのバッチに分割します。バッチごとにサブエージェントを 1 つ並列で起動します。各サブエージェントは割り当てられたシンクのみを分析し、各自のバッチファイルに結果を書き込みます。

バッチ分割の手順(サブエージェントではなく、オーケストレーターが実行します):

  1. sast/pathtraversal-recon.md を読み、番号付きのシンクセクション(### 1.、### 2. など)の数を数えます。
  2. 最大 3 件ずつのバッチに分割します。例:シンクが 8 件の場合、3 バッチ(1~3、4~6、7~8)に分割します。
  3. 各バッチについて、調査ファイルから該当するシンクセクションの全文を抽出します。
  4. すべてのバッチサブエージェントを並列で起動し、それぞれに割り当てられたシンクのみを渡します。
  5. 各サブエージェントは sast/pathtraversal-batch-N.md に書き込みます。N は 1 から始まるバッチ番号です。
  6. sast/architecture.md からプロジェクトの主要な言語/フレームワークを特定し、上記の「脆弱な例と安全な例」セクション(および参照用の「パストラバーサルを防ぐパターン」/「パストラバーサルではないもの」)から、該当する例のみを選択します。たとえば、プロジェクトが Node.js/Express を使用している場合は、「Node.js — Express」のブロックを含めます。以下の [TECH-STACK EXAMPLES] で示されている箇所に、選択した例を各サブエージェントへの指示に含めます。

各バッチサブエージェントに、次の指示を与えます(バッチ固有の値に置き換えます)。

目標:割り当てられた各ファイル読み込みシンクについて、ユーザーが指定した値が動的パス変数に到達するか、またパスが意図されたベースディレクトリの外に出るのを防ぐ緩和策があるかを判断してください。目標はパストラバーサルの脆弱性を見つけることです。結果は sast/pathtraversal-batch-[N].md に記載してください。

割り当てられたシンク(調査フェーズより):

[Paste the full text of the assigned sink sections here, preserving the original numbering]

コンテキスト:プロジェクトのアーキテクチャ概要が提供されます。これを使って、リクエストのエントリーポイント、ミドルウェア、アプリケーション内でのデータの流れを把握してください。

パストラバーサルの参考情報 — 確認すべき内容:

解決されたパスが意図されたベースディレクトリ内に収まるよう制限せずに、ユーザー入力をファイルシステムパスに組み込んでいること(ZipSlip 型のアーカイブ展開を含む)。SSRF、純粋な RCE/ファイル書き込みのクラス、完全にハードコードされたパス、安全な realpath/resolve とベースパスのプレフィックスチェックは、パストラバーサルとして報告しないでください(必要に応じて、メインスキル文書の「パストラバーサルではないもの」と「パストラバーサルを防ぐパターン」のセクションを参照してください)。

各シンクについて、次の 2 つのチェックを行ってください:

チェック 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 エンコードによって回避できます
  • 入力が / で始まらないことの確認 — 相対パスによるトラバーサルは防止できません
  • realpath を使わずに os.path.join のみを使う — os.path.join('/base', '../etc/passwd') は依然として /etc/passwd を生成します
  • 入力を 1 回だけ URL デコードする — 攻撃者は二重エンコードできます:%252e%252e%252f → %2e%2e%2f → ../
  • 型検証(例:拡張子が .pdf であることの確認)だけを行い、パス逸脱をチェックしない — 攻撃者は、古いシステムで ../../etc/passwd%00.pdf(ヌルバイト)を使ったり、末尾が正しい拡張子になるようパスを構成したりできます

このプロジェクトの技術スタックにおける、脆弱な例と安全な例:

[TECH-STACK EXAMPLES]

分類:

  • 脆弱:ユーザー入力がパス変数に到達することが明確で、ファイル操作の前に効果的な緩和策が適用されていない。
  • 脆弱の可能性あり:ユーザー入力がパス変数に到達している可能性が高い(間接的なデータフロー)、または弱い/不完全な緩和策がある(例:replace('../', '')、プレフィックスチェックに末尾の区切り文字がない)。
  • 脆弱ではない:パス変数がサーバー側の値のみから成る、または効果的な緩和策(realpath + プレフィックスチェック、basename、厳格な許可リスト、安全なフレームワークヘルパー)が正しく適用されている。
  • 手動レビューが必要:変数の起点を確実に特定できない(不透明なヘルパーや複雑な条件分岐を経由する)、または緩和ロジックが一般的ではなく、静的解析で評価するのが難しい。

出力形式 — sast/pathtraversal-batch-[N].md に記載してください:

# パストラバーサル バッチ [N] の結果

## 検出結果

### [VULNERABLE] 説明的な名前
- **ファイル**:`path/to/file.ext`(行 X-Y)
- **エンドポイント/関数**:[ルートまたは関数名]
- **問題**:[例:「HTTP クエリパラメーター `file` が `realpath` チェックなしで直接 `os.path.join` に渡される」]
- **汚染トレース**:[エントリーポイントからファイル操作までの手順]
- **不足している緩和策**:[欠けているチェック]
- **影響**:プロセスのユーザーがアクセス可能な任意のファイル(`/etc/passwd`、アプリケーション設定、ソースコード、秘密鍵など)を読み取れます。
- **修正方法**:[具体的な修正]
- **動的テスト**:

[確認用の curl コマンドまたはペイロード。必要に応じて、トラバーサルやエンコードされた変種も提示]


### [LIKELY VULNERABLE] 説明的な名前
- **ファイル**:`path/to/file.ext`(行 X-Y)
- **エンドポイント/関数**:[ルートまたは関数名]
- **問題**:[例:「ヘルパーを介して変数がユーザー入力に由来する可能性が高い」または「弱い緩和策:`../` を削除しているが、`....//` で回避可能」]
- **汚染トレース**:[不確かな箇所を明示した、可能な範囲でのトレース]
- **懸念事項**:[部分的な緩和策があってもリスクが残る理由]
- **修正方法**:[結合前に `realpath` + プレフィックスチェックまたは `basename` を適用]
- **動的テスト**:

[部分的な緩和策の回避を試すペイロード]


### [NOT VULNERABLE] 説明的な名前
- **ファイル**:`path/to/file.ext`(行 X-Y)
- **エンドポイント/関数**:[ルートまたは関数名]
- **理由**:[例:「パスは完全にサーバー側の設定から導出されている」または「`os.path.realpath()` + プレフィックスチェックが正しく適用されている」]

### [NEEDS MANUAL REVIEW] 説明的な名前
- **ファイル**:`path/to/file.ext`(行 X-Y)
- **エンドポイント/関数**:[ルートまたは関数名]
- **不確実な点**:[変数の起点や緩和策を特定できなかった理由]
- **提案**:[手動で追跡する内容]
フェーズ 3:統合 — バッチ結果をまとめる

フェーズ 2 のすべてのバッチサブエージェントが完了したら、sast/pathtraversal-batch-*.md ファイルをすべて読み込み、単一の sast/pathtraversal-results.md に統合します。これはオーケストレーターであるあなたが直接行います。サブエージェントは必要ありません。

統合手順:

  1. sast/pathtraversal-batch-1.md、sast/pathtraversal-batch-2.md、... のすべてのファイルを読み込みます。
  2. 各バッチファイルからすべての検出事項を収集し、元の分類と詳細フィールドをすべて保持したまま、1 つのリストにまとめます。
  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 を読み込み、その内容をすべてのサブエージェントにコンテキストとして渡してください。
  • フェーズ 2 はフェーズ 1 の完了後に実行する必要があります。フェーズ 2 は偵察結果に依存します。
  • フェーズ 3 は、フェーズ 2 のすべてのバッチ完了後に実行する必要があります。フェーズ 3 はすべてのバッチ出力に依存します。
  • 1 つのサブエージェントあたりのバッチサイズはシンク 3 個です。シンクが合計 1~3 個の場合は、サブエージェントを 1 つ使用します。10 個の場合は、サブエージェントを 4 つ(3+3+3+1)使用します。
  • すべてのバッチサブエージェントを並列で起動してください。順次実行してはいけません。
  • 各バッチサブエージェントに渡すのは、偵察ファイル全体ではなく、割り当てられたシンクのテキストのみです。これにより、各サブエージェントのコンテキストを小さく、焦点を絞ったものに保てます。
  • フェーズ 1 は構造のみを対象とします。動的な要素を含むパスを使用するファイル読み込みシンクがあれば、その出所に関係なくフラグを付けます。フェーズ 1 ではユーザー入力の追跡を試みないでください。それはフェーズ 2 の役割です。
  • フェーズ 2 は汚染解析と緩和策のレビューです。フェーズ 1 で見つかった各シンクについて、(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 のエントリー名に ../ の並びが含まれることがあります。エントリーごとの検証をせずに、エントリー名を出力パスとして使用する展開処理にはフラグを付けてください。
  • 二次トラバーサルが発生する可能性があります。ユーザー入力から DB に保存されたファイル名が、後にコードベース内の別の場所でファイル読み込みに使われる場合があります。DB から読み込んだパス値は汚染されている可能性があるものとして扱い、書き込まれた場所まで遡って追跡してください。
  • 判断に迷う場合は、「脆弱ではない」ではなく「手動レビューが必要」に分類してください。セキュリティ評価では、偽陰性より偽陽性のほうが望ましいです。
  • 中間ファイルをクリーンアップしてください。最終的な sast/pathtraversal-results.md を書き込んだ後、sast/pathtraversal-recon.md とすべての sast/pathtraversal-batch-*.md ファイルを削除してください。

コメント