hunt-spa-api について
hunt-spa-apiは、シングルページアプリのバンドルからAPIホストやルートを見つけ、未認証アクセスを調べるセキュリティテスト用スキルです。保護されたエンドポイントを対照にし、必要最小限の証拠で検証します。
主な機能
- 01JavaScriptバンドルからAPIホストとルートを抽出
- 02遅延読み込みチャンクを検出
- 03未認証エンドポイントを対照と比較
- 04最小限の証拠でアクセス制御の差を確認
活用シーン
- 01ログイン画面の背後にあるAPIの独立した認証を確認
- 02SPAコードから未公開APIルートを整理
- 03ルートグループの認証漏れをテスト
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ elementalsouls/ claude-bughunter --skill hunt-spa-apiインストール後、Claude Code で/hunt-spa-apiを入力して読み込まれたか確認します。
詳細
このスキルを使うタイミング
次の場合に実行します。
- 対象ホストが、小さな HTML シェルと大きな
/static/js/*.jsまたは/_next/static/*バンドルを返す(React/Vue/Angular/Next/Svelte の SPA) console、app、dashboard、portal、admin、panel、manage、internalという名前のサブドメインがある- 偵察で
*api*、*-api*、api.*ホストが見つかる - ログインページが OAuth/SSO で保護されている(フロントエンドの認証からは、APIで認証が強制されているかどうかは分かりません)
核心となる洞察:SPA はバックエンド API のクライアントであり、その API の全体像(ホスト、ルート、ときにはキー)をソースを見た人なら誰でも確認できる状態で配布しています。 ログインページが保護されていても、その背後にある API がトークンをチェックしているとは限りません。認証は多くの場合、ゲートウェイ/ログインでは強制されていますが、API のルートグループでは欠落しています。
「アプリにはログインが必要だから」といって、この手順を飛ばさないでください。まさにこういう場合に効果があります。
手順(5 ステップ)
1. シェルを取得し、バンドルをすべて列挙する
curl -s https://console.target.com/ -o index.html
# React/CRA:
grep -oE '/static/js/[^"]+\.js' index.html
# Next.js:
grep -oE '/_next/static/[^"]+\.js' index.html
# generic:
grep -oiE 'src="[^"]+\.js[^"]*"' index.html
すべてのバンドルをダウンロードします(数 MB あっても問題ありません。ルートの情報がすべて含まれています)。
mkdir bundles
for j in $(grep -oE '/static/js/[^"]+\.js' index.html | sort -u); do
curl -s "https://console.target.com$j" -o "bundles/$(echo "$j"|tr '/' '_')"
done
2. バンドルから API ホスト、ルート、シークレットを抽出する
B=bundles/*.js
# Backend API hosts (incl. dev/beta/staging variants — often weaker auth)
grep -ohiE 'https://[a-z0-9.-]*(api|console|backend|service)[a-z0-9.-]*\.target\.com[a-z0-9/_-]*' $B | sort -u
# Versioned API base paths
grep -ohiE '/api/v[0-9]+/?' $B | sort -u
# Route literals — minified bundles store routes as STRING segments, not full URLs.
# Grep for quoted "resource/action" strings:
grep -ohiE '"[a-z0-9_-]+/[a-z0-9_/-]+"' $B | tr -d '"' \
| grep -iE '(login|user|account|order|billing|invoice|payment|deal|report|token|otp|password|reset|admin|profile|auth|upload|export|role|permission|dashboard|wallet|finance|sales)' | sort -u
# Secrets (validate before trusting — most AIza keys are Maps/analytics, not Auth)
grep -ohiE '(AIza[0-9A-Za-z_-]{35}|AKIA[0-9A-Z]{16}|sk_live_[0-9A-Za-z]+|eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}|apiKey["'"'"']?\s*[:=]\s*["'"'"'][^"'"'"']+)' $B | sort -u
注: ミニファイアーはルートを連結された文字列の断片(例:"account/payment/list")として格納するため、完全な /api/v2/... URL ではありません。そのため、単純な /api/v* の grep では何も見つかりません。リソース名を含むルート文字列を grep し、ベースを手動で前に付けてください。
遅延読み込み(非同期)チャンク — HTML から参照されるバンドルだけで終わらせないでください。 CRA/webpack の SPA では、index.html から参照されるのは runtime、main、vendor のみです。番号付きの非同期ルートチャンクは main.js のチャンクマップ内に名前が記載され、実行時に読み込まれます。そのため、シェルから /static/js/*.js だけを grep すると、先行読み込みされるチャンクしか対象にならず、ルートの網羅性が損なわれます。
grep -oE '[0-9]+:"[a-f0-9]+"' bundles/*main*.js # {chunkId:"hash"} pairs
# reconstruct /static/js/<id>.<hash>.chunk.js, download each, re-run route/host/secret harvest
# asset-manifest.json (if present) lists them all -> hunt-source-leak
3. CONTROL を確立する — 認証で保護されているエンドポイントを探す
脆弱性があると判断する前に、保護されていると予想されるエンドポイントへ未認証リクエストを送り、正しい拒否応答の内容を記録します。
curl -s -X POST https://api.target.com/api/users -H 'Content-Type: application/json' -d '{}'
# secure → {"error":"Missing or invalid authorization header"} or HTTP 401
これが比較基準になります。兄弟関係にある API(例:2 つ目の API ホスト、または同じホスト上の別のルートグループ)が理想的な対照対象です。同じスタックであるため、応答の違いは単なる挙動の癖ではなく、実際の認可の欠落を示します。
4. 各ルートファミリーを未認証で、両方のメソッドでテストする
発見したすべてのルートに対し、Authorization ヘッダーを付けずにリクエストを送り、対照対象と比較します。
for r in <routes>; do
curl -s -o /tmp/r -w "[%{http_code}] $r\n" -X POST -H 'Content-Type: application/json' -d '{}' "https://api.target.com/api/v2/$r"
done
解釈:
401/"Missing authorization"→ 認証で保護されています(正常)。次へ進みます。- データを含む
200→ 未認証でのデータ露出です。Finding です。 400 "field X is mandatory"→ 認証チェックなしでルートがリクエストを処理し、ビジネスロジックの検証まで到達しています。→ 認証バイパスです。確認のため、必須フィールドに最小限の値を指定してください。- 詳細な DB/スタックエラーを伴う
200(例:PROCEDURE db_x.sp_y does not exist)→ 未認証でデータ層まで到達しています。SQLi の攻撃対象領域を示す兆候でもあります。 is_admin/is_internal/requested_by/role_id/account_typeのような名前の必須フィールド → クライアント指定のパラメーターに基づく認可です。権限フラグを設定するだけで権限を昇格できます。クリティカルクラスです。
5. 横展開して最小限の証拠を得る
- あるエンドポイントが返す ID(
account_id、order_id、deal_id)は、他のエンドポイントが利用するキーです。これらは、1 つのルートだけでなくルーター全体に到達可能であることを証明します。 dev-/beta-/staging-の API バリアントをテストします。認証が弱い、または無効になっていることがよくあります。- 応答ヘッダーを確認します。
Access-Control-Allow-Origin: *があると問題がさらに深刻になります(任意の Web オリジンが被害者のブラウザーからデータを読み取れます)。 - 必要最小限の証拠が得られた時点で停止してください。 数件のレコード(または
totalCount)があれば、チェックの欠落を確認できます。テーブル全体を列挙しないでください。データ最小化の境界についてはredteam-mindsetを参照してください。Finding は認証の欠落であり、データ量ではありません。
「SSO ログインの背後にある API」が本当に意味すること
よくある危険なアーキテクチャです。
console.target.com(SPA)→ ログインには Entra/Okta/Google OAuth を使用(万全に見えます)。api.target.com(バックエンド)→ 一部のルートグループではベアラートークンを必須にしていますが、ミドルウェアの適用を忘れたルートグループもあります。
API がすべてのルートでトークンを独立して検証しないなら、フロントエンドのログインは見せかけにすぎません。ログイン UI がどれほど厳重に保護されていても、API を直接、認証なしで必ずテストしてください。
アンチパターン
- 「アプリにログインが必要だから、API も保護されているはずです。」 いいえ。API を直接、認証なしでテストしてください。それが重要なのです。
- 「バンドルが難読化されていて、読めません。」 読む必要はありません。ホスト、ルート、シークレットを grep で検索してください。5 分でできます。
- 「
/api/v1/fooが 404 を返したので、ここに API はありません。」 ベースパスかメソッドが違います。/api/、/api/v2/、GET ではなく POST を試し、バンドル内の正確なルート文字列も試してください(Express の 404 はパスを返すので、それを調整に使えます)。 - 「AIza キーが見つかった → 重大なシークレットです。」 まず検証してください。大半は Maps/分析用のキーです(identitytoolkit で
CONFIGURATION_NOT_FOUNDが返る場合は、Auth が有効になっていません)。誇張した主張は避けてください。 - 200 が返ったらデータセット全体をダンプする。 PoC で止めてください(
redteam-mindset)。 - アカウント作成/書き込みエンドポイントを「証拠」にする。 読み取りエンドポイントであれば、状態を作成せずに認証の欠落を証明できます。「実演」のために
create/signup/uploadの POST を送ってはいけません。これは操作ごとの明示的な認可が必要な破壊的な書き込みです。
関連スキルと連携
hunt-api-misconfig— API のマッピングができたら、ルートごとに、より広範な設定ミスのマトリクス(メソッドの改ざん、Mass Assignment、JWT アルゴリズムの混同)を実行します。hunt-idor—account_id/order_idを使った探索は、発見したルート全体での IDOR テストに直結します。hunt-source-leak— ソースマップ(*.js.map)を使って元のソースを再構築し、ルートやシークレットをさらに抽出します。同じ収集スキルが役立ちます。hunt-nextjs— Next.js のターゲットでは、ミドルウェアのバイパス(x-middleware-subrequest)と/_next/dataのルートテストを、これに重ねて実施します。redteam-mindset— データ最小化の原則に従い、ステップ 5 ではチェックの欠落を証明するだけにとどめ、テーブルを流出させないでください。recon-scope-triage— テスト前に API ホストが本当にターゲットに属していることを確認してください(同じ名前を持つ第三者の API に侵入しないでください)。