GraphQL-core: コメントトークンを利用したサービス拒否攻撃(パーサーの最大トークン数制限を回避)
### 概要
パーサーの `max_tokens` 制限は、オーバーサイズの GraphQL ドキュメントに対する防御策として文書化されていますが、この制限はコメントトークンをカウントしていませんでした。そのため、コメントで埋められたドキュメントは、`max_tokens` のチェックを通過する可能性があります。ただし、各コメントに対して `Token` オブジェクトが割り当てられ、それらは解析されたドキュメントの有効期間中、メモリ上に保持されます。38 MB のコメントで埋められたデータは、`max_tokens=5` の設定で、約 92 MB の追加メモリを消費します。
### 詳細
`Lexer.lookahead()` は、トークンを1つずつ読み込み、コメントをスキップし、最初のコメント以外のトークンのみを返します。スキップされた各コメントは、完全にトークン化され、`Token` オブジェクトとして割り当てられ、`token.next` / `token.prev` を介して永続的なトークンリストにリンクされます。このリンクは、ソースコードの場所の報告を可能にし、ドキュメントが有効な間は保持されます。
`Parser.advance_lexer()` は、実際にレクサーが返したトークンに対してのみトークンカウンタをインクリメントします。そのため、コメントはカウントされず、`max_tokens` は *コメント以外の* トークンのみに適用されます。
この結果、ユーザーが `max_tokens` が保護すると信じていることと、実際に保護していたことの間にはギャップが生じていました。通常のドキュメントでは、この制限は破られていませんでした。実際のトークンは正しく制限されていました。そのため、この問題は、コメントの埋め込みを特にテストしない限り、表面化しませんでした。
関連する問題と同様に、このペイロードは非常に効率的に圧縮されます(`# c\n` を繰り返す)。そのため、圧縮されたリクエストに対して適用されるボディサイズ制限や WAF の検査では、ペイロードの実際のサイズが認識されません。しかし、フレームワークは、パーサーが処理する前に、このペイロードをメモリ内で展開します。
### 実証コード
```python
import gc
import resource
from graphql import parse
# 100,000 個のコメント、5トークンのクエリ、および max_tokens=5 — 許可される。
query = "# c\n" * 100_000 + "{ a }"
parse(query, max_tokens=5) # エラーは発生しない
```
# メモリ増幅
big = "# パディングコメント。トークンごとにメモリを増幅するために使用されます。\n" * 500_000 + "{ a }"
gc.collect()
before = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
parse(big, max_tokens=5) # 約38MBのデータ
after = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
print(f"RSSの差分: +{ (after - before) / 1024:.0f} MB")
GraphQL-core 3.2.8 / CPython 3.12 / Linux x86_64環境で測定した結果、391KBのデータは127msで解析され、38.1MBのデータはRSSの差分が**+92MB**となり、元のデータサイズの約2.4倍になりました。
### 影響
メモリの枯渇によるサービス拒否。比較的少ない数の同時リクエストでも、ワーカーが一般的な256MBまたは512MBのコンテナ制限を超え、OOM(Out of Memory)キルが発生する可能性があります。認証は不要であり、データは開示も変更もされません。
深さ制限や複雑さ分析は効果がありません。これらは解析が成功した後のAST上で実行され、ドキュメントには実際には5つのトークンしか含まれていません。
### 修正
**3.2.12**および**3.3.0rc1**で修正されました。
`Parser.advance_lexer`は、以前に注目していたトークンと新しいトークンの間の、レクサーがスキップするコメントトークンも含め、消費されるすべてのトークンをカウントします。また、EOFへの進みもカウントするため、末尾のコメントも対象となります。したがって、`max_tokens`は、パーサーが割り当てるすべての`Token`に適用されます。
注意点:動作が変更されました。**コメントは`max_tokens`のカウントに含まれるようになりました。** 以前は受け入れられていた、コメントが多いドキュメントが、現在は拒否される可能性があります。SDLファイルのように、ドキュメントコメントを含むドキュメントを解析する場合、`max_tokens`を設定していると、制限を上げる必要があるかもしれません。解析されたドキュメントの`token_count`属性にも、コメントが含まれるようになりました。
### 対策
アップグレードできない場合は、ドキュメントが`parse()`に到達する前に、**圧縮された**リクエストボディのサイズ制限を適用してください。
このような制限は、アップグレード後も維持する価値があります。`max_tokens`は、いくつかの保護機能の一つであり、リクエスト全体のコストではなく、トークン数を制限します。
### 参照
- GraphQL仕様 § 2.1.4 — コメント
### 貢献者
@BZHunt氏が報告し、リリース前に修正内容をレビューしました。この発見の原動力となったZaX氏を偲んで。
影響を受けるパッケージ:
- pip (Python) graphql-core: 影響を受けるバージョン >= 3.2.3, < 3.2.12、修正は3.2.12で実施
- pip (Python) graphql-core: 影響を受けるバージョン >= 3.3.0a3, < 3.3.0rc1、修正は3.3.0rc1で実施