GraphQL adoption has exploded. What started as Facebook's internal query language in 2012 is now the API layer for companies like GitHub, Shopify, Yelp, and thousands of startups that chose it for its flexibility and developer experience. But that flexibility comes with a security cost that most teams underestimate.

Unlike REST APIs, where each endpoint has a defined input and output, GraphQL exposes a single endpoint that accepts arbitrary queries against your entire data graph. Clients decide what data they want, how deeply they traverse relationships, and how many operations they batch into a single request. This design, which makes GraphQL so powerful for frontend developers, creates an attack surface that traditional API security testing misses entirely.[1]

In our penetration testing engagements, GraphQL APIs consistently produce more findings per endpoint than their REST equivalents. Not because GraphQL is inherently less secure, but because the security model is fundamentally different and most teams apply REST security patterns that do not translate.

Introspection: Your Schema is Showing

GraphQL introspection is a built-in feature that allows clients to query the schema itself. By sending an introspection query, anyone can retrieve the complete type system: every type, every field, every argument, every mutation, and every subscription your API supports. This is the equivalent of handing an attacker a complete map of your API surface before they write a single exploit.

The standard introspection query looks like this:

{
  __schema {
    types {
      name
      fields {
        name
        args { name type { name } }
        type { name kind ofType { name } }
      }
    }
    mutationType { name fields { name } }
    queryType { name fields { name } }
  }
}

When this returns a full schema in production, the attacker immediately knows every query and mutation available, including internal or admin-only operations that are not referenced anywhere in the client-side code. We routinely discover hidden mutations like createAdminUser, deleteAllData, impersonateUser, and exportDatabase through introspection that were intended for internal tooling but are accessible to any authenticated (or sometimes unauthenticated) user.[2]

When Disabling Introspection Is Not Enough

Many teams disable introspection and consider the problem solved. It is not. GraphQL field suggestion is a separate feature that leaks schema information even when introspection is disabled. When a client sends a query with a misspelled field name, many GraphQL implementations helpfully suggest the correct field name in the error response:

// Request
{ user { naem } }

// Response
{
  "errors": [{
    "message": "Cannot query field 'naem' on type 'User'. Did you mean 'name'?"
  }]
}

An attacker can systematically brute-force field names by sending queries with common field names and collecting the suggestions. The tool graphql-cop automates this process, and the Burp Suite extension InQL includes a field suggestion scanner that reconstructs partial schemas from error messages alone.[3]

The tool Clairvoyance, developed by Nikita Stupin, takes this further by using wordlists and suggestion responses to reconstruct the full schema without introspection. In our testing, Clairvoyance typically recovers 70-90% of the schema on implementations that have introspection disabled but field suggestions enabled.[4]

Remediation: Disable introspection in production (most GraphQL frameworks support this via configuration). Also disable field suggestions, or configure your server to return generic error messages that do not reveal field names. In Apollo Server, set debug: false in production. In graphql-java, customize the GraphQLError handler.

Batched Query Attacks

GraphQL supports query batching, which means a client can send an array of operations in a single HTTP request. This feature was designed for performance optimization, allowing frontends to reduce round trips by combining multiple queries. Attackers use it for credential stuffing, brute-force attacks, and rate limit bypass.

Authentication Brute-Force via Batching

Consider a login mutation that accepts a username and password. With REST, brute-forcing a password requires one HTTP request per attempt, making rate limiting straightforward. With GraphQL batching, an attacker can send thousands of login attempts in a single HTTP request:

[
  { "query": "mutation { login(user:\"admin\", pass:\"password1\") { token } }" },
  { "query": "mutation { login(user:\"admin\", pass:\"password2\") { token } }" },
  { "query": "mutation { login(user:\"admin\", pass:\"password3\") { token } }" },
  // ... 997 more attempts
]

If your rate limiting operates at the HTTP request level (as most WAFs and API gateways do), this entire batch counts as one request. The attacker just sent 1,000 login attempts while consuming one unit of their rate limit quota. In a real engagement, we used this technique to bypass Cloudflare rate limiting on a GraphQL endpoint and successfully brute-forced a 4-digit OTP code in under 60 seconds.[5]

Query Aliasing for the Same Effect

Even if batching is disabled, GraphQL aliases achieve the same result within a single query. Aliases allow a client to execute the same field multiple times with different arguments in one operation:

query {
  attempt1: login(user: "admin", pass: "password1") { token }
  attempt2: login(user: "admin", pass: "password2") { token }
  attempt3: login(user: "admin", pass: "password3") { token }
  # ... hundreds more aliases
}

