| name | apply-graphql-security |
| description | Use when building or securing a GraphQL API — limiting query depth and complexity, disabling introspection in production, enforcing field-level authorization, and preventing batching abuse. |
| source | OWASP GraphQL Security Cheat Sheet (owasp.org/www-project-cheat-sheets); OWASP API Security Top 10 2023; HackerOne GraphQL security research; Apollo Server security documentation |
| tags | ["security","owasp","graphql","api","authorization","rate-limiting","developer"] |
Apply GraphQL Security
Secure GraphQL APIs by enforcing query depth limits, disabling introspection in production, implementing field-level authorization, and rate limiting by query complexity — preventing DoS via deeply nested queries and unauthorized data access via field enumeration.
Why This Is Best Practice
Adopted by: OWASP GraphQL Security Cheat Sheet (2023) is the primary reference. HackerOne's GraphQL security research documents the most common GraphQL vulnerability classes found across bug bounty programs. Apollo Server (used by Airbnb, The New York Times, Expedia) provides built-in query depth limiting and complexity analysis. GitHub's GraphQL API enforces complexity limits (5,000 points per query) and requires authentication for all requests including introspection.
Impact: HackerOne's 2022 "Hacking GraphQL for Fun and Profit" found introspection enabled in production in 40% of GraphQL APIs tested — allowing attackers to map entire schemas. Deeply nested GraphQL queries (10+ levels) can cause O(n^k) database queries from a single HTTP request, enabling DoS with a single request. Shopify paid out multiple GraphQL-related bug bounties for missing field-level authorization allowing access to other merchants' data — direct object reference without per-field auth checks.
Why best: REST APIs use URL-based authorization (middleware checks each endpoint); GraphQL resolvers require per-field authorization because a single endpoint serves all queries. A REST security model applied to GraphQL (middleware on /graphql) allows a user with read access to viewer { name } to also query viewer { paymentMethods { cardNumber } } if field-level checks are missing.
Sources: OWASP GraphQL Security Cheat Sheet; HackerOne "Hacking GraphQL for Fun and Profit" (2022); Apollo Server documentation; GitHub GraphQL API documentation
Steps
-
Limit query depth and complexity — prevent DoS via nested queries:
from graphql import parse, validate
from graphql.validation.rules import NoSchemaIntrospectionCustomRule
class QueryDepthLimiter:
MAX_DEPTH = 7
def resolve(self, next, root, info, **args):
if root is None:
depth = self._get_query_depth(info.operation)
if depth > self.MAX_DEPTH:
raise Exception(f"Query depth {depth} exceeds maximum {self.MAX_DEPTH}")
return next(root, info, **args)
def _get_query_depth(self, node, depth=0):
max_depth = depth
if hasattr(node, 'selection_set') and node.selection_set:
for selection in node.selection_set.selections:
child_depth = self._get_query_depth(selection, depth + 1)
max_depth = max(max_depth, child_depth)
return max_depth
depthLimit ;
{ createComplexityLimitRule } ;
server = ({
schema,
: [
(),
(, {
: .(, cost),
}),
],
});
Rules
- Disable introspection in production — if schema discovery is needed (for internal tools), require authentication.
- Every resolver that returns sensitive data must check authorization — not just the top-level query.
- Query aliases allow executing the same field multiple times; depth and complexity limits must account for aliases.
- Persisted queries (allowlist of approved query hashes) eliminate arbitrary query execution — consider for production APIs.
Common Mistakes
__typename bypassing introspection blocks — __typename is a meta-field that leaks type names even when introspection is disabled; consider whether it needs to be blocked too.
- Using DataLoader only for performance — DataLoader (batched loading) also mitigates N+1 query amplification attacks; skipping it allows a single query to cause thousands of DB round trips.
- Missing authorization on mutation arguments — checking
canUpdateUser(viewer, userId) but not canSetRole(viewer, newRole) allows privilege escalation through mutation arguments.
- Error messages exposing schema details — default GraphQL error messages often include resolver and field names; customize error formatting to remove internal details.