GraphQLの「再帰」という甘い罠:クエリ深度制限でサーバーを守る極意
現場でエンジニア諸君と話していると、「GraphQLはRESTより柔軟で効率的だ」という言葉をよく耳にする。確かにそうだが、その「柔軟性」は、設計を怠れば攻撃者にとっての最高の武器に化ける。
特に今日取り上げる「再帰的クエリによるDoS攻撃(Query Depth Limiting)」は、APIの設計思想を逆手に取った非常に巧妙な手口だ。WAFでSQLインジェクションを弾いて安心しているチームほど、この盲点を突かれてサービスがダウンする。今日は、なぜこれが危険なのか、そしてどう実装レベルで防ぐのかを、現場のリアリティを交えて解説しよう。
—
なぜ「再帰」がサーバーを殺すのか?
REST APIなら /api/users/1 を叩けば特定のデータが返ってくる。しかし、GraphQLはクライアントが「欲しい構造」を自由に指定できる。ここで悪意ある攻撃者が以下のような「再帰的な深淵」を突きつけるとどうなるか。
query maliciousQuery {
user(id: “1”) {
friends {
friends {
friends {
friends {
# これを100階層繰り返す
}
}
}
}
}
}
このクエリを投げられたサーバーは、DBに対して指数関数的な結合(JOIN)を要求し、メモリとCPUを瞬く間に食い尽くす。これが再帰的クエリによるDoS攻撃の正体だ。多くの場合、バックエンドのORMがエラーを吐く前に、サーバーのイベントループがブロックされ、サービス全体が unresponsive(無応答)に陥る。
—
実装での防御:クエリ深度制限(Query Depth Limiting)
「とりあえずWAFで制限すればいい」という意見もあるが、WAFはHTTPリクエストボディの中身まで深く解釈できないことが多い。防御は、アプリケーション層(GraphQLサーバー側)で完結させるのが最も確実だ。
Node.js (Apollo Server) を例に、最も実用的な防御ロジックを提示する。
実装サンプル:graphql-depth-limit の導入
Apollo Serverを使用しているなら、車輪の再発明はせず、コミュニティで信頼されているミドルウェアを活用するのが鉄則だ。
// npm install graphql-depth-limit
import { ApolloServer } from ‘apollo-server’;
import depthLimit from ‘graphql-depth-limit’;
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
// クエリの深さを最大 5 階層までに制限する
// これを超えたクエリが来ると、サーバーは処理を開始する前にエラーを返す
depthLimit(5, {}, (depths) => {
// ログに記録して攻撃の予兆を検知する(SIEMへの転送推奨)
console.warn([Security Alert] Query depth exceeded: ${JSON.stringify(depths)});
})
],
});
server.listen().then(({ url }) => {
console.log(🚀 Server ready at ${url});
});
このコードのポイントは、「処理の前」に検証していることだ。DBにクエリを発行する前に「このリクエストは深すぎる」と弾く。これがセキュリティの基本原則だ。
—
さらなる高みへ:コストベースの制限
深度制限だけでは防げない攻撃もある。例えば、「深度は浅いが、計算コストが異常に高いフィールド」を大量に含んだクエリだ。
これを防ぐには、各フィールドに complexity(コスト)を割り当てる手法が有効だ。
- 単純なフィールド: 1ポイント
- DBアクセスの伴うフィールド: 5ポイント
- 再帰的な結合フィールド: 10ポイント
合計コストが一定値(例:50)を超えたら拒否する。このロジックを実装しておけば、攻撃者は「量」でも「質」でもサーバーを攻撃できなくなる。graphql-validation-complexity などのライブラリを調査し、プロジェクトのスキーマに合わせて定義してほしい。
—
セキュリティチーフからの「現場の心得」
最後に、技術的な実装以上に大切なマインドセットを伝えておく。
1. Introspectionを無効化せよ: 本番環境でGraphQLのスキーマ情報(Introspection)を公開しているサーバーは、攻撃者に「地図」を渡しているのと同義だ。環境変数で本番環境のみ無効化する設定は必須だ。
2. レートリミットとの併用: クエリの複雑性を制限しても、低速なクエリを大量に投げられる「Low and Slow」攻撃はあり得る。IPベース、またはユーザーIDベースのレートリミットを必ず前段(NginxやAPI Gateway)で設けること。
3. ログを疑え: 「エラーが起きた」というログだけでは不十分だ。どんなクエリが、どのIPから、どの程度の頻度で飛んできたのかを分析できるログ基盤を作っておくこと。
GraphQLは強力だが、その分「守るべき境界線」を設計者が明確に定義しなければならない。便利さと引き換えにセキュリティを投げ出すのではなく、「設計段階で攻撃の芽を摘む」。それが、我々エンジニアがプロとして持ち合わせるべき矜持だ。
次回のデプロイ前に、一度自社のクエリ深度を計測してみてほしい。もし「制限なし」なら、今日中に修正コードをコミットすることを強く勧める。安全な開発は、こうした地味な積み重ねから始まるのだから。
コメント