3つのGraphQLセキュリティ対策:ハッカーに隙を見せないための秘策

Security tutorial - IT technology blog
Security tutorial - IT technology blog

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の無効化を行いましょう。安全で高速なアプリケーション開発を応援しています!

Share: