GraphQL:柔軟性がもたらすリスクとは?
GraphQLはその柔軟なデータ取得能力により、徐々にREST APIに取って代わりつつあります。しかし、この自由度は諸刃の剣でもあります。ライブラリをインストールしただけで「そのまま」にしておくと、無意識のうちにハッカーをデータベース構造の探索に招待していることになりかねません。
筆者も以前、データベースの過負荷でサーバーがフリーズし、一晩中対応に追われたことがあります。犯人は大規模なDDoS攻撃ではなく、単に深くネストされたいくつかのクエリでした。その教訓は、「システムがダウンするまで対策を後回しにしない」ということです。今回は、ジュニアデベロッパーなら必ず押さえておくべき、基本的かつ非常に効果的な3つのセキュリティ手法を紹介します。
1. スキーマの鍵を閉める:Introspectionの無効化
Introspectionは、GraphQL PlaygroundやPostmanなどのツールでコードの自動補完を可能にする機能です。しかし、本番環境(Production)では、泥棒に家の詳細な設計図を渡すようなものです。単純なクエリひとつで、ハッカーはすべてのテーブル、リレーション、さらには機密性の高いフィールドまで把握できてしまいます。
筆者の苦い経験から言えるのは、この機能は開発環境(Dev)でのみ有効にすべきだということです。本番環境にデプロイする際は、即座に無効化しましょう。
Apollo Serverでの安全な設定例
const { ApolloServer } = require('apollo-server');
const server = new ApolloServer({
typeDefs,
resolvers,
// 本番環境以外でのみ有効にする
introspection: process.env.NODE_ENV !== 'production',
playground: process.env.NODE_ENV !== 'production',
});
これにより、ハッカーは自動ツールを使用して脆弱性をスキャンすることができなくなります。彼らは「暗闇の中」で手探りすることを強いられ、標的型攻撃のリスクを大幅に軽減できます。
2. Query Depth(クエリの深さ)を制限する
GraphQLでは、オブジェクトが再帰的な関係を持つことがよくあります。例えば、Author(著者)は複数のPosts(記事)を持ち、各Postはまた一人のAuthorに属します。悪意のあるユーザーは、メモリ溢れ(Stack Overflow)を引き起こすために、100回もネストされたクエリを送信する可能性があります。
query evilQuery {
author(id: "1") {
posts { author { posts { author { # 無限に繰り返す... } } } }
}
}
わずか10階層の深さのクエリであっても、サーバーは何千もの計算やデータベースクエリを同時に実行しなければならなくなる可能性があります。これを解決するために、筆者はよく graphql-depth-limit というライブラリを使用します。
インストールと適用方法
npm経由で素早くインストールできます:
npm install graphql-depth-limit
その後、深度の制限を設定します(通常は5〜7階層もあれば十分です):
const depthLimit = require('graphql-depth-limit');
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [depthLimit(5)], // 5階層を超える深さのクエリをすべてブロックする
});
この仕組みにより、悪意のあるリクエストがデータベースに到達する前に、入り口で拒否することができます。
3. Resolver層でのスマートなRate Limiting
NginxなどのリバースプロキシでIP制限をかけるだけで十分だと思われがちです。しかし、GraphQLではすべてのリクエストが単一のエンドポイントに集中します。正常なHTTP 200リクエストであっても、1万件のレコードと関連データを取得するような「超重量級」のクエリが含まれている可能性があります。
そのため、Resolverレベルでの制限が必要になります。graphql-rate-limit ライブラリは、特定の時間内にフィールドが呼び出される回数を制御するための優れた選択肢です。
Mutationを保護する実践例
例えば、パスワードリセットのメール送信機能があるとします。一人のユーザーが1分間に100回もこの機能を呼び出すことは避けたいはずです。
const { createRateLimitDirective } = require('graphql-rate-limit');
const rateLimitDirective = createRateLimitDirective({
identifyContext: (ctx) => ctx.user.id,
});
const typeDefs = gql`
type Mutation {
# ユーザーごとに1分間に1回までに制限
sendResetEmail(email: String!): Boolean @rateLimit(window: "1m", max: 1)
}
`;
このアプローチにより、ブルートフォース攻撃やクライアントからの直接的なスパムからシステムリソースを保護できます。
エンジニアへのアドバイス
セキュリティはゴールではなく、継続的なプロセスです。「Zero Trust」の考え方を持ち、クライアントから送られてくるデータは一切信用しないようにしましょう。システムが大規模になった場合は、各リクエストの「重さ」を計算する Query Cost Analysis についても学習することをお勧めします。
データベースがダウンしてから修正を始めるのではなく、今日のうちにDepth Limitの設定とIntrospectionの無効化を行いましょう。安全で高速なアプリケーション開発を応援しています!

