Directive Deception: Exploiting Custom GraphQL Directives for Logic Bypass

現代のAPI環境において、GraphQLはしばしば「RESTキラー」と称され、開発者に比類なき柔軟性と効率性を提供します。しかし、ことわざにもあるように、大きな力には多くの偶発的な失敗の可能性も伴います。GraphQLエコシステムの中で最も洗練されているが見落とされがちな攻撃対象の一つがDirectiveです。
標準のディレクティブ(例:@skipや@include)は仕様に組み込まれていますが、真の危険性と有用性はカスタムディレクティブにあります。@auth、@cache、@logなどのパワフルなアノテーションは、多くの場合セキュリティミドルウェアとして機能しますが、誤設定されると完全にバイパスされる可能性があります。
この深掘りでは、「Directive Deception」— GraphQLディレクティブを悪用してロジックをバイパスし、セキュリティチェックを回避し、リソースの枯渇を引き起こす手法について解説します。
GraphQLディレクティブの構造:両刃の剣
まず、ディレクティブの仕組みを理解しましょう。ディレクティブは@記号に続く識別子で、GraphQLクエリやスキーマのほぼ任意の部分に付加できます。
1. スキーマディレクティブと操作ディレクティブ
スキーマディレクティブ: サーバー側で型やフィールドに付与される(例:field: String @auth(role: "ADMIN"))。実行時に特定のロジックをトリガーするために使われます。
操作ディレクティブ: クライアントからクエリ内で送信される(例:query { user @include(if: $isMe) { name } })。
脆弱性は、カスタムディレクティブがミドルウェアのラッパーやビジターパターンとして実装され、クエリドキュメントを検査する場合に生じます。これらのディレクティブを処理するロジックがGraphQLのAST(抽象構文木)の複雑さを考慮していないと、攻撃者は「盲点」を突くことができます。
脆弱性1:@authのロジックスキップによるバイパス
多くのチームは、カスタム@authディレクティブを用いてフィールドレベルのセキュリティを実装しています。これはエレガントで、スキーマ内のフィールドにタグ付けし、「Directive Transformer」が認証済みユーザーだけが見られるようにします。
攻撃手法:”Ghost”ディレクティブ
問題は、サーバー側のコードがフィールドノードのディレクティブだけをチェックし、Inline FragmentsやFragment Definitionsを見落とす場合に発生します。
例として、次のクエリを考えます:
query BypassingAuth {
sensitiveData @auth(role: "ADMIN") # ミドルウェアはこれを検知しブロック
}
これを次のように偽装します:
query StealthyBypass {
... on Query {
sensitiveData # ミドルウェアがトップレベルのフィールドだけを検査している場合、これが通る可能性
}
}
ディレクティブ処理ロジックが再帰的でなかったり、フラグメントを解決していないと、@authのロジックはトリガーされません。実行エンジンは単に解決すべきフィールドを見て、セキュリティゲートをスキップしてデータベースに進みます。
なぜ起こるのか
開発者はしばしば「Schema Visitors」を使ってリゾルバをラップしますが、ビジターがfieldDefinitionだけを見ていて、クライアントがフラグメント内でフィールドを再宣言する可能性を考慮していない場合、ラッパーは特定の実行パスに適用されません。
脆弱性2:ディレクティブインジェクションと引数操作
「ディレクティブインジェクション」は、SQLインジェクションのGraphQL版とも呼ばれ、アプリケーションが未検査のユーザー入力を使って動的にGraphQLクエリ文字列を構築する際に発生します。
シナリオ:Backend-for-Frontend(BFF)の罠
ユーザーの「ソート」設定を受け取り、それをバックエンドのGraphQLクエリに注入するBFFを想定します:
const query = `query { products(sort: "${userInput}") { id name } }`;
攻撃者は単にソート文字列を送るだけでなく、次のような値を送ることも可能です:
"price") @include(if: true) @customDirective(arg: "malicious") #
結果のクエリはこうなります:
query { products(sort: "price") @include(if: true) @customDirective(arg: "malicious") #") { id name } }
引数を”エスケープ”し、自分のディレクティブを注入することで、攻撃者は以下を行えます:
- ロジックの上書き:
@skip(if: true)を注入し、重要なフィールドをログから隠しつつミューテーションを実行 - 内部ディレクティブのトリガー:
@internalDebugや@bypassCacheなどの内部専用ディレクティブを呼び出す
脆弱性3:ネストされたフラグメントと選択セットの回避
ディレクティブは、データマスキングやPII(個人識別情報)の保護に使われることがありますが、GraphQLのネストされたフラグメントのサポートは”マスキングミスマッチ”を引き起こす可能性があります。
セキュリティディレクティブが高レベルに適用されていても、攻撃者が深くネストされたフラグメントを使って同じフィールドを異なるリレーションシップ経由で参照した場合、高レベルのディレクティブは継承されないことがあります。
「循環参照」エクスプロイト
多くのスキーマでは、PostからUserに到達し、UserからPostに戻ることができます。
@authがUser型のUser.emailフィールドにだけ適用されている場合でも、Author型(Userオブジェクトを返す)に適用し忘れると、攻撃者は長回りしてアクセスできます:
query DeepEvasion {
post(id: "1") {
author { # 'author'リゾルバが'User'ディレクティブロジックをトリガーしない場合
email
}
}
}
脆弱性4:キャッシュ操作とリソース枯渇 🌀
@cacheディレクティブはパフォーマンス向上に役立ちますが、これを悪用してサービス拒否(DoS)攻撃に利用することも可能です。攻撃者はディレクティブを使って高負荷な未キャッシュ処理を強制させることができます。
強制キャッシュミス
@cache(ttl: 3600)ディレクティブを使っている場合、サーバーはクエリハッシュや引数に基づいてキャッシュキーを生成します。攻撃者はディレクティブのオーバーロードや引数のジッタリングを使い、すべてのリクエストをキャッシュミスにできます。
query Exhaustion($random: String) {
heavyReport(id: "123") @cache(ttl: 0) # デフォルトTTLを上書き
alias1: heavyReport(id: "123", dummy: $random)
alias2: heavyReport(id: "123", dummy: $random)
}
ダミー引数にランダムな文字列を渡したり、キャッシュを無効化するディレクティブを注入することで、攻撃者はheavyReportの解決(高負荷な処理)を複数回強制します。
複雑さの乗数
サーバーがクエリの複雑さを計算する場合、ディレクティブはしばしば”コスト”が0に設定されます。しかし、@transformImage(size: "ultra-hd")のようなカスタムディレクティブは計算コストが高いことがあります。
攻撃手法: 攻撃者は何百ものエイリアスフィールドを含むクエリを送り、それぞれに”低コスト”だが”高インパクト”なディレクティブを付与します。
クエリの複雑さは次のように計算されるとします:
$$Complexity = \sum_{i=1}^{n} (FieldCost_i + DirectiveCost_i)$$
DirectiveCostが誤って0に設定されていると、実際のサーバ負荷は$O(n \cdot ActualImpact)$となり、即座にリソース枯渇を引き起こします。
戦略的対策:スキーマを守る方法 🛡️
ディレクティブのセキュリティを確保するには、「浅い」ミドルウェアから「深い」スキーマ優先のセキュリティへ移行する必要があります。
1. 統一されたディレクティブ変換
トップレベルの選択セットだけをスキャンするミドルウェアに頼らず、ディレクティブトランスフォーマーを使ってリゾルバマップ自体を変更します。スキーマレベルでリゾルバをラップすることで、クエリの方法(フラグメント、エイリアス、深いネスト)に関係なくセキュリティロジックが追従します。
2. 厳格なディレクティブ検証
クライアントが送信できるディレクティブを制限します。
- ホワイトリスト化:
@includeと@skipのみ許可 - スキーマのみ:
@authのようなセキュリティディレクティブはスキーマディレクティブに限定し、操作文字列内でクライアントが提供・上書きできないようにします。
3. ディレクティブの複雑さマッピング
すべてのカスタムディレクティブに非ゼロのコストを割り当てます。データベースルックアップや重い変換を行うディレクティブは、そのコストを反映させる必要があります。
セキュリティのヒント: graphql-cost-analysisのようなコスト分析ライブラリを使い、ディレクティブに対して乗数を定義します。
4. 永続化クエリ(究極の防御策)
「ディレクティブインジェクション」や「クエリ操作」を防ぐ最も効果的な方法は、永続化クエリを使用することです。事前に登録されたクエリハッシュのみを実行させることで、攻撃者は悪意のあるディレクティブや深いネストのDoSクエリを作成できなくなります。
5. 本番環境でのイントロスペクション無効化
根本的な解決策ではありませんが、イントロスペクションを無効にすることで、攻撃者がカスタムディレクティブを「マッピング」し、脆弱なものを見つけるのを難しくします。
結論
GraphQLのディレクティブは拡張性の高い設計の一例ですが、セキュリティが隠れやすい抽象層を導入し、攻撃者が好むターゲットとなっています。「Directive Deception」は単なるバグの問題ではなく、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.