This is a single query (not a batch), so it bypasses batch-level restrictions. Rate limiting for GraphQL must operate at the operation level, counting each aliased field execution and each batched operation independently. Most off-the-shelf API gateways do not support this granularity without custom configuration.

Deep Nesting and Resource Exhaustion

GraphQL's type system allows types to reference each other, creating circular relationships. A User has posts, each Post has an author (a User), who has posts, who have an author, and so on. An attacker exploits this by crafting deeply nested queries that consume exponential server resources.

The Nested Query DoS

Here is a query that creates exponential database load:

{
  users(first: 100) {
    posts(first: 100) {
      author {
        posts(first: 100) {
          author {
            posts(first: 100) {
              author {
                posts(first: 100) {
                  title
                }
              }
            }
          }
        }
      }
    }
  }
}

At a nesting depth of 5 with 100 items per level, this query requests up to 100^5 (10 billion) database rows. Even if the database does not contain that many records, the server will exhaust memory and CPU attempting to resolve the query. A single request can take down a production server.[6]

This is not theoretical. In 2019, a researcher demonstrated a denial-of-service attack against a major SaaS platform's GraphQL API by sending a nested query that consumed all available worker threads, rendering the API unresponsive for legitimate users. The platform had no query depth or complexity limits in place.

Fragment-Based Amplification

GraphQL fragments provide another amplification vector. By defining fragments that reference other fragments, an attacker can create query payloads that expand to enormous sizes during resolution:

query {
  users {
    ...F1
  }
}
fragment F1 on User { posts { ...F2 } }
fragment F2 on Post { author { ...F3 } }
fragment F3 on User { posts { ...F4 } }
fragment F4 on Post { author { posts { title content body } } }

Each fragment level multiplies the data requested, and fragment spreading makes the query appear compact while resolving to a massive operation. Some older GraphQL implementations do not even count fragment depth toward query depth limits.

Remediation: Implement query depth limiting (typically 7-10 levels maximum), query complexity analysis (assign cost values to each field and reject queries exceeding a threshold), and query timeout enforcement. Libraries like graphql-depth-limit for Node.js and QueryComplexity for graphql-java make this straightforward. Also limit the maximum number of items returned per connection (cap first/last arguments).

Authorization Bypass on Resolvers

This is the most critical class of GraphQL vulnerability we encounter, and it stems from a fundamental architectural mistake: implementing authorization at the wrong layer.

The Resolver Authorization Problem

In REST APIs, authorization is typically implemented as middleware that runs before the route handler. The pattern is well-understood: check the user's role, verify they have permission for this endpoint, and reject unauthorized requests before any business logic executes.

GraphQL does not have "endpoints" in the REST sense. It has resolvers, which are functions that fetch data for individual fields. A single GraphQL query might invoke dozens of resolvers, and each resolver needs its own authorization check. Many teams implement authorization on the top-level query resolver but forget to add checks to nested field resolvers.[7]

Consider this schema:

type Query {
  me: User          # Returns the authenticated user
}

type User {
  id: ID
  name: String
  email: String
  orders: [Order]
  paymentMethods: [PaymentMethod]
  adminNotes: String  # Internal notes, admin-only
}

The me query resolver checks that the user is authenticated. But if the adminNotes field resolver does not independently check that the requesting user has admin privileges, any authenticated user can query { me { adminNotes } } and read internal admin notes about their own account.

We see this pattern repeatedly with fields like internalId, costPrice (vs. retailPrice), supportTickets, auditLog, and apiKeys. The field exists on the type, the resolver fetches the data from the database, and no one added an authorization check because the frontend never queries that field.

Cross-Object Authorization Failures

A more dangerous variant involves traversing relationships to access objects the user should not see. If a user can query their own orders, and each order has a processedBy field that returns an AdminUser type, the user might be able to traverse from their order to the admin user's profile, including sensitive fields like the admin's email, phone number, or other orders they processed.

{
  me {
    orders {
      processedBy {
        email
        phone
        allProcessedOrders {
          customer {
            email
            paymentMethods { last4 }
          }
        }
      }
    }
  }
}

Through a chain of legitimate-looking relationship traversals, the attacker has accessed other customers' payment information. Each individual resolver works correctly. The authorization failure is in allowing unrestricted traversal across the graph.

Mutation Abuse and Side Effects

Mutations are GraphQL's mechanism for modifying data, and they carry the same authorization risks as queries, plus additional concerns around input validation and side effects.

Mass Assignment via Mutations

