TL;DR: GraphQL's flexibility is its greatest strength and its most dangerous security liability. A single endpoint, self-documenting schema, nested query resolution, and batched operations create an attack surface that is fundamentally different from REST APIs - and most security testing tools and methodologies are not designed to test it. The vulnerabilities pentesters find most frequently in production GraphQL implementations are enabled introspection leaking the full schema, missing resolver-level authorization checks leading to IDOR, unbounded query depth enabling denial of service, and batching that bypasses rate limiting on authentication endpoints.
GraphQL vs REST: Attack Surface Comparison
| Attack Category | REST API | GraphQL API | GraphQL-Specific Risk |
|---|---|---|---|
| Schema Discovery | Requires enumeration or documentation leaks | Introspection query returns full schema | Complete API map available by default |
| Authorization | Per-endpoint middleware | Per-resolver enforcement required | IDOR through nested object traversal |
| Denial of Service | Rate limiting per endpoint | Deeply nested or aliased queries | Single request causes exponential backend load |
| Brute Force | Rate limited per request | Batched operations in single request | 1000 login attempts in one HTTP request |
| Injection | Through parameters and headers | Through variables and directive arguments | Complex input types obscure injection points |
| Information Leakage | Error messages and headers | Verbose error messages with stack traces | Schema suggestions in error responses |
Introspection: The API Hands You Its Own Blueprint
GraphQL introspection is a built-in feature that allows clients to query the schema itself - every type, every field, every argument, every relationship. In development, this powers tools like GraphiQL and GraphQL Playground. In production, it hands an attacker the complete API documentation including internal types, administrative mutations, deprecated fields that may still be functional, and relationships between objects that reveal the application's data model.
The introspection query is straightforward. A single request to the GraphQL endpoint with __schema returns the complete type system. Pentesters routinely find introspection enabled in production on applications that have invested significant effort in securing their REST APIs but treated the GraphQL endpoint as an afterthought.
What makes introspection particularly dangerous is that GraphQL schemas often include types and mutations that the frontend application never uses. Internal administrative endpoints, debugging queries, and experimental features may be defined in the schema and fully functional - they simply are not called by the production client. Introspection reveals them, and an attacker can call them directly.
Remediation: Disable introspection in production. Every major GraphQL server framework supports this - Apollo Server, graphql-yoga, Hasura, and others all provide configuration options to disable introspection queries in production environments. For APIs that require introspection for legitimate tooling, restrict it to authenticated requests from authorized roles.
Query Depth and Complexity: Denial of Service by Design
GraphQL's nested query structure allows clients to request deeply nested related objects in a single query. In an application with circular relationships - a user has posts, each post has comments, each comment has an author (user), each user has posts - an attacker can construct a query that recursively nests these relationships to an arbitrary depth, creating exponential load on the server.
A query nested 10 levels deep through a circular relationship can generate millions of database queries on the backend. The server dutifully resolves each level, exhausting database connections, memory, and CPU - all from a single HTTP request that a standard WAF would pass without inspection because it is a syntactically valid GraphQL query.
Alias-Based Batching for Resource Exhaustion
Even without deep nesting, GraphQL aliases allow an attacker to request the same expensive field hundreds of times in a single query. Each alias triggers a separate resolver execution. If the resolver performs a database query or external API call, 500 aliases means 500 backend operations from one HTTP request. Rate limiting at the HTTP level sees one request. The backend sees 500.
Remediation: Implement query depth limiting, query complexity analysis, and operation cost budgets. Libraries like graphql-depth-limit and graphql-query-complexity provide middleware that rejects queries exceeding configured thresholds before execution begins. Set maximum alias counts per query. These controls should be enforced at the GraphQL server layer, not delegated to a WAF that cannot parse GraphQL query structure.
Authorization Failures at the Resolver Level
This is the most impactful class of GraphQL vulnerability we find in penetration testing. In REST APIs, authorization is typically implemented as middleware on each route - the check happens before the controller logic executes. In GraphQL, every request hits the same route (/graphql), and authorization must be implemented within each resolver function.
The failure pattern is consistent: developers implement authentication (verifying the user is logged in) at the GraphQL middleware level, but skip authorization (verifying the user is permitted to access this specific resource) at the resolver level. The result is IDOR vulnerabilities accessible through nested queries.
Consider a query that fetches a user's orders, and each order includes the customer's payment methods. The orders resolver may correctly verify that the requesting user owns the orders. But the paymentMethods resolver on the order type simply returns all payment methods associated with the order's customer ID - without checking whether the requesting user is authorized to see them. By querying another user's orders through a different path (or guessing order IDs), an attacker can traverse the graph to reach payment methods belonging to other users.
Remediation: Implement authorization at every resolver, not at the gateway. Use authorization libraries designed for GraphQL (like graphql-shield) that allow declarative permission rules on types and fields. Treat every resolver as a potential entry point for unauthorized access, regardless of how the frontend application structures its queries.
Batching Attacks: Bypassing Rate Limiting
GraphQL's support for batched operations - sending multiple queries or mutations in a single HTTP request - creates a fundamental challenge for rate limiting. Standard rate limiting operates at the HTTP request level: X requests per minute per IP or per user. GraphQL batching allows an attacker to pack hundreds of operations into a single request, effectively bypassing request-level rate limits.
The most exploited scenario is authentication brute-forcing. An attacker sends a single HTTP POST containing 500 login mutations, each with a different password candidate. The rate limiter sees one request. The authentication system processes 500 login attempts. Combined with credential lists from previous breaches, this can compromise accounts at scale without triggering any rate-limiting alerting.
Remediation: Implement rate limiting at the GraphQL operation level, not the HTTP request level. Count each operation in a batched request independently against the rate limit. Consider disabling batching entirely for sensitive mutations (authentication, password reset, payment processing) or setting a maximum batch size.
Injection Through Variables and Directives
GraphQL injection follows the same principles as SQL injection or command injection, but the attack surface is less obvious to developers. GraphQL variables are the primary input mechanism, and when variable values are passed unsanitized to backend data stores - SQL databases, NoSQL databases, ORMs, search engines - injection is possible.
The complexity of GraphQL input types can obscure injection points. A mutation that accepts a nested input object with multiple fields creates numerous injection surfaces that may not be obvious from the query structure alone. Pentesters test each variable field independently for SQL injection, NoSQL injection, and LDAP injection based on the likely backend data store.
Custom directives add another injection surface. If the application implements custom directives that accept arguments and use them in backend operations - especially string interpolation into queries or system commands - those directive arguments are injection points that many automated scanners will miss entirely.
Mutation Abuse and State-Changing Operations
GraphQL mutations that modify server-side state deserve particular scrutiny. Common findings include mutations that lack proper authorization (any authenticated user can call administrative mutations), mutations that accept more fields than the frontend sends (mass assignment through additional input fields discovered via introspection), and mutations that perform sensitive operations without adequate confirmation or rate limiting.
Pentesters systematically test every mutation discovered through introspection - not just the ones the frontend application uses. Administrative mutations for user role changes, data deletion, configuration updates, and feature flag toggling are frequently accessible to regular users because they were "hidden" by not being called from the frontend, rather than being protected by authorization checks.
Secure Your GraphQL API Before Attackers Map It
Lorikeet Security's API penetration testing includes comprehensive GraphQL assessment - introspection analysis, resolver-level authorization testing, query complexity DoS, batching abuse, injection testing, and mutation authorization review. Our manual testing catches the vulnerabilities automated scanners miss.