GraphQL Batching Attacks: 100 Queries = 10,000 DB Calls 📊

はじめに:GraphQLの最も便利な機能に潜む危険
GraphQLは、現代アプリケーションのデータクエリ方法を革新し、前例のない柔軟性と効率性を提供します。しかし、その力には多くの開発者が見落としがちな重大なセキュリティ脆弱性、バッチ攻撃があります。単なるHTTPリクエストに見えるものが、静かに何千ものデータベース操作に変わり、インフラ全体を破綻させる可能性があります。
この包括的ガイドでは、攻撃者がGraphQLのバッチ機能を悪用して攻撃を指数関数的に拡大させる方法、配列入力を許可することでリソース枯渇の悪夢に変わる理由、そしてこれらの壊滅的な攻撃からAPIを守る方法について解説します。
GraphQLバッチの理解:両刃の剣
GraphQLバッチとは
GraphQLバッチは、2018年6月に公開されたGraphQL仕様の6.3.1節に記載された機能で、複数のクエリを1つのGraphQLリクエストで送信できる仕組みです。この技術は、複数のデータリクエストを1つのHTTPコールにまとめることでネットワークのオーバーヘッドを削減するために設計されました。
ネットワーク上では1つのバッチだけが通過し、その後すべてのクエリが逐次実行されます。正当なユーザーにはパフォーマンス向上をもたらしますが、悪意のある攻撃者にとっては巨大な攻撃面となります。
2種類のバッチ攻撃
GraphQLは、主に2つのバッチ方法をサポートしており、それぞれにセキュリティ上の影響があります:
1. 配列ベースのバッチ
配列ベースのバッチは、攻撃者が複数の操作を1つのHTTPリクエストで送ることを可能にします。ただし、配列はすべての場所でサポートされているわけではありません。サポートされている場合、リクエストは次のように構築できます:
[
{"query": "mutation { login(username: \"user1\", password: \"pass1\") { token } }"},
{"query": "mutation { login(username: \"user2\", password: \"pass2\") { token } }"},
{"query": "mutation { login(username: \"user3\", password: \"pass3\") { token } }"}
]
2. エイリアスベースのバッチ
エイリアスはGraphQL仕様の2.5節に記載されており、APIにエイリアスが利用可能な場合は期待できます。ただし、エイリアスには制約があり、クエリまたはミューテーションのどちらか一方にしか適用できません。
攻撃者はエイリアスを利用して、複数のエイリアスされたクエリを連続して実行させるバッチを作成できます。
攻撃増幅の数学
1リクエストから10,000のデータベース呼び出しへ
バッチ攻撃の指数関数的な性質は非常に危険です。次のシナリオを考えてみましょう:
従来の攻撃(バッチなし): - 1 HTTPリクエスト = 1データベースクエリ - 10,000パスワードを試すには = 10,000リクエスト - レートリミッターやWAFに検知されやすい
バッチ攻撃: - 1 HTTPリクエスト = 100〜1,000データベースクエリ - 10,000パスワードを試すには = 10〜100リクエスト - 従来のセキュリティツールには見えない
これにより、API呼び出し回数の制限を回避しやすくなります。1つのAPI呼び出しで10,000のデータベースリクエストを生成できるとしたら、ファイアウォールやレートリミッターは異常な活動を検知できません:API呼び出しだけを監視しているからです。
実世界への影響:複雑性の爆発
1つのネットワーク呼び出しが多数のクエリやオブジェクトリクエストに変わると、アプリケーションとバックエンドデータベース間の通信がCPUやメモリの枯渇を引き起こし、正規ユーザがアクセスできなくなる可能性があります。
特にネストされたクエリでは問題が深刻です。例として、ユーザがグループに属し、グループがユーザを含むソーシャルメディアプラットフォームを考えましょう。次のようなクエリを攻撃者が作成できます:
query {
group(id: "123") {
users(limit: 100) {
groups(limit: 100) {
users(limit: 100) {
groups(limit: 100) {
name
}
}
}
}
}
}
各レベルに100のリミットを設定した場合、4層のネストで合計100×100×100×100=1億の結果を取得しようとし、データベースに大きな負荷をかけます。
攻撃の経路:バッチ攻撃の現れ方
1. ブルートフォース認証回避
複数の操作を1つのリクエストにまとめることで、攻撃者はバッチ攻撃を組織し、レートリミットを回避しようとします。
例:GraphQLのバッチ機能を利用したブルートフォース攻撃で、複数のクエリを1つのHTTPリクエストで送信し、パスワードを推測し、トークンを取得して完全なアクセスを得る。
攻撃例:
[
{"query": "mutation { login(email: \"victim@example.com\", password: \"password123\") }"},
{"query": "mutation { login(email: \"victim@example.com\", password: \"123456\") }"},
{"query": "mutation { login(email: \"victim@example.com\", password: \"qwerty\") }"},
// ... 997回の試行
]
1,000回のログイン試行を含むリクエストは、監視システムには1つのHTTP呼び出しとして見え、レートリミットを完全に回避します。
2. 2段階認証(2FA)回避
最も深刻な脆弱性の一つは、2段階認証を回避できる点です。GraphQLバッチ攻撃を利用して、OTP(ワンタイムパスワード)のすべてのバリアントを1つのリクエストで送信し、認証を突破します。
OTPコードは通常、6桁で100万通りの組み合わせがあり、攻撃者は次のことが可能です: 1. OTP生成をトリガー 2. すべての可能なコードをバッチ送信 3. 全ての試行を同時に処理 4. 正しいコードが見つかれば認証成功
この脆弱なWebアプリは、すべての”ワンタイム”トークンを一度に処理し、有効なものを見つけて攻撃者を内部にログインさせました。
3. サービス拒否(DoS)攻撃
GraphQLはクエリのバッチをサポートし、一連のクエリを一度に送信します。これは、1つのHTTPリクエストに複数のGraphQLクエリが含まれ、バックエンドでは逐次処理されることを意味します。この処理は、1つのネットワーク呼び出しが大量のクエリやオブジェクトリクエストに変わるため、アプリケーションレベルのDoSにつながる可能性があります。
13,000のGraphQL APIの問題から得られた知見によると、80%はアクセス制御や認証、入力検証、レートリミットの実装によって解決可能です。
4. データ列挙とスクレイピング
バッチ攻撃は、ユーザ情報(名前、メールアドレス、アカウント)をターゲットにしたブルートフォースやオブジェクト列挙攻撃の新たな経路をもたらします。
攻撃者は次の情報を効率的に列挙できます: - ユーザアカウント - メールアドレス - リソースID - プライベートデータ(連続ID推測による)
最近の調査では、GraphQLを用いたAPIが、”シーケンス番号”を提供することでPHI(患者の健康情報)を取得可能だった事例もあります。このシーケンス番号はアカウントごとにランダムに生成されていましたが、長いUUIDではなく、容易に列挙可能でした。
なぜ従来のセキュリティ対策は失敗するのか
レートリミットの問題
従来のレートリミットはHTTPリクエスト単位で動作し、一定期間内のAPI呼び出し回数をカウントします。しかし、これでは次のような問題があります:
外部のレート監視ツールは、すべての攻撃を検知できません。なぜなら、APIリクエストが何千もの悪意のあるリクエストを内包していても、リクエスト数だけをカウントしているからです。
従来のセキュリティの見方:
クライアントIP: 192.168.1.100
リクエスト数(分間): 5
正常と判断
実態:
クライアントIP: 192.168.1.100
HTTPリクエスト: 5
実際の操作数: 5,000
データベースクエリ: 50,000
攻撃中
WAFとファイアウォールの盲点
Webアプリケーションを守るツール(WAFやRASPs)は、各APIリクエストに悪意のある複数のリクエストが詰まっている場合、その異常を検知しづらいです。
最新のWebアプリケーションファイアウォールはHTTPトラフィックパターンを解析しますが、GraphQLクエリの意味論を深く理解できず、実際の計算コストを把握できません。
監視のギャップ
したがって、一般的な防御策は、ブルートフォース攻撃を検知・阻止できません。攻撃者は、API呼び出し1回で何百回もの試行を行い、ユーザ資格情報(メール、パスワード)を見つけ出すことが可能です。OTPのトークンバリアントも同様に送信可能です。
総合的な防御戦略
1. クエリの複雑さ分析を導入
複雑さは、クエリ内のフィールド数です。各フィールドのデフォルトの複雑さは1ですが、計算コストに応じてカスタム値を設定します:
const schema = Schema.build(Query, EmptyMutation, EmptySubscription)
.limit_complexity(100) // 最大複雑さ
.finish();
クエリの複雑さを定義し、最大複雑さを超えるクエリを制限します。各フィールドの複雑さを数値で定義するのです。
実装例:
field :posts, [PostType], null: false do
argument :limit, Integer, required: false, default_value: 10
complexity ->(ctx, args, child_complexity) {
args[:limit] * child_complexity
}
end
2. クエリ深度制限の強制
クエリの深さを制限することで、過度に複雑でリソースを大量に消費するクエリの実行を防ぎます。
このライブラリは、クエリとミューテーションの合計深さを検証します。深さ制限を超えるクエリやミューテーションはバリデーションエラーとなります。
例:
import depthLimit from 'graphql-depth-limit';
app.use('/graphql', graphqlHTTP({
schema,
validationRules: [depthLimit(10)]
}));
例として、最大深さ3に設定されたサーバーでは、それを超えるクエリは無効とみなされます。
3. バッチ操作の制限
バッチできる操作数を制限することも、DoSにつながるGraphQLバッチ攻撃を防ぐ一つの方法です。これは万能ではなく、他の対策と併用すべきです。
ベストプラクティス: - 配列ベースのバッチは5〜10操作に制限 - クエリごとのエイリアス数(例:最大20)を制限 - HTTPリクエストのボディサイズ制限を実施
4. 機密操作のバッチ禁止
ユーザ名、メール、パスワード、OTP、セッショントークンなど、ブルートフォース攻撃を受けやすい操作に対してバッチを無効化します。これにより、攻撃者はREST APIのように個別のネットワーク呼び出しを行う必要があります。
保護すべき重要操作: - 認証ミューテーション - パスワードリセット - OTP検証 - 支払い処理 - アカウント変更
5. 操作レベルのレートリミット導入
HTTPレベルのレートリミットを超えて、操作ごとのスロットリングを行います:
const rateLimits = {
login: { max: 5, window: '1m' },
verifyOTP: { max: 3, window: '5m' },
resetPassword: { max: 3, window: '1h' }
};
これにより、バッチされた操作でも合計回数に基づき制限がかかります。
6. クエリタイムアウトの導入
クエリの実行時間を制限する”セキュリティタイムアウト”も有効です。過度に負荷のかかるリクエストを実行前にブロックするのではなく、実行時間を監視し、長すぎる場合は停止します。
複数レベルでタイムアウトを設定: - GraphQLリゾルバレベル:1〜2秒 - クエリ実行全体:5〜10秒 - HTTPリクエスト:最大30秒
7. GraphQLセキュリティツールの活用
Escapeでは、JavaScriptのGraphQL実装向けにオープンソースのプラグイン「GraphQL Armor」を開発し、セキュリティのベストプラクティスをデフォルトで提供しています。
推奨ツール: - GraphQL Armor:包括的なセキュリティプラグイン - graphql-depth-limit:深さ制限ライブラリ - graphql-query-complexity:複雑さ分析 - Escape Security Scanner:自動脆弱性検出
8. 適切な入力検証の実施
80%の問題は、アクセス制御や認証、入力検証、レートリミットの実装によって解決可能です。
検証リスト: - 引数範囲の検証(例:limit:1〜100) - 全ユーザ入力のサニタイズ - データ型制約の強制 - 過剰に長い文字列の拒否 - 配列サイズの制限
9. クエリパターンの監視とログ記録
攻撃パターンを検知するために包括的なロギングを実施:
class LogQueryComplexity {
result() {
const complexity = super();
logger.info(`[GraphQL] Complexity: ${complexity}, Depth: ${depth}, Operations: ${operations}`);
}
}
疑わしいパターンにアラート: - クエリの複雑さの急増 - 連続した認証失敗 - 異常なバッチパターン - 悪意のあるアクターからのクエリ
10. 本番環境での永続化クエリの利用
信頼できるドキュメントのみを受け入れることを強く推奨します。Facebookも内部で実施しています。
許可されたクエリのホワイトリストを作成: - 正当なクエリを事前登録 - 本番環境ではアドホッククエリを拒否 - クエリIDを利用し、クエリ文字列を隠す - バージョン管理を厳格に
セキュリティ設定のベストプラクティス
デフォルト設定のセキュリティ強化
多くのGraphQL実装は、デフォルトで安全でない設定になっているため、変更が必要です:過剰なエラーメッセージを返さないこと。
本番用設定チェックリスト:
const secureConfig = {
// 複雑さと深さ
maxComplexity: 100,
maxDepth: 7,
maxListDepth: 3,
// バッチ制限
maxBatchSize: 10,
maxAliases: 15,
// タイムアウト
queryTimeout: 5000,
resolverTimeout: 1000,
// セキュリティ機能
introspection: false, // 本番環境では無効化
playground: false, // 無効化
debug: false, // スタックトレース非表示
// レートリミット
rateLimit: {
window: '15m',
max: 100,
skipSuccessfulRequests: false
}
};
エラーハンドリング
本番のGraphQL APIは詳細なスタックトレースやデバッグモードを避けるべきです。エラー制御にはミドルウェアを利用し、エラー内容をマスクします。開発者には詳細を見せつつ、API利用者には一般的なエラーメッセージを返すのが望ましいです。
例:
{
"errors": [{
"message": "リクエスト処理中にエラーが発生しました",
"extensions": {
"code": "INTERNAL_SERVER_ERROR"
}
}]
}
詳細エラーはサーバ側でログに記録します。
GraphQL APIのセキュリティテスト
セキュリティテストチェックリスト
バッチ攻撃テスト
- 1リクエストで100以上の操作を送信
- 配列ベースのバッチ制限をテスト
- エイリアス制限を検証
認証テスト
- バッチコードによるOTP回避を試行
- バッチによるパスワードブルートフォースをテスト
- 2FA保護の検証
DoSシミュレーション
- 深くネストされたクエリを送信
- 高複雑度のクエリをテスト
- タイムアウト機能の動作確認
列挙テスト
- ユーザIDの列挙を試行
- メールアドレスの検証
- リソースアクセス制御の確認
自動セキュリティスキャン
Escapeでは、GraphQL APIに特化したスキャナーを開発し、APIエンドポイントの問題点を迅速に特定し、修正例を提供します。15分以内に完了し、複雑な連携やトラフィック監視は不要です。
定期的に自動ツールを利用: - 週次のセキュリティスキャン - デプロイ前のテスト - 本番環境での継続監視 - 定期的なペネトレーションテスト
実例:ケーススタディ
ケーススタディ1:医療APIの情報漏洩
最近の調査で、GraphQLを用いたAPIが、”シーケンス番号”を提供することでPHI(患者の健康情報)を取得できることが判明しました。この番号はアカウントごとにランダム生成されていましたが、長いUUIDではなく、容易に列挙可能でした。
攻撃手法: - 9桁のシーケンス番号(10^9通り) - バッチにより攻撃回数を1億から10万に削減 - Turbo Intruderと最適化されたバッチを使用 - 患者記録を列挙成功
教訓: “推測しにくい”識別子だけに頼らず、適切な認証と操作レベルのレートリミットを実施すべきです。
ケーススタディ2:ECサイト
複数のリクエストを短時間にまとめてデータ取得を行うバッチ技術は一般的です。
この手法を悪用し、アカウント乗っ取りが行われました: - 攻撃者はHTTPリクエストごとに1,000のパスワード試行 - レートリミットは見逃し、合計500以上のアカウントが侵害 - 不正取引は2百万ドル超に
対策: 操作レベルのレートリミット導入と認証ミューテーションのバッチ無効化
GraphQLセキュリティの未来
新たな標準とベストプラクティス
コミュニティはセキュリティ改善に積極的です:
標準化されたセキュリティ指令
- スキーマレベルのセキュリティ宣言
- 組み込みのレート制限サポート
- ネイティブの複雑さ分析
強化されたツール群
- 静的解析ツール
- 自動セキュリティテスト
- リアルタイム脅威検知
コミュニティ教育
- セキュリティ優先のGraphQLコース
- オープンソースのセキュリティライブラリ
- 脆弱性データベースの共有
まとめ:パワーとセキュリティのバランス
GraphQLのバッチ攻撃は、現代APIのセキュリティにおいて根本的な課題です。柔軟性とパワーを維持しつつ、攻撃の指数関数的拡大を防ぐにはどうすれば良いでしょうか?
アプリの技術スタックにGraphQLが含まれる場合、新たな攻撃面が生まれます。最初の防御策は、安全なコーディングです。仕様に忠実に従い、特定のメソッド実装時にはデータフィルタリングやレートリミットに注意を払う必要があります。
重要ポイント:
- リスクを理解する:単一のGraphQLリクエストが何千ものデータベース操作を引き起こす
- 多層防御:複数の対策を併用
- 継続監視:クエリの複雑さ、深さ、操作数を追跡
- 定期テスト:自動セキュリティスキャンをCI/CDに組み込む
- 最新情報の把握:GraphQLのセキュリティは進化中、最新のベストプラクティスを追う
これらの防御策を実施することで、GraphQLの強力な機能を活かしつつ、バッチ攻撃やリソース枯渇からインフラを守ることができます。セキュリティは一度の設定ではなく、継続的な監視、テスト、改善のプロセスです。
GraphQLのバッチの便利さは、セキュリティを犠牲にする必要はありません。適切な対策を講じれば、パフォーマンスと保護の両立が可能です。
追加リソース
- OWASP GraphQL Cheat Sheet
- GraphQL Armor Security Plugin
- GraphQL Security 2024レポート
- GraphQL Depth Limiter Library
キーワード: GraphQLセキュリティ, バッチ攻撃, APIセキュリティ, クエリ複雑さ, DoS防止, GraphQL脆弱性, レートリミット, 認証回避, 2FAセキュリティ, GraphQLベストプラクティス
Related InstaTunnel pages
Continue from this article into the most relevant product guides and workflows.
Related Topics
Keep building with InstaTunnel
Read the docs for implementation details or compare plans before you ship.