sast-ssrf について
コードベースのSSRFを調査するセキュリティ評価向けスキルです。通信箇所の探索、ユーザー入力のバッチ追跡、結果の統合を行い、事前に `sast/architecture.md` が必要です。
主な機能
- 01外部通信箇所の検出
- 02ユーザー入力のバッチ追跡
- 03SSRF対策パターンの確認
- 04分析結果レポートの統合
活用シーン
- 01WebhookやURL取得機能の監査
- 02入力値が通信処理に渡るかの確認
- 03コード内のSSRF対策の点検
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ utkusen/ sast-skills --skill sast-ssrfインストール後、Claude Code で/sast-ssrfを入力して読み込まれたか確認します。
詳細
サーバーサイドリクエストフォージェリ(SSRF)の検出
コードベース内の SSRF 脆弱性を見つけるため、対象を絞ったセキュリティ評価を実施します。このスキルでは、サブエージェントを使った 3 段階のアプローチを採用します。偵察(アウトバウンド TCP、DNS、HTTP リクエストを行うすべての箇所を見つけます)、バッチ検証(ユーザー入力がこれらの呼び出し箇所に到達するかを、3 件ずつの並列バッチで追跡します)、統合(バッチレポートを 1 つのファイルにまとめます)。
前提条件:sast/architecture.md が存在している必要があります。存在しない場合は、先に分析スキルを実行してください。
SSRF とは
SSRF は、攻撃者が URL、ホスト名、IP アドレス、またはサーバーサイドリクエストで使用されるポートを指定したり、影響を与えたりすることで、サーバーに任意の宛先(内部サービス、クラウドのメタデータエンドポイント、その他の外部対象を含む)へのアウトバウンドネットワークリクエストを実行させられる場合に発生します。
基本的なパターン:検証されていないユーザー制御の入力が、アウトバウンドネットワーク呼び出しの宛先引数に到達すること。
SSRF に該当するもの
- URL またはホストがユーザー入力から構築される HTTP クライアント呼び出し:
requests.get(user_url) - クライアントから提供された場所にあるリソースの取得:
fetch(req.body.webhook_url) - ユーザーが指定したホスト名の DNS ルックアップ:
dns.lookup(req.query.host) - ユーザー入力から導出されたホスト/ポートへの生の TCP 接続:
socket.connect((user_host, user_port)) - ユーザー入力の HTTP/FTP URL に使用されるファイル取得関数:
file_get_contents($user_url) - 検証せずにユーザー指定の宛先へ転送する URL リダイレクター
- Webhook、URL からのインポート、スクリーンショットサービス、PDF レンダラー、画像プロキシなど、ユーザーに代わってリモートリソースを取得する機能全般
SSRF に該当しないもの
以下にはフラグを立てないでください。
- オープンリダイレクト:ユーザー指定の URL にブラウザーをリダイレクトする(HTTP 302)こと。これはクライアントサイドのリダイレクトであり、サーバーサイドリクエストではありません。
- URL を介した XSS:ユーザー指定の URL をエスケープせずに
<a>タグ内に表示すること。これは XSS です。 - IDOR:オブジェクト ID を変更して別のユーザーのデータにアクセスすること。別種の脆弱性です。
- ハードコードされたアウトバウンド呼び出し:ユーザーの影響を受けない、固定された完全なハードコード URL への HTTP リクエスト。SSRF ではありません。
SSRF を防ぐパターン
以下のパターンが見られる場合、コードは脆弱ではない可能性が高いです。
1. 許可された宛先の厳格な許可リスト
ALLOWED_HOSTS = {"api.example.com", "cdn.example.com"}
parsed = urlparse(user_url)
if parsed.hostname not in ALLOWED_HOSTS:
raise ValueError("Destination not allowed")
requests.get(user_url)
2. 許可された URL プレフィックス/スキームの許可リスト
ALLOWED_PREFIXES = ["https://api.example.com/", "https://cdn.example.com/"]
if not any(user_url.startswith(p) for p in ALLOWED_PREFIXES):
abort(400)
requests.get(user_url)
3. 宛先にユーザーの影響が及ばない
# Destination fully hardcoded — no user input involved
response = requests.get("https://api.thirdparty.com/data")
注:IP ブロックリスト(169.254.0.0/16、10.0.0.0/8 などをブロックするもの)は、十分な保護にはなりません。DNS リバインディング、URL エンコーディング、IPv6 表記、10 進数の IP 表現、リダイレクトチェーンによって回避される可能性があります。ブロックリストがあるからといって安全と判断しないでください。「脆弱である可能性が高い」と分類してください。
脆弱な例と安全な例
Python — requests
# VULNERABLE: URL fully controlled by user
@app.route('/fetch')
def fetch():
url = request.args.get('url')
response = requests.get(url)
return response.text
# SECURE: strict allowlist on destination host
ALLOWED = {"api.example.com"}
@app.route('/fetch')
def fetch():
url = request.args.get('url')
if urlparse(url).hostname not in ALLOWED:
abort(403)
response = requests.get(url)
return response.text
Python — urllib
# VULNERABLE: user controls the URL passed to urlopen
def preview(request):
target = request.GET.get('target')
data = urllib.request.urlopen(target).read()
return HttpResponse(data)
# SECURE: only allow https scheme to a hardcoded host
def preview(request):
target = request.GET.get('target')
parsed = urlparse(target)
if parsed.scheme != 'https' or parsed.hostname != 'media.example.com':
return HttpResponse(status=400)
data = urllib.request.urlopen(target).read()
return HttpResponse(data)
Node.js — fetch / axios
// VULNERABLE: webhook URL comes directly from request body
app.post('/webhook/test', async (req, res) => {
const { url } = req.body;
const result = await fetch(url);
res.json(await result.json());
});
// SECURE: allowlist check before fetch
const ALLOWED_HOSTS = new Set(['hooks.example.com']);
app.post('/webhook/test', async (req, res) => {
const { url } = req.body;
const { hostname } = new URL(url);
if (!ALLOWED_HOSTS.has(hostname)) return res.status(403).send('Forbidden');
const result = await fetch(url);
res.json(await result.json());
});
Node.js — http.request
// VULNERABLE: host and path from query string
app.get('/proxy', (req, res) => {
const { host, path } = req.query;
http.get({ host, path }, (proxyRes) => proxyRes.pipe(res));
});
Ruby on Rails — Net::HTTP / OpenURI
# VULNERABLE: open() fetches arbitrary URL
def import
url = params[:url]
content = URI.open(url).read # also triggers for open(url) via Kernel#open
# ...
end
# SECURE: restrict scheme and host
def import
url = params[:url]
uri = URI.parse(url)
raise "Forbidden" unless uri.is_a?(URI::HTTPS) && uri.host == "data.example.com"
content = uri.open.read
# ...
end
PHP — cURL
// VULNERABLE: user-supplied URL piped into curl
function fetch_preview($url) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$result = curl_exec($ch);
curl_close($ch);
return $result;
}
// Called as: fetch_preview($_GET['url'])
// SECURE: validate URL against allowlist before curl
function fetch_preview($url) {
$allowed = ['https://cdn.example.com/'];
foreach ($allowed as $prefix) {
if (strpos($url, $prefix) === 0) {
// ... proceed with curl
}
}
throw new Exception("Destination not allowed");
}
PHP — file_get_contents
// VULNERABLE: file_get_contents with http:// wrapper and user input
$url = $_GET['source'];
$data = file_get_contents($url); // fetches remote URL if scheme is http/https/ftp
Java — Spring / OkHttp
// VULNERABLE: RestTemplate with user-controlled URL
@GetMapping("/proxy")
public ResponseEntity<String> proxy(@RequestParam String url) {
RestTemplate restTemplate = new RestTemplate();
return restTemplate.getForEntity(url, String.class);
}
// VULNERABLE: OkHttp with user-controlled host
public String fetch(String host, String path) {
Request request = new Request.Builder()
.url("https://" + host + path)
.build();
return client.newCall(request).execute().body().string();
}
Go — net/http
// VULNERABLE: user-supplied URL passed to http.Get
func proxyHandler(w http.ResponseWriter, r *http.Request) {
target := r.URL.Query().Get("url")
resp, err := http.Get(target)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
io.Copy(w, resp.Body)
}
// VULNERABLE: user controls host in net.Dial
func dialHandler(w http.ResponseWriter, r *http.Request) {
host := r.URL.Query().Get("host")
port := r.URL.Query().Get("port")
conn, _ := net.Dial("tcp", host+":"+port)
// ...
}
C# — HttpClient
// VULNERABLE: user-supplied URL passed to HttpClient
[HttpGet("proxy")]
public async Task<IActionResult> Proxy([FromQuery] string url)
{
var response = await _httpClient.GetAsync(url);
var content = await response.Content.ReadAsStringAsync();
return Content(content);
}
実行
このスキルはサブエージェントを使って 3 段階で実行されます。sast/architecture.md の内容を、すべてのサブエージェントにコンテキストとして渡してください。
フェーズ 1:アウトバウンドネットワーク呼び出し箇所をすべて見つける
以下の指示を使ってサブエージェントを起動してください。
目標:アプリケーションが外部へのネットワークリクエスト(HTTP、HTTPS、FTP、TCP、DNS)を行うコードベース内の場所をすべて特定してください。宛先がユーザーによって制御されているかどうかは問いません。結果を
sast/ssrf-recon.mdに出力してください。コンテキスト:プロジェクトのアーキテクチャ概要が提供されます。これを使って、技術スタック、使用中の HTTP クライアントライブラリ、ネットワーキングや Webhook 関連のコンポーネントを把握してください。
検索対象 — 外部リクエストの呼び出し箇所:
ネットワーク接続を開くコードや、リモートリソースを取得するコードを探してください。実質的にハードコードされていない URL、ホスト、アドレス値が引数として渡される呼び出しは、すべて報告してください。その値がユーザーによって制御されているかどうかは、まだ追跡しません。それはフェーズ 2 で行います。
- Python の HTTP クライアント:
requests.get(url)、requests.post(url)、requests.put(url)、requests.request(method, url)、requests.Session().get(url)urllib.request.urlopen(url)、urllib2.urlopen(url)httpx.get(url)、httpx.post(url)、httpx.AsyncClient().get(url)aiohttp.ClientSession().get(url)、aiohttp.ClientSession().post(url)- Python のソケット/DNS:
socket.connect((host, port))、socket.create_connection((host, port))dns.resolver.resolve(name)、socket.getaddrinfo(host, ...)- リモートスキームを使った Python のファイル取得:
urllib.request.urlopen(url)。url が http/https/ftp の可能性がある場合from urllib.request import urlopenなどを介したopen(url)。url がリモートの可能性がある場合は報告してください- Node.js/JavaScript の HTTP クライアント:
fetch(url)、node-fetch(url)axios.get(url)、axios.post(url)、axios.request({url})http.get(url)、https.get(url)、http.request(options)、https.request(options)got(url)、superagent.get(url)、needle.get(url)、undici.request(url)require('request')(options)- Node.js のソケット/DNS:
net.createConnection({host, port})、net.connect(port, host)dns.lookup(hostname, ...)、dns.resolve(hostname, ...)、dns.resolve4(hostname)- Ruby の HTTP クライアント:
Net::HTTP.get(uri)、Net::HTTP.start(host, ...)、Net::HTTP.get_response(url)URI.open(url)、open(url)(Kernel#open/OpenURI)RestClient.get(url)、RestClient::Resource.new(url)Faraday.new(url).get(path)、HTTParty.get(url)Typhoeus::Request.new(url)- PHP の HTTP クライアントとファイル関数:
curl_setopt($ch, CURLOPT_URL, $url)に続くcurl_exec($ch)file_get_contents($url)—$urlが http/https/ftp URL の可能性がある場合は報告してください- リモート URL スキームを使った
fopen($url, 'r')Guzzle:$client->request('GET', $url)、$client->get($url)Symfony HttpClient:$client->request('GET', $url)- Java の HTTP クライアント:
new URL(url).openConnection()、new URL(url).openStream()- 動的な URL を使った
HttpURLConnection/HttpsURLConnectionOkHttpClient().newCall(new Request.Builder().url(url)...)RestTemplate.getForObject(url, ...)、RestTemplate.getForEntity(url, ...)WebClient.get().uri(url)、WebClient.create(url)Apache HttpClient:httpClient.execute(new HttpGet(url))- Go の HTTP クライアントとネットワーク接続:
http.Get(url)、http.Post(url, ...)、http.NewRequest("GET", url, ...)net.Dial("tcp", addr)、net.DialTCP(...)、net.DialTimeout("tcp", addr, ...)net.LookupHost(hostname)、net.LookupAddr(addr)、net.ResolveIPAddr(...)net.ResolveTCPAddr("tcp", addr)- C#/.NET の HTTP クライアント:
HttpClient.GetAsync(url)、HttpClient.PostAsync(url, ...)、HttpClient.SendAsync(request)WebRequest.Create(url)、WebClient.DownloadString(url)、WebClient.DownloadData(url)- 動的な URL を使った
HttpWebRequest- ネットワークツールの外部プロセス実行(subprocess、exec、system などを介したもの):
subprocess.run(["curl", url, ...])、subprocess.Popen(["wget", url, ...])os.system("curl " + url)、exec("wget " + url)- 宛先に変数を使っている
curl、wget、nc、ncat、nmapの呼び出し対象外(安全なため、報告しないでください):
- URL とホスト名の全体が動的な部分のない文字列リテラルで完全にハードコードされている呼び出し:
requests.get("https://api.example.com/data")- サービス間アーキテクチャの一部であることが明らかな、
localhostまたは127.0.0.1への内部ループバック接続(ローカルキューへの接続など)。ただし、アドレスが動的な場合は報告してください出力形式 —
sast/ssrf-recon.mdに出力してください:# SSRF Recon: [Project Name] ## Summary Found [N] outbound network call sites. ## Outbound Call Sites ### 1. [説明的な名前 — 例:「Webhook ディスパッチャー内の HTTP GET」] - **ファイル**:`path/to/file.ext`(行 X-Y) - **関数/エンドポイント**:[関数名またはルート] - **呼び出しの種類**:[HTTP GET/HTTP POST/TCP 接続/DNS ルックアップ/subprocess の curl など] - **ライブラリ/メソッド**:[requests.get/fetch/http.Get/curl_exec など] - **宛先引数**:`var_name` または `url_expression` — [簡単な説明。例:「クエリパラメーターから組み立て」や「ホストが変数で、パスの一部はハードコード」] - **コードスニペット**:[宛先を組み立てる直前の行と、外部呼び出しを含めてください]
[各呼び出し箇所について繰り返す]
Phase 1 の後:候補を確認してから次に進みます
Phase 1 が完了したら、sast/ssrf-recon.md を読みます。調査で発信呼び出し箇所が 0 件だった場合(概要に「Found 0」と報告されている、または「Outbound Call Sites」セクションが空もしくは存在しない場合)、Phase 2 と Phase 3 は完全にスキップします。代わりに、次の内容を sast/ssrf-results.md に書き込み、終了します。
# SSRF Analysis Results
No vulnerabilities found.
Phase 1 で発信呼び出し箇所が 1 件以上見つかった場合にのみ、Phase 2 に進みます。
Phase 2:検証 — ユーザー入力から発信呼び出し箇所までを追跡する(一括処理)
Phase 1 が完了したら、sast/ssrf-recon.md を読み、発信呼び出し箇所を1 バッチあたり最大 3 箇所に分割します。各バッチにつき 1 つのサブエージェントを並列で起動します。各サブエージェントは、割り当てられた箇所についてのみ汚染を追跡し、それぞれのバッチファイルに結果を書き込みます。
バッチ分割の手順(サブエージェントではなく、オーケストレーターが行います):
sast/ssrf-recon.mdを読み、「Outbound Call Sites」の下にある番号付きの箇所のセクション(### 1.、### 2. など)を数えます。- 最大 3 箇所ずつに分割します。例:8 箇所の場合は 3 バッチ(1~3、4~6、7~8)です。
- 各バッチについて、recon ファイルから該当する箇所のセクション全文を抽出します。
- すべてのバッチサブエージェントを並列で起動し、それぞれに割り当てた箇所のみを渡します。
- 各サブエージェントは
sast/ssrf-batch-N.mdに書き込みます。N は 1 から始まるバッチ番号です。 sast/architecture.mdからプロジェクトの主要な言語/フレームワークを特定し、上記の「脆弱な例と安全な例」セクションから一致する例のみを選択します。たとえば、プロジェクトが Node.js の fetch/axios を使用している場合は、「Node.js — fetch / axios」と「Node.js — http.request」の例のみを含めます。以下の[TECH-STACK EXAMPLES]の箇所に示されているように、選択した例を各サブエージェントへの指示に含めます。
各バッチサブエージェントに、以下の指示を与えます(バッチごとの値に置き換えてください)。
目標:割り当てられた各アウトバウンドネットワーク呼び出し箇所について、ユーザーが指定した値が接続先(URL、ホスト、パス、ポート、スキーム)を制御または左右するかどうかを判定します。SSRF 脆弱性を見つけることが目標です。結果を
sast/ssrf-batch-[N].mdに記録してください。割り当てられたアウトバウンド呼び出し箇所(偵察フェーズより):
[Paste the full text of the assigned site sections here, preserving the original numbering]
コンテキスト:プロジェクトのアーキテクチャ概要が提供されます。これを使って、アプリケーションのエントリーポイント、ミドルウェア、データフローを把握してください。
SSRF の参考情報 — 確認事項:
SSRF は、サーバー側のアウトバウンドネットワーク呼び出しの接続先引数にユーザー制御の入力が到達し、サーバーが接続できる接続先に有効な許可リストが設定されていない場合に発生します。
SSRF ではないもの — 以下を SSRF として報告しないでください:
- オープンリダイレクト:ユーザー指定の URL への HTTP 302 — クライアント側のリダイレクトであり、サーバー側のリクエストではありません
- URL を介した XSS:ユーザーの URL をエスケープせずに HTML に出力 — XSS です
- IDOR:オブジェクト ID の改ざん — 別の脆弱性クラスです
- 完全にハードコードされたアウトバウンド URLで、ユーザーの影響を受けないもの — SSRF ではありません
各呼び出し箇所について、接続先の引数をさかのぼってその起点を追跡してください:
- 直接的なユーザー入力 — 接続先が、変換を介さずにリクエストの入力元から直接設定されています:
- HTTP クエリパラメーター:
request.GET.get('url')、req.query.url、params[:url]、$_GET['url']、c.Query("url")- リクエストボディ/JSON フィールド:
request.json['webhook_url']、req.body.target、params[:source]- パスパラメーター:
req.params.host、params[:endpoint]- HTTP ヘッダー:
request.headers.get('X-Forwarded-For')、req.headers['destination']- Cookie:
req.cookies.redirect_url- 間接的/組み立てられた接続先 — URL がハードコードされたプレフィックスと、ユーザーが指定したサフィックスまたはパスを連結して構築されています:
"https://example.com/" + user_path— HTTP クライアントによっては、パストラバーサルやスキームインジェクションを通じて悪用される可能性がありますbase_url + user_query— ユーザーがクエリ文字列を制御しており、インジェクションが可能な場合があります- これらは「脆弱性の可能性あり」として報告し、ユーザーが制御できる部分を記載してください
- 保存されたユーザー入力を後から取得 — 接続先が以前にユーザー入力から保存された値(例:保存済みの Webhook URL)で、現在はデータベースから取得してリクエストに使用されています:
- 保存された値が書き込まれた箇所を確認してください — 書き込み時に許可リストによる検証を行わず、ユーザー入力を受け入れていましたか?
- リクエスト実行前の読み取り時に検証されていましたか?
- サーバー側/ハードコードされた値 — 接続先が、設定、環境変数、ハードコードされた定数、またはユーザーの影響を受けないサーバー側のロジックに由来する場合、この箇所は悪用できません。
各呼び出し箇所について、緩和策も確認してください:
- 厳格なホスト/プレフィックスの許可リスト:リクエスト前に接続先を検証する、許可されたホスト名または URL プレフィックスのハードコードされた一覧 — これは有効な緩和策です。「脆弱性なし」と判定してください。
- スキームのみの制限(例:
https://のみ許可):部分的な緩和策です — 影響は軽減しますが、任意の HTTPS ホストへの SSRF は防げません。「脆弱性の可能性あり」として報告してください。- プライベート IP 範囲/メタデータエンドポイントのブロックリスト:
169.254.169.254、10.0.0.0/8、192.168.0.0/16など — 十分ではありません。DNS リバインディング、別表記の IP アドレス、リダイレクトチェーンで回避できます。「脆弱性の可能性あり」として報告してください。- DNS 解決+IP チェック(先にホスト名を解決し、解決された IP をブロックリストと照合する):単純なブロックリストより強力ですが、チェックからリクエストまでの間の DNS リバインディング(TOCTOU)の影響を受ける可能性があります。同じ解決済み IP がリクエストに明示的に固定されていない限り、「脆弱性の可能性あり」として報告してください。
このプロジェクトの技術スタックにおける脆弱/安全な例:
[TECH-STACK EXAMPLES]
分類:
- 脆弱:ユーザー入力が有効な緩和策(許可リスト、またはブロックリスト/スキームチェックのみ)のないアウトバウンドリクエストの接続先に到達することが明らかである
- 脆弱性の可能性あり:ユーザー入力が接続先に到達する可能性が高い(間接的なデータフローまたは部分的な構築)、または弱い緩和策(ブロックリスト、スキームのみのチェック、部分的な URL プレフィックス)しかない
- 脆弱性なし:接続先が完全にサーバー側で決まる、またはリクエスト前に厳格なホスト/プレフィックスの許可リストが適用される
- 手動レビューが必要:接続先の起点を確実に判断できない(内容が不明なヘルパー、複雑な条件分岐、URL を解決する外部ライブラリなど)
出力形式 —
sast/ssrf-batch-[N].mdに記録してください:# SSRF バッチ [N] の結果 ## 検出事項 ### [脆弱] 説明的な名前 - **ファイル**:`path/to/file.ext`(行 X-Y) - **エンドポイント/関数**:[ルートまたは関数名] - **問題**:[例:「HTTP クエリパラメーター `url` が直接 requests.get() に渡される」] - **汚染トレース**:[エントリーポイントから呼び出し箇所までの手順 — 例:「request.args.get('url') → target_url → requests.get(target_url)」] - **影響**:[攻撃者が実行できること — 169.254.169.254 のクラウドメタデータへのアクセス、内部サービスへの侵入、内部ネットワークのポートスキャン、データの流出、ファイアウォールの回避など] - **緩和策の有無**:[なし/ブロックリストのみ/スキームチェックのみ — 不十分な理由を説明] - **修正方法**:[厳格なホスト許可リストを設定する、または接続先に対するユーザーの制御を完全に取り除く] - **動的テスト**:[検出事項を確認するための curl コマンドまたはペイロード。 パラメーター、ペイロード、確認する内容を示してください。 例:curl "https://app.example.com/fetch?url=http://169.254.169.254/latest/meta-data/" または内部への侵入の場合:curl "https://app.example.com/fetch?url=http://internal-db:5432/"]
### [脆弱性の可能性あり] 説明的な名前 - **ファイル**:`path/to/file.ext`(行 X-Y) - **エンドポイント/関数**:[ルートまたは関数名] - **問題**:[例:「部分的にハードコードされた URL のパス部分をユーザーが制御できる」または「保存時に許可リスト検証を受けずに Webhook URL が登録される」] - **汚染トレース**:[不確実または部分的な制御のステップを特定した、可能な範囲でのトレース] - **懸念事項**:[リスクが残る理由 — 例:「攻撃者がパストラバーサルを介して内部ホストへリダイレクトできる可能性がある」または「ブロックリストは DNS リバインディングで回避できる」] - **修正方法**:[厳格な許可リストを設定する、またはユーザーによる制御を取り除く] - **動的テスト**:[試行するペイロード — 例:パストラバーサルまたは DNS リバインディングのシナリオ]
### [脆弱性なし] 説明的な名前 - **ファイル**:`path/to/file.ext`(行 X-Y) - **エンドポイント/関数**:[ルートまたは関数名] - **理由**:[例:「URL が完全にハードコードされている」または「リクエスト前に厳格なホスト許可リストが適用される」] ### [手動レビューが必要] 説明的な名前 - **ファイル**:`path/to/file.ext`(行 X-Y) - **エンドポイント/関数**:[ルートまたは関数名] - **不確実な点**:[接続先の起点を特定できない理由] - **提案**:[手動で追跡する対象 — 例:「helpers.py の `resolve_target()` を追って、URL の起点を確認する」]
フェーズ 3:マージ — バッチ結果の統合
すべてのフェーズ 2 のバッチサブエージェントが完了したら、sast/ssrf-batch-*.md ファイルをすべて読み込み、1 つの sast/ssrf-results.md に統合します。これをオーケストレーターであるあなたが直接実行します。サブエージェントは不要です。
マージ手順:
sast/ssrf-batch-1.md、sast/ssrf-batch-2.md、…のファイルをすべて読み込みます。- 各バッチファイルからすべての検出結果を収集し、元の分類とすべての詳細フィールドを保持したまま、1 つのリストにまとめます。
- すべてのバッチの合計数を集計し、エグゼクティブサマリーに記載します(分析対象サイトの合計数は、偵察結果の数、または割り当てられたサイト数の合計と一致します)。
- 次の形式で、統合したレポートを
sast/ssrf-results.mdに書き込みます。
# SSRF Analysis Results: [Project Name]
## Executive Summary
- Outbound call sites 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.]
sast/ssrf-results.mdの書き込み後、すべての中間バッチファイル(sast/ssrf-batch-*.md)を削除します。
重要な注意事項
sast/architecture.mdを読み込み、その内容をコンテキストとしてすべてのサブエージェントに渡します。- フェーズ 2 はフェーズ 1 の完了後に実行する必要があります。偵察結果に依存するためです。
- フェーズ 3 はすべてのフェーズ 2 のバッチ完了後に実行する必要があります。すべてのバッチ出力に依存するためです。
- バッチサイズは、サブエージェントごとに外部への呼び出し箇所 3 件です。サイトが合計 1~3 件の場合は、サブエージェントを 1 つ使用します。10 件の場合は、サブエージェントを 4 つ使用します(3+3+3+1)。
- すべてのバッチサブエージェントを並列で起動します。順番に実行してはいけません。
- 各バッチサブエージェントに渡すのは、偵察ファイル全体ではなく、割り当てられたサイトのテキストだけです。これにより、各サブエージェントのコンテキストを小さく保ち、対象を絞れます。
- フェーズ 1 は構造の分析のみです。宛先の引数が動的(変数、式、または組み立てられた文字列)である呼び出し箇所は、ユーザー入力が流入するかどうかにかかわらず、すべて検出します。フェーズ 1 ではユーザー入力の追跡を試みないでください。それはフェーズ 2 の役割です。
- フェーズ 2 は汚染解析のみです。バッチ内の各箇所について、宛先の引数をその起点まで追跡します。実効性のある許可リストがない状態で、ユーザーが制御できるソースに由来する場合、その箇所は実際の脆弱性です。
- ブロックリストは緩和策ではありません。プライベートレンジやクラウドのメタデータエンドポイントに対する IP ブロックリストは、簡単に回避できます。そのような箇所は必ず「脆弱」または「脆弱の可能性が高い」と分類し、安全とは判定しないでください。
- URL の一部だけを制御できる場合も危険です。攻撃者が URL のパス部分またはクエリ文字列部分しか制御できない場合でも、「脆弱の可能性が高い」として検出してください。HTTP クライアントの動作、リダイレクトの追跡、対象サービスによっては、一部の制御だけでも十分な場合があります。
- 保存された宛先も汚染されています。URL またはホスト名が書き込み時にユーザー入力から受け入れられ、その後の外部リクエストに使用される場合は、書き込み時の受け入れ処理まで追跡してください。書き込み時に許可リストによる検証がない場合、SSRF となります。
- サブプロセスでの curl/wget も SSRF です。ユーザー提供の URL を指定して
curlまたはwgetを実行するシェル呼び出しも、HTTP クライアントの呼び出しと同じくらい危険です。特に画像処理、インポート、ダウンロード機能で確認してください。 - 判断に迷う場合は、「脆弱ではない」ではなく「手動レビューが必要」と分類してください。セキュリティ評価では、偽陰性は偽陽性よりも深刻です。
- DNS リバインディングに関する注意:DNS 解決後にブロックリストで確認するだけの検出結果では、TOCTOU の時間枠を検出結果に明記してください。これは既知の回避手法です。
- 中間ファイルをクリーンアップします。最終的な
sast/ssrf-results.mdの書き込み後に、sast/ssrf-recon.mdとすべてのsast/ssrf-batch-*.mdファイルを削除してください。