GraphQL を保護する方法
GraphQL は、REST の代替として使用できるオープンソースのクエリ言語です。急速な進化を続ける最先端の API のニーズに応える柔軟性を実現する GraphQL は、現在多くの開発者によって着々と導入されています。GraphQL には、必要なデータをピンポイントでリクエストし、継続的な API の進化を容易にするといったメリットがあります。
このように多くのメリットがある GraphQL ですが、攻撃者によって悪用される可能性のある機能や、クエリの柔軟性によって意図せず引き起こされてしまう問題、攻撃者にとって恰好のターゲットとなる脆弱性など、その使用に伴うセキュリティの課題についてはまだよく知られていません。
このブログ記事では、これらの課題を探るとともに、GraphQL のより安全な導入を可能にするデフォルトやコントロール設定を、リスクを伴う設定、悪意のあるクエリ、そして Web API の脆弱性の3つのカテゴリーに分けてご紹介します。では、さっそく見ていきましょう。
リスクを伴う設定に関するアドバイス
GraphQL の実装には、イントロスペクション、フィールドの提案、デバッグモードといった、攻撃者によって悪用される可能性のある正規の機能があります。悪用を防ぐためにも、これらの機能の使用には注意が必要です。
1. イントロスペクションを回避する
課題 : GraphQL にはイントロスペクションと呼ばれる機能があります。つまり、引数、フィールド、型、説明、型の非推奨ステータスなど、データ構造に関する詳細情報を取得するために GraphQL スキーマをクエリすることができます。このような情報が漏えいしてしまうと、攻撃対象領域が拡大し、攻撃者にとって恰好の的となる脆弱性を提供してしまう可能性があります。
例えば、スキーマのすべての型と各型の詳細を取得したい場合、以下のようにイントロスペクションクエリを実行します。
{
__schema {
types {
name
kind
description
fields {
name
}
}
}
}
また、GraphQL の統合開発環境 (IDE) である GraphiQL (iに注意) では、ユーザーフレンドリーなインターフェースを通じてフィールドやインプットをクリックしてクエリを構築することが可能です。GraphiQL を通じてサポートされているスキーマの情報が得られるため、存在するクエリ型やミューテーション型などがさらけ出され、さらなる攻撃対象領域がリスクにさらされてしまう可能性があります。
このようにクエリを送信してサーバーを尋問することで、攻撃者は深刻度の高い複雑な攻撃を仕掛けるのに必要な情報を取得することができます。例えば、イントロスペクションを利用して、UploadFile と呼ばれるオブジェクトを見つけたとします。
{
"name": "UploadFile",
"kind": "OBJECT",
"description": null,
"fields": [
{
"name": "content"
},
{
"name": "filename"
},
{
"name": "result"
}
]
}
このオブジェクトを悪用して、Web ルートフォルダ外に保管されているファイルやディレクトリにアクセス・変更するトラバーサル攻撃という攻撃を仕掛けることができます。では、filename の引数に文字列の制限がなく、サーバーのファイルシステム内のすべてのロケーションへの記述が可能であるという想定で、poc.php というファイルを記述する目的でクエリを構築してみましょう。
mutation {
uploadFile(filename:”../../../../var/www/html/app/poc/poc.php”, content: “<?php
phpinfo(); ?>”){
result
}
}
攻撃ベクトルを通じてコールを行うと、任意のコードが実行されたことが分かります。ここから、リバースシェルを利用して基盤となるサーバー環境との通信を試みることができます。
解決策 : イントロスペクションは開発においては便利ですが、保護されている機密情報へのアクセスを提供する場合は避けた方が良いでしょう。
ユーザーに API をクエリする方法を教えるためにイントロスペクションを使いたくなるかもしれませんが、readthedocs などの別のドキュメントを使用する方が安全です。イントロスペクションを無効にすることで脆弱性が修正されるわけではありませんが、攻撃者に対して攻撃の近道を与えることを避けることができます。
GraphQL を導入する際、デフォルトでイントロスペクションが有効になっていることがよくありますが、システム全体でイントロスペクションを無効にすることが最も安全なアプローチです。幸い Ruby、NodeJS、Java、Python、PHP などの人気のフレームワークやプログラミング言語においてイントロスペクションを制限する方法に関する便利なリソースも多く存在します。また、Nuclei が提供するテンプレートを使ってご利用の GraphQL のイントロスペクションをテストすることもできます。
2. フィールド提案機能を無効にする
課題 : イントロスペクションが無効な場合、攻撃者はフィールド提案機能を利用して GraphQL スキーマにブルートフォース攻撃を仕掛ける可能性があります。フィールド提案機能は、誤ったフィールド名が入力されたクエリに対するエラーレスポンスで、似た名前を持つフィールドを公開する仕組みになっています。
例えば、名前のクエリ (query { name }
) に対して、次のようなレスポンスが返されます。
{
"errors": [
{
"message": "Cannot query field \"name\" on type \"Query\". Did you mean \"node\"?",
"locations": [
{
"line": 2,
"column": 3
}
]
}
]
}
解決策 : フィールド提案機能は、開発者が API を GraphQL と統合する、またはパブリック API に対して統合する際に役に立つ便利な機能ですが、使用には十分注意が必要です。保護された機密情報へのアクセスを提供する環境においては、この機能を無効にすることをお勧めします。
3. 「デバッグ」モードを無効にする
課題 : エラー通知は、問題が発生した際にインサイトを提供する便利な機能です。しかし、エラーが適切に処理されない場合、GraphQL においてさまざまなセキュリティ問題が引き起こされることがあります。
GraphQL は、デバッグモードで実行することが可能です。デバッグモードの主要機能は、開発に役立つ詳細なリクエストエラーを表示することです。このデバッグモードを有効にしたまま本番環境で GraphQL を実行するのは非常に危険です。この場合、スタックトレースなどの過剰なエラー通知が発生し、レスポンスにて機密情報が公開されてしまい、セキュリティ以外にもコンプライアンスの問題が生じてしまいます。
スタックトレースの例
解決策 : 本番環境では「デバッグ」モードが無効になっていることを確認し、クライアントに情報が返される前にスタックトレースを除外するようにしましょう。
スタックトレースをログしたい場合は、ユーザーに返さず、開発者のみアクセス可能にする方法がいくつかあります。例えば、リクエストの検査・変更を通じてエラーのマスキングや編集を可能にするミドルウェアの導入は、エラーをより効果的にコントロールするためによく使用される方法の1つです。
悪意のあるクエリに関するアドバイス
攻撃者は、悪意のある GraphQL クエリを作成し、サービス妨害攻撃 (DoS)、ブルートフォース攻撃、列挙攻撃などを仕掛けることができます。
4. 深さの最大値を設定する
課題 : GraphQL の各クエリの深さは、ネストされたフィールドの数およびネストされたフィールド内のオブジェクトの数で表されます。攻撃者は通常のリクエストを送信する代わりに、容易に指数関数的に複雑化するクエリを作成し、システムに過負荷をかけることでサービス妨害攻撃 (DoS) を引き起こさせます。またこの問題は、クエリの適切な作成方法を知らない正当なユーザーによって誤って引き起こされてしまうこともあります。
GraphQL で型同士が参照し合う場合、指数関数的に拡大する循環クエリが発生し、それによってリソースが拘束され、サーバーがダウンする可能性があります。
例えば、GraphQL の実装に次のように定義された循環的な依存関係があるとします。
type Blog {
comments(first: Int, after: String)
}
Type Comment {
blog: Comment
}
Type Query {
blog(id: ID!): Blog
}
この場合、ブログのコメントと、コメントのブログ両方をクエリできるようになっています。攻撃者はこれを利用して、読み込まれるオブジェクト数を指数関数的に増加させる、次のような高コストのネスト化されたクエリを作成することができます。
query nefariousQuery {
blog(id: “some-id”) {
comments(first: 9999) {
blog {
comments(first: 9999) {
blog {
# ... repeat
}
}
}
}
}
}
解決策 : クエリの深さの最大値を設定することで、深さや複雑性を利用した GraphQL 攻撃を阻止することができます。とはいえ、クエリの深さだけでは、クエリのコストを予測することができない場合があります。クエリ内に解決に時間のかかるフィールドがあれば、コンピューティングコストがかさむことがあります。
クエリコスト分析と呼ばれる手法では、フィールドにコストを割り当て、コストが高すぎるフィールドをサーバーが拒否するように設定することができます。しかし、導入前にこの対策が本当に必要であるか見極めることが重要です。高コストのネスト化された関係がサービスにない場合、またはサービスで負荷の処理が可能と予想される場合は、この対策は必要ないかもしれません。
5. バッチ化の改善
課題 : GraphQL の大きなメリットの1つはクエリのバッチ化、つまり複数のリクエストを単一のリクエストにまとめられることです。しかし、事前にセキュリティを考慮した制限を確立しなければ、GraphQL でのクエリのバッチ化が攻撃者に有利な機会を与えてしまう可能性があります。
例えば、以下のコードスニペットは user
オブジェクトの複数のインスタンスをリクエストするためのバッチ化されたクエリを示していますが、これはブルートフォース攻撃に悪用される可能性があります。
query {
user(id: "101") {
name
}
second:user(id: "102") {
name
}
third:user(id: "103") {
name
}
}
攻撃者はクエリを拡張し、単一のリクエストを介してすべての user
を列挙することができます。これは GraphQL 特有のブルートフォース攻撃の手口で、単一のリクエストで複数のオブジェクトインスタンスをリクエストできるため、検出されにくいという特徴があります。一方 REST API では、攻撃者は各オブジェクトインスタンスを取得するために個別にリクエストを送信しなければなりません。
バッチ化されたクエリは一見、単一のリクエストのように見えるため、悪用された場合、Web アプリケーションファイアウォール (WAF)、ランタイムアプリケーション自己保護 (RASP)、不正侵入検知・防御システム (IDS/IPS)、セキュリティ情報/イベント管理システム (SIEM) などの一般的なアプリケーション・セキュリティ・ツールをすり抜けてしまう可能性があります。
解決策 : GraphQL のクエリバッチ化を悪用した攻撃を効果的に阻止する方法の1つとして、オブジェクトにレート制限を追加することが挙げられます。例えば、リクエストされるオブジェクトインスタンス数を追跡し、リクエストされたオブジェクト数が制限を超えた場合、ブロックすることができます。
この種の攻撃を回避するもう1つの方法は、機密性の高いオブジェクトがバッチ化されないように制限をかけることです。そうすることで攻撃者は、オブジェクトごとにリクエストを送信しなくてはならない REST API など、他の方法を利用せざるを得なくなります。
これら2つの方法に加え、一度にバッチ化して実行できる操作数を制限するのも効果的です。
6. 入力検証を早期に実行する
課題 : GraphQL は他の API アーキテクチャと比べて、それほど大きな違いはありません。GraphQL を使用しているアプリケーションにも、私たちのよく知る一般的な脆弱性が存在する可能性があります。悪質なリクエストを防止するためには、開発者は入力を適切に検証し、サニタイズする必要があります。
例えば、GraphQL 経由で OS コマンドインジェクションの脆弱性を悪用するとします。
例として、特定の商品の在庫数を確認するためのオペレーションを実装しているオンラインリテーラーのケースを考えてみましょう。履歴上の理由から、サーバーは以下のように、itemId、vendorId,、color を引数としたシェルスクリプトを使い、レガシーシステムをクエリします。
inventorycount.sh 80 200 red
このスクリプトは、各商品の在庫数を出力し、リクエストで返します。このアプリケーションでは防御対策が実装されていないため、攻撃者によって以下のように任意のコマンドが入力される恐れがあります。
red ; env ;
それに対する GraphQL クエリは次のようになります。
query {
inventoryCount(itemId:80, vendorId:200 color:”red; env ;”,)
}
このクエリによって、シークレットやその他の機密情報を含む可能性がある環境変数の内容が返されてしまいます。
レスポンスの例
解決策 : 原則として、入力内容の検証はデータフローのできる限り初期において行うことが推奨されています。入力がサニタイズおよび検証されていても、それを利用してユーザーがデータフローをコントロールできるようではいけません。すべての受信データは GraphQL の scalar および enum のデータ型を使用して検証する必要があります。
また、より高度な検証を行うため、GraphQL のカスタムバリデーターを記述することもできます。GraphQL クエリの許可リストも、事前に許可されていないクエリを通さないようサーバーに命じることで潜在的な脅威による影響を軽減できます。それでも不安が解消されない場合や複雑性に悩まされる場合は、Fastly の次世代 WAF (Powered by Signal Sciences) の使用をお勧めします。
今後の対策
GraphQL は、API との通信における新たな技術標準となっています。そのため、GraphQL の使用に伴うセキュリティの課題や攻撃対象領域についても注意が必要です。
基本的に、安全性の高いソフトウェアを構築するには、基盤となるテクノロジーの根底にあるセキュリティ原理を理解する必要があります。GraphQL を使用するかどうかに関わらず、開発サイクルのあらゆる段階において発生しうるセキュリティ上の欠陥を認識することで、頭を悩ませる問題やインシデントが後に生じる可能性を大幅に削減できます。
セキュリティを強化するには、多層防御アプローチが最も効果的であることを、Fastly はよく理解しています。セキュリティコントロールが機能しなかった場合や、新たな脆弱性が見つかった場合などに備え、Fastly の次世代 WAF (Powered by Signal Sciences) では GraphQL のサポートをベータ版でご利用いただけます。