GraphQL has rapidly become the API technology of choice for modern applications, offering clients the flexibility to request exactly the data they need. However, this flexibility introduces a unique attack surface that differs significantly from traditional REST APIs. Where REST endpoints are fixed and predictable, GraphQL exposes a single endpoint with a powerful query language that attackers can leverage in unexpected ways. In this guide, we will examine the most critical GraphQL security issues and provide a structured testing methodology.
What Makes GraphQL Different From REST
From a security perspective, several characteristics of GraphQL fundamentally change the threat model. REST APIs expose multiple endpoints, each returning a fixed data structure. GraphQL consolidates everything behind a single endpoint, accepting flexible queries that the server resolves dynamically. This means traditional per-endpoint security controls, rate limiting, and WAF rules often fail. The strongly typed schema acts as both a strength and a weakness: it enforces data types but can also leak the entire API structure through introspection. Additionally, the client-driven nature of queries means attackers can craft requests that developers never anticipated during design.
Introspection Attacks
Introspection is a built-in feature of GraphQL that allows clients to query the schema itself. While invaluable during development, leaving it enabled in production effectively hands attackers a complete map of your API.
By sending a query to __schema, an attacker can enumerate every type, field, argument, mutation, and subscription the API exposes. This includes fields that may not be referenced anywhere in the frontend but are still accessible, such as administrative mutations or deprecated endpoints with weaker security controls.
- Schema discovery - Querying
{ __schema { types { name fields { name type { name } } } } }reveals the complete data model including hidden or undocumented types - Mutation enumeration - Attackers can discover mutations like
deleteUser,changeRole, orresetPasswordthat may lack proper authorization checks - Argument analysis - Field arguments reveal expected input types, filter options, and internal naming conventions that inform further attacks
- Visualization tools - Tools like GraphQL Voyager generate interactive visual maps of the schema, making it trivial to identify high-value targets such as user objects, payment fields, or admin mutations
Even when introspection is disabled, attackers can use field suggestion errors to reconstruct the schema through brute force. Many GraphQL implementations return helpful error messages like "Did you mean userEmail?" when an invalid field is queried, enabling incremental schema discovery.
Batching and Aliasing Attacks
GraphQL natively supports sending multiple operations in a single HTTP request through query batching and aliasing. This feature, while convenient for legitimate clients, creates powerful attack vectors.
- Rate limit bypass - If rate limiting is applied at the HTTP request level rather than the operation level, an attacker can send hundreds of queries in a single request, effectively bypassing rate controls entirely
- Credential brute forcing - Using aliases, an attacker can attempt hundreds of login combinations in one request:
a1: login(user:"admin", pass:"pass1") { token } a2: login(user:"admin", pass:"pass2") { token }and so on, testing vast numbers of credentials while appearing as a single request - OTP bypass - Two-factor authentication codes can be brute-forced by batching hundreds of verification attempts, often completing before the OTP expires
- Enumeration at scale - Batching user lookups or password reset mutations to enumerate valid accounts without triggering detection thresholds
Denial of Service Through Query Complexity
The recursive nature of GraphQL types creates opportunities for resource exhaustion attacks that have no direct equivalent in REST APIs.
- Deeply nested queries - When types reference each other (e.g., User has Posts, Post has Author, Author has Posts), an attacker can construct queries nested dozens of levels deep, causing exponential database load
- Query complexity explosion - Even without deep nesting, requesting many fields with list types that each trigger expensive resolvers can exhaust server resources
- Circular fragment references - Maliciously crafted fragments that reference each other can create infinite loops if the server does not implement proper validation. While most mature implementations now prevent this, custom implementations may be vulnerable
- Resource exhaustion through pagination - Requesting extremely large page sizes (e.g.,
first: 999999) on connection types can force the server to load and serialize massive datasets
Authorization Issues
Authorization in GraphQL is notoriously difficult to implement correctly because access control must be enforced at the field level, not just the endpoint level as in REST.
- Field-level authorization gaps - A user query might properly restrict access to other user profiles but still expose sensitive fields like
email,ssn, orsalarywhen accessed through a different path in the graph - IDOR via Relay node IDs - Implementations using the Relay specification expose a global
node(id: "...")query. Since Relay node IDs are often base64-encoded sequential integers, attackers can decode, increment, and access arbitrary objects regardless of the intended access path - Mutation authorization bypass - Read access and write access are often inconsistent. A user who cannot view admin settings through a query might still be able to modify them through a mutation if authorization checks are only implemented on query resolvers
- Subscription authorization - WebSocket-based subscriptions frequently lack the same authorization checks applied to queries and mutations, allowing unauthorized users to receive real-time updates on sensitive data changes
Injection Attacks
GraphQL is not immune to traditional injection vulnerabilities. The query language is merely a transport layer, and the underlying resolvers still interact with databases and other systems.
- SQL injection - GraphQL arguments passed directly into SQL queries without parameterization are vulnerable. This is especially common in filter and search arguments
- NoSQL injection - When resolvers interact with MongoDB or similar databases, JSON-based argument structures can be manipulated to inject operators like
$gt,$regex, or$where
Information Disclosure
GraphQL implementations frequently leak sensitive information through error handling and debug features.
- Verbose error messages - Detailed error messages can reveal database table names, column names, internal service URLs, and technology stack information
- Stack traces - Development configurations accidentally deployed to production expose full stack traces with file paths, library versions, and internal architecture details
- Debug mode - Some GraphQL servers expose a debug mode that returns query execution plans, resolver timing information, and SQL queries being executed
- Field suggestions - Helpful error messages that suggest valid field names enable schema reconstruction even without introspection
Step-by-Step Testing Methodology
- Discover the endpoint - Look for common paths:
/graphql,/gql,/api/graphql,/v1/graphql. Check JavaScript source files for endpoint references - Test introspection - Send a full introspection query and analyze the complete schema. If disabled, attempt field brute forcing using wordlists
- Map the schema - Use GraphQL Voyager to visualize the type graph and identify high-value targets: user types, admin mutations, payment-related fields
- Test authentication - Verify that all queries, mutations, and subscriptions require proper authentication. Test with expired, invalid, and missing tokens
- Test authorization - For every query and mutation, test access with different user roles. Pay special attention to the
nodequery and relationships that cross authorization boundaries - Test batching and aliases - Attempt credential brute forcing and rate limit bypass using batched queries and aliased operations
- Test query complexity - Craft deeply nested queries and measure server response time and resource consumption to identify DoS potential
- Test for injection - Inject SQL, NoSQL, and command injection payloads into all arguments, particularly filter, search, and sort parameters
- Analyze error handling - Send malformed queries and invalid inputs to check for information leakage in error responses
- Review subscriptions - If WebSocket subscriptions are available, test authorization and input validation on all subscription operations
Essential Tools
- GraphQL Voyager - Interactive schema visualization that generates a visual graph of types and their relationships, invaluable for understanding complex schemas
- InQL (Burp Suite extension) - Automates GraphQL introspection, generates queries for all types and mutations, and integrates with Burp Scanner for automated vulnerability detection
- Altair GraphQL Client - Feature-rich GraphQL IDE with support for subscriptions, file uploads, and environment variables, useful for manual testing
- graphql-cop - Security audit tool that automatically checks for common GraphQL misconfigurations including introspection, field suggestions, batching support, and query depth limits
- CrackQL - Purpose-built tool for GraphQL credential brute forcing using aliased queries
Remediation Recommendations
- Disable introspection in production - This is the single most important hardening measure. Ensure introspection is disabled in all non-development environments
- Implement query depth limiting - Set a maximum query depth (typically 7-10 levels) to prevent deeply nested query attacks
- Apply query complexity analysis - Assign complexity scores to fields and set a maximum total complexity per query. List fields and fields with expensive resolvers should have higher scores
- Enforce field-level authorization - Implement authorization checks in every resolver, not just at the query or mutation level. Use a middleware or directive-based approach for consistency
- Rate limit at the operation level - Count individual operations within batched requests and apply rate limits per operation, not per HTTP request
- Validate and sanitize all inputs - Treat GraphQL arguments with the same rigor as any other user input. Use parameterized queries for all database interactions
- Implement proper error handling - Return generic error messages in production. Never expose stack traces, database errors, or internal system details
- Disable field suggestions - Turn off the "Did you mean..." feature in production to prevent schema enumeration without introspection
Want to learn more about this topic? Read my expertise page on Web Application Security →
Comments
No comments yet. Be the first!
Leave a Comment