GraphQL input types define the fields a mutation accepts. If the input type includes fields that should not be user-modifiable, and the resolver passes the entire input object to the database update function, the result is mass assignment:

mutation {
  updateProfile(input: {
    name: "John"
    role: "admin"
    emailVerified: true
    subscriptionTier: "enterprise"
  }) {
    id name role
  }
}

If the updateProfile resolver does not explicitly allowlist which fields can be updated, the attacker can modify their role, verification status, and subscription tier in a single request. The GraphQL schema might technically accept these fields because the UserInput type was generated from the database model and includes all columns.

Discovering Hidden Mutations

Beyond introspection, testers can discover mutations through client-side JavaScript analysis. Single-page applications that use GraphQL typically contain all their queries and mutations in the JavaScript bundle. Tools like GraphQL Voyager can visualize the schema, while manual analysis of webpack bundles often reveals mutations that are defined but not used in the current UI, suggesting internal or deprecated functionality.[8]

During a recent engagement, we extracted 47 mutations from a client's JavaScript bundle. Only 23 were used in the production UI. Of the remaining 24, three were admin-level mutations (including one that could modify user roles) that were accessible to regular authenticated users because the resolvers only checked for a valid session token, not for admin privileges.

Information Disclosure Through Error Messages

GraphQL error handling is notoriously leaky. By default, many implementations return detailed error messages that include stack traces, database query details, internal file paths, and type system information.

Stack Traces in Production

When a resolver throws an unhandled exception, the error response often includes the full stack trace. This reveals the programming language, framework version, file structure, database type, and sometimes even database connection strings or API keys embedded in configuration paths.

Type Coercion Errors

Sending incorrect types for arguments produces error messages that reveal the expected type structure:

// Request: pass a string where an ID is expected
{ user(id: "not-a-number") { name } }

// Response might reveal:
{
  "errors": [{
    "message": "Argument 'id' of type 'Int!' was provided invalid value 'not-a-number'. Expected type 'Int', found 'not-a-number'. Coercion error: value is not a valid Int - at path 'user.id' in UserResolver.findById()"
  }]
}

This tells the attacker that user IDs are integers (not UUIDs), that the resolver function is called findById, and the class is UserResolver. Combined with field suggestion brute-forcing, these error messages help reconstruct the internal architecture.

Subscription Exploits

GraphQL subscriptions use WebSocket connections for real-time data streaming. The security implications are often overlooked because teams focus on query and mutation security.

Subscription Authorization

Subscriptions typically authenticate during the WebSocket handshake, but many implementations do not re-validate authorization when the subscription receives new data. If a user subscribes to orderUpdates while they have permission to view orders, and their permissions are later revoked, the subscription may continue streaming data because the authorization was only checked at connection time.

Subscription Flooding

An attacker can open hundreds of WebSocket connections, each subscribing to data-intensive streams, to exhaust server resources. Unlike HTTP requests that complete and release resources, WebSocket connections persist and continuously consume memory and CPU for each active subscription.

Tools for GraphQL Security Testing

Effective GraphQL testing requires specialized tools. Here is our recommended toolkit.

InQL (Burp Suite Extension)

InQL is the most comprehensive GraphQL testing extension for Burp Suite. It performs introspection analysis, generates queries and mutations for every operation in the schema, and provides a scanner that tests for common GraphQL vulnerabilities. The query editor allows manual crafting of test queries with syntax highlighting and auto-completion from the discovered schema.[3]

graphql-cop

graphql-cop is a Python-based security auditor specifically designed for GraphQL. It tests for introspection disclosure, field suggestion information leakage, batching support, query depth limits, alias-based attacks, and several other GraphQL-specific issues. Running it against a target takes seconds and provides an immediate overview of the attack surface.[9]

python3 graphql-cop.py -t https://target.com/graphql

GraphQL Voyager

Voyager provides an interactive visual representation of the GraphQL schema as a graph. Seeing the relationships between types visually makes it much easier to identify authorization bypass paths, circular references for DoS queries, and sensitive fields that should be restricted. Feed it an introspection result and it generates a navigable diagram of the entire API.[8]

Clairvoyance

When introspection is disabled, Clairvoyance reconstructs the schema through field suggestion analysis. It uses wordlists and iterative querying to discover types, fields, and arguments without introspection access. This is an essential tool for testing hardened GraphQL endpoints.[4]

Altair GraphQL Client

Altair is a feature-rich GraphQL client that supports custom headers, environments, file uploads, and subscription testing. Unlike the built-in GraphiQL playground, Altair allows full control over request headers and authentication tokens, making it suitable for security testing. It also supports query collections, making it easy to organize and replay test cases.

