API deployments grew 167% year-over-year according to the 2025 State of APIs report, and GraphQL is claiming an outsized share of that growth.[1] What was once a niche alternative championed by Facebook has become the default API architecture for companies like GitHub, Shopify, Stripe, PayPal, Twitter, and a rapidly expanding list of enterprises that decided REST endpoints were not keeping up with the complexity of their frontend requirements.

Yet the penetration testing industry has not caught up. The vast majority of API security assessments still follow REST-centric methodologies: enumerate endpoints, test each HTTP verb, fuzz parameters, check for BOLA on sequential IDs, validate JWT handling, and move on. When the target API is GraphQL, these testers interact with a single POST /graphql endpoint, run a handful of queries pulled from the client-side JavaScript, and call it tested.

That approach misses entire categories of vulnerabilities that are unique to GraphQL's query language, type system, and resolver architecture. In our engagements, GraphQL APIs produce 2-3x more critical findings than equivalent REST APIs, not because GraphQL is inherently insecure, but because the attack surface is fundamentally different and most security teams are not testing for it.

Why REST-Centric Testing Fails on GraphQL


REST and GraphQL differ in ways that break core assumptions of traditional API pentesting.

In REST, the server defines what data each endpoint returns. GET /api/users/123 returns a fixed JSON structure determined by backend code. The client has no say in what fields come back. This means the attack surface is the set of endpoints and HTTP methods, and testing means hitting each endpoint with manipulated inputs.

In GraphQL, the client decides. A single endpoint accepts an arbitrarily complex query that can traverse relationships, batch multiple operations, nest to arbitrary depth, and select any combination of fields from the schema. The server's job is to resolve whatever the client asks for. This inverts the security model. Instead of securing 50 endpoints with fixed behavior, you are securing a query language that can express millions of unique requests against your data graph.

Key distinction: REST security is about protecting endpoints. GraphQL security is about protecting a query language. Every resolver, every field, every relationship path, and every combination of operations is a potential attack vector. A pentester who only tests the queries they find in the frontend JavaScript is testing less than 1% of what the API actually accepts.

This fundamental difference means that REST-centric testing misses these GraphQL-specific attack classes:

Introspection Queries: The Complete Reconnaissance Tool


GraphQL introspection is a built-in feature that allows any client to query the schema itself. A single introspection query returns every type, field, argument, mutation, subscription, enum, and relationship in your API. This is not a vulnerability being exploited. It is a feature working as designed.

# Full introspection query - returns your entire API schema
{
  __schema {
    queryType { name }
    mutationType { name }
    subscriptionType { name }
    types {
      name
      kind
      fields {
        name
        args {
          name
          type { name kind ofType { name kind } }
        }
        type { name kind ofType { name kind } }
      }
    }
  }
}

When introspection is enabled in production (which we find in approximately 60% of GraphQL deployments we test), the attacker receives a complete map of the API before writing a single exploit. We routinely discover hidden mutations like adminCreateUser, internalTransferFunds, debugResetPassword, and exportAllCustomerData that are not referenced anywhere in the client application but are fully accessible through the GraphQL endpoint.[2]

Beyond Disabling Introspection

Many teams disable introspection and consider the attack surface closed. It is not. GraphQL implementations leak schema information through two additional channels.

Field suggestions reveal field names through error messages when you send queries with misspelled fields:

# Request
query { user { emai } }

# Response - leaks the actual field name
{
  "errors": [{
    "message": "Cannot query field 'emai' on type 'User'. Did you mean 'email' or 'emailVerified'?"
  }]
}

The tool Clairvoyance automates this process, iterating through wordlists and collecting suggestions until it reconstructs 70-90% of the schema without introspection access.[3] Combined with field names extracted from client-side JavaScript bundles, an attacker can typically recover the full schema within minutes even on hardened deployments.

Query Depth and Complexity Attacks


GraphQL's relational query model allows clients to traverse relationships to arbitrary depth. When your schema defines circular relationships (which nearly every real-world schema does), attackers can construct queries that grow exponentially in resolution cost.

Consider a schema where users have posts, posts have comments, and comments have an author (a user). This creates a cycle: User -> Posts -> Comments -> Author -> Posts -> Comments -> Author, repeating infinitely:

