1. GraphQL とは #what-is
クライアントが「欲しいフィールドだけ」を1つのエンドポイントで指定して取得できる API スキーム。REST のように「リソースごとにエンドポイント」ではなく、単一エンドポイント(/graphql)に対してクエリ言語でリクエストする。
POST /graphql
{
"query": "{ user(id: 123) { name email orders { product price } } }"
}
2. Introspection の漏洩 #introspection
スキーマ全体が公開されると、隠しフィールドや管理者専用 mutation が露出する。
GraphQL には「自分自身のスキーマを問い合わせる」Introspection 機能がある。本番環境で有効のままだと、API のすべてのフィールド・mutation・引数が攻撃者に公開されてしまう。
// Introspection クエリで全スキーマを取得
{
__schema {
types {
name
fields { name type { name } }
}
queryType { fields { name } }
mutationType { fields { name args { name } } }
}
}
// 隠していたつもりの管理機能が露出:
// - deleteAllUsers(confirm: String): Boolean
// - exportCustomerData(format: String): String
// - resetUserPassword(userId: ID!): String
3. 認可がフィールド単位で必要 #authz
REST だとエンドポイント単位で認可をかければよかった。GraphQL では1つのクエリで複数の型・フィールドが取得されるため、各フィールドで認可チェックが必要。
// 一見無害なクエリ
{
product(id: 1) {
name
price
supplier { ← 一般会員に見せていいか?
contractPrice ← 卸売原価(社外秘)
contactEmail
}
}
}
resolver でフィールドごとに認可チェックを実装する必要がある。
4. DoS — ネスト深さとバッチクエリ #dos
- 深いネスト —
{ user { friends { friends { friends { ... } } } } }で巨大な join - 大量バッチ — 1リクエストに 1000 個のクエリを詰め込んで送信
- 高コストフィールド —
allUsersのような全件取得フィールド
5. 正しい防御 #defense
- 本番環境では Introspection を無効化 — Apollo Server なら
introspection: false - resolver でフィールド単位の認可 — フィールドリゾルバの先頭で
context.userの権限チェック - クエリ複雑度・深さ制限 —
graphql-depth-limit、graphql-cost-analysis - レート制限 — リクエスト数に加えて、複雑度ベースの制限を併用
- 許可リスト方式の API — 既知のクエリだけを受け付ける(persisted queries)。柔軟性は失われるが攻撃面は最小化