Real-World GraphQL Breaches

GraphQL security is not just a theoretical concern. Several high-profile incidents have demonstrated the real-world impact.

A Practical Testing Methodology

Here is the step-by-step approach we use for GraphQL security assessments at Lorikeet Security.

Phase 1: Reconnaissance

  1. Identify the GraphQL endpoint (typically /graphql, /api/graphql, or /gql)
  2. Test for introspection access (both authenticated and unauthenticated)
  3. If introspection is disabled, run Clairvoyance and analyze JavaScript bundles for queries
  4. Run graphql-cop for an automated baseline assessment
  5. Visualize the schema in GraphQL Voyager to identify relationship paths

Phase 2: Authorization Testing

  1. For every query and mutation, test with unauthenticated requests, low-privilege users, and cross-tenant sessions
  2. Test every nested relationship path for authorization leakage
  3. Look for sensitive fields on shared types (admin-only fields accessible to regular users)
  4. Test mutations for mass assignment by adding extra fields to input objects

Phase 3: Abuse Testing

  1. Test batched queries for rate limit bypass on login, OTP, and other sensitive mutations
  2. Test alias-based brute-force for the same endpoints
  3. Craft deep nesting queries to test for DoS protections
  4. Test fragment amplification
  5. Assess query complexity limits

Phase 4: Information Disclosure

  1. Send malformed queries and analyze error messages for stack traces and internal details
  2. Test field suggestions with common field names
  3. Look for debug mode indicators (GraphiQL playground, verbose errors, tracing extensions)

Common mistake: Many teams test their GraphQL API with the same methodology they use for REST APIs. This misses GraphQL-specific vulnerabilities like batching abuse, nesting DoS, and resolver-level authorization gaps. GraphQL requires its own testing methodology built around the query language's unique capabilities.

Hardening Your GraphQL API

Based on the vulnerabilities we find most frequently, here are the essential hardening steps for production GraphQL APIs.


GraphQL's power comes from its flexibility, and that same flexibility is what makes it challenging to secure. The single-endpoint architecture, client-driven query structure, and relationship-based data model create an attack surface that is fundamentally different from REST. Securing it requires understanding these differences and applying GraphQL-specific controls rather than retrofitting REST security patterns.

If your team has deployed a GraphQL API and has not had it specifically tested for GraphQL vulnerabilities, you likely have findings waiting to be discovered. The question is whether your pentest team or an attacker finds them first.

Sources

  1. GraphQL Foundation, "GraphQL Specification," https://spec.graphql.org/
  2. OWASP, "GraphQL - OWASP Cheat Sheet Series," https://cheatsheetseries.owasp.org/cheatsheets/GraphQL_Cheat_Sheet.html
  3. Doyensec, "InQL - Introspection GraphQL Scanner," https://github.com/doyensec/inql
  4. Nikita Stupin, "Clairvoyance - Obtain GraphQL API Schema Despite Disabled Introspection," https://github.com/nikitastupin/clairvoyance
  5. Assetnote, "Exploiting GraphQL," Assetnote Blog, https://blog.assetnote.io/2021/08/29/exploiting-graphql/
  6. PortSwigger, "GraphQL API Vulnerabilities," PortSwigger Web Security Academy, https://portswigger.net/web-security/graphql
  7. Apollo GraphQL, "Authorization in GraphQL," Apollo Documentation, https://www.apollographql.com/docs/apollo-server/security/authentication/
  8. GraphQL Voyager, "Represent any GraphQL API as an Interactive Graph," https://github.com/graphql-kit/graphql-voyager
  9. dolevf, "graphql-cop - Security Auditor Utility for GraphQL APIs," https://github.com/dolevf/graphql-cop
  10. GitLab, "CVE-2024-4835 - GraphQL Authorization Bypass," GitLab Security Releases, https://about.gitlab.com/releases/categories/releases/

Is Your GraphQL API Exposing More Than It Should?

GraphQL requires specialized testing that goes beyond traditional API security assessments. Our team tests for introspection leakage, batching abuse, authorization bypass, and DoS vulnerabilities specific to GraphQL architectures.

Book a Consultation Our Services
-- views
Link copied!
Lorikeet Security

Lorikeet Security Team

Penetration Testing & Cybersecurity Consulting

We've completed 170+ security engagements across web apps, APIs, cloud infrastructure, and AI-generated codebases. Everything we publish here comes from patterns we see in real client work.