# Exponential resource consumption through circular relationship traversal
query DepthAttack {
  users(first: 100) {
    posts(first: 100) {
      comments(first: 100) {
        author {
          posts(first: 100) {
            comments(first: 100) {
              author {
                posts(first: 100) {
                  comments(first: 100) {
                    author {
                      email
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

This single query requests 100 users, each with 100 posts, each with 100 comments, each traversed 3 levels deep. The theoretical resolution cost is 100^7 = 100 trillion resolver invocations. Even with pagination limits, a depth of 8-10 levels with 10-20 items per level can generate millions of database queries and consume all available server memory.

Real-world impact: In a recent engagement, we crashed a production GraphQL API with a single query that nested 12 levels deep through a user-organization-team-member-project-task-assignee cycle. The server ran out of memory in 4.2 seconds. There was no query depth limit, no complexity analysis, and no timeout on query execution. The application had passed its previous pentest with zero findings on the API.

Complexity Analysis vs. Depth Limiting

Simple depth limiting (rejecting queries beyond N levels) is a start but insufficient. An attacker can stay within the depth limit while still generating massive cost through wide queries at each level. The correct defense is query complexity analysis, where each field has an assigned cost multiplied by its pagination arguments:

# This query is only 3 levels deep but has enormous complexity
query WideAttack {
  users(first: 1000) {        # cost: 1000
    posts(first: 1000) {       # cost: 1000 * 1000 = 1,000,000
      title
      body
      comments(first: 1000) {  # cost: 1000 * 1000 * 1000 = 1,000,000,000
        text
      }
    }
  }
}
# Total complexity: ~1,001,001,000 (exceeds any reasonable threshold)

Libraries like graphql-query-complexity for Node.js and graphql-ruby's built-in complexity analyzer allow you to assign cost values to fields and reject queries exceeding a threshold before execution begins.[4]

Batching Attacks: Bypassing Rate Limits


GraphQL supports two forms of operation batching that most rate limiting implementations do not account for: array batching and alias batching. Both allow an attacker to execute hundreds or thousands of operations in a single HTTP request, completely bypassing per-request rate limits.

Array Batching

Most GraphQL servers accept an array of operations in a single POST request:

# Single HTTP request containing 1000 login attempts
POST /graphql
Content-Type: application/json

[
  {"query": "mutation { login(email: \"[email protected]\", password: \"password1\") { token } }"},
  {"query": "mutation { login(email: \"[email protected]\", password: \"password2\") { token } }"},
  {"query": "mutation { login(email: \"[email protected]\", password: \"password3\") { token } }"},
  ... // 997 more attempts
]

The server processes all 1,000 login attempts as part of a single HTTP request. If rate limiting is implemented at the HTTP level (counting requests), this counts as one request. The attacker gets 1,000 password guesses per rate limit window.

Alias Batching

Even when array batching is disabled, GraphQL aliases allow multiple invocations of the same field or mutation within a single query:

# Single query with 100 login attempts using aliases
mutation BruteForce {
  attempt1: login(email: "[email protected]", password: "password1") { token }
  attempt2: login(email: "[email protected]", password: "password2") { token }
  attempt3: login(email: "[email protected]", password: "password3") { token }
  attempt4: login(email: "[email protected]", password: "password4") { token }
  # ... up to hundreds of aliases
  attempt100: login(email: "[email protected]", password: "password100") { token }
}

This is a single query in a single HTTP request. Each alias executes the login mutation independently with different arguments. We have used this technique to brute-force OTP codes (typically 6 digits = 1,000,000 combinations), bypass account lockout mechanisms, and enumerate valid usernames through timing differences across aliased operations.[5]

Remediation: Rate limiting must count operations, not requests. Count each element in a batch array and each alias invocation separately. Libraries like graphql-rate-limit and graphql-shield provide resolver-level rate limiting that correctly counts per-operation regardless of batching method.

Authorization Bypass Through Field-Level Access Control Failures


This is the most impactful category of GraphQL vulnerabilities we find, and it stems from a fundamental misunderstanding of how GraphQL resolves data. In REST, authorization is typically checked once at the endpoint level. In GraphQL, each field has its own resolver, and authorization must be enforced at every resolver that accesses sensitive data.

Consider a schema with a User type that has both public and sensitive fields:

type User {
  id: ID!
  name: String!           # Public
  avatar: String           # Public
  email: String!           # Sensitive - only self or admin
  ssn: String              # Highly sensitive - only self
  salary: Float            # Highly sensitive - only HR/admin
  internalNotes: String    # Internal only - admin
}

The developer correctly adds authorization to the user query resolver, checking that the requester can access the target user. But the individual field resolvers for email, ssn, salary, and internalNotes inherit the parent resolver's context without additional checks. Since the parent allowed access (the requester can see the user's public profile), every field on that user is returned.

Relationship Traversal Bypass

The problem compounds through relationships. Even if direct access to sensitive fields is restricted, attackers can reach the same data through alternative graph paths:

# Direct query - properly blocked
query { user(id: "other-user") { salary } }
# Response: "Not authorized to access this user's salary"

# Relationship traversal - bypasses authorization
query {
  project(id: "shared-project") {
    team {
      members {
        salary    # Same field, accessed through a different path
        ssn       # Resolver doesn't re-check authorization
      }
    }
  }
}

The project query checks that the requester is a project member. The team resolver returns the project's team. The members resolver returns team members. At no point does the salary or ssn field resolver verify that the requester is authorized to see this specific data for this specific user. Authorization was checked at the entry point but not propagated to nested resolvers.[6]

In one engagement, we accessed the salary data of every employee at a 500-person company by traversing through a shared project that a low-privilege guest account had been invited to. The project query was authorized, and nothing downstream re-validated access.

N+1 Query Abuse for Denial of Service


The N+1 query problem is a well-known performance antipattern in GraphQL, but it is also a security vulnerability when intentionally exploited. When a resolver fetches related data without batching (using DataLoader or equivalent), each parent record triggers a separate database query for its children.

# This query triggers 1 + N + N*M + N*M*K database queries
query N1Attack {
  organizations {              # 1 query: SELECT * FROM organizations
    employees {                # N queries: SELECT * FROM employees WHERE org_id = ?
      projects {               # N*M queries: SELECT * FROM projects WHERE employee_id = ?
        tasks {                # N*M*K queries: SELECT * FROM tasks WHERE project_id = ?
          title
          assignee { name }    # N*M*K*L queries: SELECT * FROM users WHERE id = ?
        }
      }
    }
  }
}

With 10 organizations, 50 employees each, 10 projects each, and 20 tasks each, this generates 1 + 10 + 500 + 5,000 + 100,000 = 105,511 database queries from a single GraphQL request. Without DataLoader batching (which we find absent in roughly 40% of GraphQL deployments), each of those is a separate round-trip to the database.

Even with DataLoader, attackers can construct queries that maximize unique cache keys to force individual lookups. The combination of N+1 exploitation with fragment spreading creates particularly effective resource exhaustion:

# Fragment spreading amplifies resolver execution
fragment UserFields on User {
  posts { author { posts { author { name } } } }
}

query Amplified {
  users(first: 50) {
    ...UserFields
    friends(first: 50) {
      ...UserFields
    }
  }
}

Mutation-Based Attacks: Mass Assignment via GraphQL Inputs


GraphQL's typed input system creates a unique mass assignment attack surface. Unlike REST APIs where unexpected JSON fields are typically ignored by most frameworks, GraphQL mutations with permissive input types can accept and process fields that the developer did not intend to be user-modifiable.

# The schema defines an UpdateUser input
input UpdateUserInput {
  name: String
  email: String
  avatar: String
  role: UserRole        # Should only be settable by admins
  isVerified: Boolean   # Should only be settable by system
  creditBalance: Float  # Should only be modified through transactions
}

# Attacker sends a mutation including restricted fields
mutation {
  updateUser(input: {
    name: "Normal Update"
    role: ADMIN
    isVerified: true
    creditBalance: 999999.99
  }) {
    id
    role
    isVerified
    creditBalance
  }
}

If the mutation resolver applies the entire input object to the database update without filtering, the attacker has escalated to admin, verified their account, and given themselves unlimited credit in a single request. The GraphQL type system makes this attack easier than in REST because the attacker can discover all available input fields through introspection and knows exactly what types and values each field accepts.[7]

Nested Mutation Injection

GraphQL mutations with nested input types are particularly dangerous because the nested objects often map directly to related database records:

mutation {
  createOrder(input: {
    items: [{ productId: "123", quantity: 1 }]
    payment: {
      amount: 0.01          # Attacker sets their own price
      currency: "USD"
      method: "credit_card"
    }
    shipping: {
      priority: "express"    # Gets express shipping
      cost: 0.00             # At zero cost
    }
  }) {
    id
    total
  }
}

When mutation resolvers trust input values for fields that should be server-calculated (prices, shipping costs, tax amounts), the attack is trivially exploitable. We find this pattern frequently in e-commerce GraphQL APIs where the frontend sends calculated totals and the backend does not recalculate.

Subscription Abuse for Data Exfiltration


GraphQL subscriptions use WebSocket connections to push real-time data to clients. This introduces attack vectors that do not exist in request-response APIs. A subscription, once established, continuously streams data without requiring additional authentication checks per message.

# Subscribe to all order events - may leak other users' orders
subscription {
  orderUpdated {
    id
    customer {
      name
      email
      address
    }
    items { name quantity price }
    total
    paymentMethod { last4 type }
  }
}

Common subscription authorization failures we exploit:

Testing approach: Establish subscriptions with different privilege levels and monitor what events are received. Use multiple WebSocket clients to test connection limits. Verify that token expiry terminates active subscriptions. Test subscription creation for operations the user should not have access to.

A Complete GraphQL Pentesting Methodology


Based on hundreds of GraphQL assessments, here is the methodology we follow at Lorikeet Security. Each phase builds on the previous one.

Phase 1: Schema Analysis and Reconnaissance

  1. Identify the GraphQL endpoint (common paths: /graphql, /api/graphql, /gql, /query)
  2. Test introspection (authenticated and unauthenticated)
  3. If introspection is disabled, run Clairvoyance for schema reconstruction via field suggestions
  4. Extract queries from client-side JavaScript bundles and mobile app binaries
  5. Run graphql-cop for automated baseline assessment
  6. Visualize the schema in GraphQL Voyager to map relationship cycles and sensitive fields
  7. Catalog all mutations, especially those not referenced by the frontend

Phase 2: Authorization Testing Per Field

  1. Test every query and mutation with: no auth, low-privilege auth, cross-tenant auth, expired tokens
  2. Test every sensitive field through direct access AND relationship traversal paths
  3. Map authorization boundaries: which fields are protected, which are not, and where do protections have gaps
  4. Test mutations for mass assignment by including every field from the input type
  5. Test subscription authorization for each event type and privilege level

Phase 3: Rate Limiting and DoS Analysis

  1. Test array batching with 10, 100, and 1000 operations per request
  2. Test alias batching on authentication mutations (login, OTP verification, password reset)
  3. Craft deep nesting queries targeting circular relationships
  4. Test wide queries that maximize resolver execution at each level
  5. Measure query execution time to identify N+1 vulnerable resolvers
  6. Verify query complexity limits are enforced before execution (not after timeout)

Phase 4: Query Complexity Analysis

  1. Determine maximum query depth allowed
  2. Test whether complexity is calculated statically (before execution) or dynamically (during execution)
  3. Craft queries that stay under complexity limits but maximize actual resource consumption
  4. Test fragment spreading for complexity amplification
  5. Verify that persisted queries or query allowlisting is enforced, if present

Tools for GraphQL Penetration Testing


Effective GraphQL pentesting requires purpose-built tooling. Here is the toolkit we use in every engagement.

Burp Suite with GraphQL Extensions

Burp Suite remains the foundation, but it needs GraphQL-specific extensions to be effective. The InQL extension by Doyensec performs introspection analysis, generates queries and mutations for every schema operation, and includes an automated scanner for common GraphQL vulnerabilities. The GraphQL Raider extension adds GraphQL-aware editing and repeating capabilities to Burp's core tools.[8]

graphql-cop

A Python-based security auditor that tests for introspection disclosure, field suggestion leakage, batching support, query depth limits, alias-based attacks, GET-based query execution, and several other GraphQL-specific issues in seconds:[9]

# Run a comprehensive baseline scan
python3 graphql-cop.py -t https://target.com/graphql

# Example output:
# [HIGH] Introspection enabled - full schema accessible
# [HIGH] Batch queries supported - no operation limit detected
# [MEDIUM] Field suggestions enabled - schema leakage possible
# [MEDIUM] No query depth limit detected
# [LOW] GET method queries supported - potential CSRF risk
# [INFO] GraphQL implementation: Apollo Server v4.x

Clairvoyance

When introspection is disabled, Clairvoyance reconstructs the schema through systematic field suggestion brute-forcing. Feed it a wordlist of common field names and it iteratively discovers types, fields, and arguments. Essential for testing hardened GraphQL endpoints.[3]

GraphQL Voyager

Visualizes the schema as an interactive graph diagram, making it easy to spot circular relationships (for depth attacks), identify sensitive fields on shared types (for authorization testing), and map the complete relationship graph that an attacker can traverse.[10]

Altair GraphQL Client

A full-featured GraphQL client that supports custom headers, environments, file uploads, and subscription testing over WebSocket. Unlike GraphiQL, Altair provides complete control over authentication headers and request configuration, making it suitable for security testing workflows.

gRPC Security Testing: The Next Frontier


While GraphQL dominates the client-facing API layer, gRPC is quietly becoming the backbone of internal service communication. Microservice architectures increasingly use gRPC for its performance benefits: binary serialization with Protocol Buffers, HTTP/2 multiplexing, bidirectional streaming, and strong typing from .proto definitions.

gRPC introduces its own security testing challenges that are even less understood than GraphQL's:

Tools like grpcurl, grpcui, and the gRPC Burp extension are beginning to address the tooling gap, but gRPC security testing remains a specialized skill that few pentesting firms offer. As enterprises migrate more internal APIs to gRPC, this gap will become a significant blind spot.[11]

Looking ahead: The API landscape is fragmenting. REST, GraphQL, gRPC, WebSocket APIs, and increasingly, tRPC and server-sent events. Each protocol has its own attack surface and requires its own testing methodology. Security teams that only test REST are falling further behind the architectures their organizations actually deploy.

Building a GraphQL Security Program


Testing is essential but not sufficient. Organizations with mature GraphQL deployments need to build security into the development lifecycle.


GraphQL is not going away. Its developer experience advantages are real, and the architecture is well-suited for complex applications with diverse frontend requirements. But its security model is fundamentally different from REST, and organizations that apply REST security patterns to GraphQL deployments are leaving critical vulnerabilities undiscovered.

The 167% growth in API deployments means the attack surface is expanding faster than most security programs can keep up. If your organization runs GraphQL in production, and statistically there is a growing chance it does, you need pentesters who understand the query language, the resolver architecture, and the specific attack classes that only exist in this paradigm.

Sources

  1. Postman, "2025 State of APIs Report," https://www.postman.com/state-of-api/
  2. OWASP, "GraphQL - OWASP Cheat Sheet Series," https://cheatsheetseries.owasp.org/cheatsheets/GraphQL_Cheat_Sheet.html
  3. Nikita Stupin, "Clairvoyance - Obtain GraphQL API Schema Despite Disabled Introspection," https://github.com/nikitastupin/clairvoyance
  4. Ivan Goncharov, "graphql-query-complexity," https://github.com/slicknode/graphql-query-complexity
  5. Assetnote, "Exploiting GraphQL," https://blog.assetnote.io/2021/08/29/exploiting-graphql/
  6. Apollo GraphQL, "Authorization in GraphQL," https://www.apollographql.com/docs/apollo-server/security/authentication/
  7. PortSwigger, "GraphQL API Vulnerabilities," https://portswigger.net/web-security/graphql
  8. Doyensec, "InQL - Introspection GraphQL Scanner," https://github.com/doyensec/inql
  9. dolevf, "graphql-cop - Security Auditor Utility for GraphQL APIs," https://github.com/dolevf/graphql-cop
  10. GraphQL Voyager, "Represent any GraphQL API as an Interactive Graph," https://github.com/graphql-kit/graphql-voyager
  11. gRPC Authors, "gRPC Server Reflection Tutorial," https://grpc.io/docs/guides/reflection/

Is Your GraphQL API Actually Tested for GraphQL Vulnerabilities?

Most API pentests follow REST methodologies and miss GraphQL-specific attack vectors entirely. Our team tests for introspection leakage, batching abuse, field-level authorization bypass, query complexity attacks, and subscription abuse specific to your GraphQL architecture.

Book a Consultation View Pricing
-- 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.