A Practical Guide to GraphQL Security
Secure GraphQL APIs against introspection abuse, query complexity attacks, broken authorization, and the unique pitfalls of resolver design.
GraphQL turned the API conversation upside down. Instead of dozens of REST endpoints each returning fixed payloads, a GraphQL service exposes a single endpoint backed by a typed schema. Clients ask for exactly what they need, often joining data across multiple sources in a single query. The result is faster development, more flexible clients, and richer integrations.
GraphQL also brings security challenges that defenders accustomed to REST need to learn. Introspection reveals the entire schema by default. Queries can be arbitrarily deep or wide, triggering expensive resolvers. Authorization patterns from REST do not always translate. Caching and rate limiting require different thinking. This guide is a practical walkthrough for securing GraphQL APIs in production.
Core Concepts
GraphQL is a query language for APIs and a runtime for executing those queries against typed data. A GraphQL service exposes a schema that describes types, fields, queries, mutations, and subscriptions. Clients send queries that select fields from the schema; the server's resolvers fetch the data and return it in the requested shape.
The unit of execution is the resolver. Each field in the schema has a resolver function that knows how to fetch that field's value. Resolvers can call databases, internal services, third-party APIs, or other resolvers. The flexibility is enormous; so is the responsibility for security.
The OWASP Top 10 for GraphQL highlights GraphQL-specific risks alongside familiar categories: introspection abuse, denial of service through complex queries, broken authorization on object and field levels, injection through query variables, batching abuse, and improper error handling.
Schema and Introspection
GraphQL introspection lets clients query the schema itself, listing all types, fields, arguments, and descriptions. This is invaluable during development; in production, it can hand attackers a roadmap of your API.
Disable introspection in production unless you have a clear reason to leave it on. Apollo Server, GraphQL Java, and most other servers support disabling introspection with a flag. Provide the schema to legitimate API consumers through documentation or developer portals rather than runtime introspection.
Even with introspection disabled, schema discovery is possible through error messages, type information leaked in responses, and pattern probing. Combine introspection control with strong error handling and rate limiting.
Avoid sensitive information in schema descriptions. Comments and descriptions are exposed through introspection. Treat them as public-facing documentation.
Use persisted queries for first-party clients. With persisted queries, clients send a hash of a pre-registered query instead of the full query text. The server only executes approved queries, eliminating broad classes of query-based attacks. Apollo Persisted Queries and Relay's persisted query support are widely used.
Query Complexity and Resource Protection
A naive GraphQL endpoint can be brought down by a single carefully crafted query. Deeply nested queries that join across many relationships, queries that request large lists, and queries that fan out to expensive resolvers can each consume enormous resources.
Apply query depth limits. Most GraphQL servers support a depth limit configuration. A query depth of 10 to 15 is usually generous; legitimate clients rarely need more.
Apply query complexity analysis. Tools like graphql-cost-analysis assign cost to each field and reject queries exceeding a threshold. Lists, expensive computations, and joins should carry higher cost than simple field reads.
Apply query timeouts. Set a maximum execution time per query at the server and at any upstream proxy or gateway. Cancel long-running queries cleanly.
Defend against batching abuse. Some GraphQL servers accept arrays of operations in a single request. Without limits, this can multiply attack volume per request. Limit batch size, apply per-operation cost, and rate limit at the request level.
Defend against alias abuse. A single query can use field aliases to request the same expensive field many times. Count aliases in complexity analysis; reject queries with excessive aliasing.
Apply rate limiting at the right granularity. Per-user, per-IP, and per-operation type limits are all useful. Adjust for the cost profile of each operation rather than treating all queries equally.
Authorization
Authorization in GraphQL deserves special care because a single query can traverse many objects. Authorization must be enforced at each resolver, not just at the entry point.
Implement field-level authorization. Each resolver should check whether the caller is permitted to read or modify the data it returns. Libraries like graphql-shield, custom directives, and policy engines simplify consistent enforcement.
Authorize at the object level too. Even when a field is generally accessible, the specific object instance may not be. A resolver for order.lineItems should verify the caller has access to the parent order before returning line items.
Beware of leaking through error messages. A "not authorized" error on a specific object can confirm the object's existence to an attacker who would not otherwise know. Consider returning a generic "not found" instead, or carefully design error responses for sensitive operations.
Apply the same rigor to mutations. Mutations change state and often have higher impact than queries. Authorize each mutation explicitly, validate all inputs against the schema, and check business logic constraints.
Use directives for centralized policy. Schema directives like @auth, @hasRole, or @hasPermission externalize policy from resolver code. They make policy reviewable in the schema and consistent across resolvers.
Externalize policy where the model fits. Open Policy Agent and similar engines can evaluate complex authorization rules in a centralized service. Reach for this when policies become too complex to encode in directives.
Input Validation
Validate query variables. GraphQL types provide basic shape validation, but business validation (length limits, formats, ranges) still requires explicit checks in resolvers. A field typed as String accepts arbitrary text until you validate it.
Defend against injection. Pass variables to underlying data stores as parameters, never as string interpolation. SQL injection, NoSQL injection, and command injection all work through GraphQL just as they do through REST.
Validate IDs and references. When variables include identifiers, validate them against expected formats and authorize before use. Treat IDs as untrusted strings even if they pass schema validation.
Apply business logic checks. Schema validation does not enforce that a discount code is valid, that a quantity is in stock, or that a transfer is within limits. Resolvers and downstream services must enforce these rules.
Handle file uploads carefully. GraphQL servers that accept file uploads (via multipart specs) need the same controls as any upload endpoint: size limits, content type validation, malware scanning, sanitized storage paths.
Caching and Idempotency
Caching GraphQL is trickier than REST because each query may be unique. Plan caching with security in mind.
Cache at the data layer, not the response layer. Tools like DataLoader batch and deduplicate calls to underlying data sources during a single request, dramatically improving performance without exposing cross-user data.
For response caching, scope per user. Apollo's response cache, Hasura's caching, and edge-cached GraphQL services support per-user scoping. Never cache responses across users for queries that include authorized data.
Be careful with persisted query CDN caching. Persisted queries with user-specific responses must be cached with proper scoping or not at all. Misconfigured caching has produced cross-user data leakage in the past.
Design mutations for idempotency where possible. Idempotency keys help clients safely retry mutations without producing duplicate state changes. This becomes important in mobile and partner integration scenarios.
Observability and Operations
Log queries and operations. Capture the operation name, query hash (for persisted queries), variables (with sensitive values redacted), and authentication context. Avoid logging full query text for arbitrary requests; it can include sensitive data.
Monitor for abuse. Track query depth distribution, complexity scores, error rates, and unusual patterns. Sudden spikes in deep queries or authorization failures suggest probing.
Apply security tests. Tools like GraphQL Cop, InQL, Clairvoyance, and Burp Suite plugins probe GraphQL endpoints for misconfiguration: enabled introspection, missing complexity limits, broken authorization. Integrate scanning into CI for new and changed schemas.
Adopt a federated approach intentionally. Apollo Federation and similar architectures compose multiple GraphQL services into a single graph. Federation amplifies the importance of consistent authorization across subgraphs; design with shared identity and policy in mind.
Plan for incident response. Define playbooks for query-based DoS, broken authorization disclosure, leaked persisted query infrastructure, and compromised internal subgraphs.
Real-world Examples
Multiple disclosed GraphQL incidents in recent years have involved introspection left enabled in production, allowing attackers to fingerprint the schema and craft targeted exploits.
Broken authorization in GraphQL APIs has produced significant data exposures at fintech, e-commerce, and social platforms. The common pattern is per-field resolvers that trust an upstream check rather than authorizing on each access.
Denial of service through nested or aliased queries was demonstrated repeatedly against popular open source platforms in 2020 and 2021, prompting wide adoption of depth and complexity limits.
A 2022 incident at a major SaaS vendor involved GraphQL endpoints inadvertently exposing fields that should have been admin-only, due to an automated schema generation tool that did not respect access annotations. Code review and post-deployment scanning are the operational counters.
GraphQL is a powerful technology that rewards thoughtful security design. The flexibility that delights developers can also delight attackers if defaults are left in place. Disable introspection in production. Use persisted queries for first-party clients. Apply depth, complexity, and rate limits. Authorize at object and field level. Validate variables and inputs. Cache carefully and per-user when applicable. Log, monitor, test, and rehearse incidents.
For intermediate practitioners, the most effective approach is to bake security into the GraphQL platform itself. Provide opinionated server templates, shared authorization directives, default complexity analysis, and standard logging. Make the secure choice the default for every team that ships a GraphQL service.
GraphQL is here to stay across web, mobile, and AI integrations. Build the discipline now, and your graph will scale without surprising you.
Ready to test your knowledge? Take the GraphQL Security MCQ Quiz on HackCert today!
Related articles
API Hardening: A Comprehensive Guide to Ensuring API Security and Avoiding Cyber Risks
8 min
API Security: Is Data Leaking Through Your Modern Web App APIs?
8 min
Code Review: Methods for Identifying Hidden Bugs and Security Vulnerabilities in Source Code
12 min
Mass Assignment: Exploiting Web API Vulnerabilities for Privilege Escalation
10 min

