Hacking GraphQL thumbnail: introspection, BOLA and batching attacks, with the GraphQL logo in a crosshair and a leaking data record

Hacking GraphQL APIs in 2026: Introspection, BOLA, and Batching Attacks

GraphQL is a gift to attackers. One endpoint, usually /graphql, that speaks a typed, self-describing language and will happily tell you every object, field, and mutation it supports if you just ask. REST spreads its attack surface across dozens of routes you have to discover; GraphQL hands you a map. The catch for defenders is that all of GraphQL’s flexibility – client-picked fields, aliases, batching, deep nesting – is exactly what turns into rate-limit bypass, access-control bugs, and denial of service in the wrong hands.

This is a hands-on playbook for testing GraphQL APIs the way it actually goes on an engagement: find the endpoint, fingerprint the engine, dump the schema (or rebuild it when introspection is off), then hunt the bugs that pay – BOLA/IDOR in resolvers, alias and batch abuse, and query-complexity DoS. Real queries, real tools, a copy-paste checklist, and the defensive fixes at the end. Only run this against targets you own or are explicitly authorized to test.

Hacking a GraphQL API: an introspection query in a terminal, the GraphQL node logo in a red crosshair, and a data graph leaking a private user record
One endpoint, a full schema, and a lot of ways to ask for data you should not see.

The short version (TL;DR)

  • Find the endpoint (/graphql, /api/graphql, /v1/graphql) and fingerprint the engine with graphw00f.
  • Run introspection to dump the entire schema. If it is disabled, use clairvoyance to rebuild it from “Did you mean…?” field suggestions.
  • The money bug is BOLA/IDOR: authorization is enforced per resolver, and developers forget it on individual fields and mutations constantly.
  • Alias and batch attacks pack many operations into one HTTP request – great for bypassing rate limits, brute forcing OTPs, and BOLA-via-aliases.
  • Deeply nested / circular queries cause denial of service when depth and cost limits are missing.
  • Best tools in 2026: InQL (Burp extension), graphw00f, clairvoyance, graphql-cop, plus a GraphQL IDE like Altair or Voyager for visual schema mapping.

Why GraphQL breaks REST testing habits

In REST, each URL is a separate thing to find, and access control usually sits at the route. In GraphQL, everything funnels through one endpoint and authorization has to be re-checked inside every resolver – the function that fetches each field. That single design choice is the source of most GraphQL bugs. A scanner that just crawls URLs sees one endpoint and moves on. The real surface is the schema behind it.

Three properties change how you test:

  • Self-describing. Introspection can hand you the full schema, including operations no UI ever calls.
  • Client-controlled shape. You choose fields, nesting, and how many operations to send. Servers that trust the client get wrecked.
  • Per-field authorization. One missing check on one field or mutation is a real vulnerability, even if the rest of the API is locked down.
GraphQL attack flow: fingerprint with graphw00f, run introspection, map the schema, then attack via BOLA/IDOR, alias and batch abuse, and depth-based DoS
The workflow: fingerprint, introspect, map, attack, then report and fix.

Step 1: Find and fingerprint the endpoint

GraphQL endpoints hide in predictable places. Check the common paths and watch for a tell-tale error when you send a bad query:

  • /graphql, /graphql/console, /graphiql, /api/graphql, /v1/graphql, /query, /gql
  • A GET to /graphql?query={__typename} that returns {"data":{"__typename":"Query"}} is a confirmed endpoint.
  • Watch JS bundles and mobile traffic – SPAs embed the endpoint and often sample queries.

Then fingerprint the engine. Different servers (Apollo, graphene, Hasura, graphql-php, and so on) have different default behaviours and known quirks, so knowing the engine tells you which attacks are worth trying:

# Identify the GraphQL engine to guide the rest of the test
pip install graphw00f
python3 -m graphw00f -f -t https://target.tld/graphql

Step 2: Dump the schema with introspection

Introspection is GraphQL’s built-in “describe yourself” feature. If it is on, one query gives you every type, field, argument, and mutation. Start small to confirm it is enabled:

POST /graphql HTTP/1.1
Host: target.tld
Content-Type: application/json

{"query":"{ __schema { queryType { name } mutationType { name } types { name kind } } }"}

If that returns type names, fire the full IntrospectionQuery (the same one GraphiQL uses) and load the result into a tool that makes it readable. Practical options:

  • InQL (Burp Suite extension, v6.1.x) – auto-generates every query, mutation, and subscription from the schema, flags circular references and “points of interest”, and sends generated requests straight to Repeater/Intruder.
  • Altair / GraphiQL – interactive IDE to explore and run operations.
  • GraphQL Voyager – turns the schema into a visual graph so you can spot sensitive types fast.

Read the schema like a target list: look for mutations (updateUser, deleteAccount, resetPassword), admin-flavoured types, fields like role, isAdmin, apiKey, ssn, and any operation the front end never seems to use.

Step 3: Introspection disabled? Rebuild the schema anyway

A disabled introspection endpoint is a speed bump, not a wall. GraphQL servers with field suggestions turned on will happily correct you – “Cannot query field ‘pasword’ … Did you mean ‘password’?” – and that leaks field names one guess at a time. clairvoyance automates exactly this to reconstruct a partial schema:

pip install clairvoyance
clairvoyance https://target.tld/graphql -o schema.json -w wordlist.txt
# Feed the rebuilt schema.json into InQL to generate operations

This is why “we disabled introspection” is not a fix on its own – it is defense in depth. If field suggestions are on, the schema is still recoverable.

Step 4: BOLA / IDOR in resolvers (the money bug)

This is the same broken access control we covered in the IDOR hunting playbook, but GraphQL makes it easier to find because every object is queryable by ID and each resolver has to enforce authorization on its own. Log in as a low-privilege user and request objects that are not yours:

query {
  user(id: "1043") {          # your id is 1042 - try a neighbour
    id
    email
    phone
    apiKey                      # sensitive field, often unprotected
  }
}

Test mutations too – they are frequently less guarded than queries:

mutation {
  updateUser(id: "1043", input: { email: "attacker@evil.tld" }) {
    id
    email
  }
}

Watch for nested object BOLA: an authorized top-level query that pulls an unauthorized child through a relationship, for example me { organization { members { email } } } returning members you should not see. The parent check passes; the nested resolver forgot to check.

Step 5: Alias and batch attacks

This is the GraphQL-specific trick that surprises testers coming from REST. Aliases let you request the same field many times in one operation under different names. If the server rate-limits by HTTP request instead of by operation, you just multiplied your attempts by 100 in a single request. That breaks OTP/2FA brute force protection, coupon checks, and login throttling:

mutation {
  a1: verifyOtp(code: "0001") { token }
  a2: verifyOtp(code: "0002") { token }
  a3: verifyOtp(code: "0003") { token }
  # ... generate up to 9999 aliases and send as ONE request
}

Batching is the sibling technique: send an array of separate operations in one HTTP call. Same effect for rate-limit bypass, and it is also the basis of the “BOLA via aliases/batching” attacks – probe many object IDs at once and diff the responses. InQL’s Batch Queries tab automates this.

[
  {"query":"{ user(id:\"1001\"){ email } }"},
  {"query":"{ user(id:\"1002\"){ email } }"},
  {"query":"{ user(id:\"1003\"){ email } }"}
]

Step 6: Query depth and complexity DoS

When two types reference each other, you can nest a query into itself until the server melts. No depth or cost limit means a single request can exhaust CPU and memory:

query {
  posts {
    author {
      posts {
        author {
          posts { author { name } }   # keep nesting the cycle
        }
      }
    }
  }
}

Related amplifiers: directive overloading (repeating a directive hundreds of times), field duplication (the same field 1000+ times), and alias overloading. Test these carefully and with permission – DoS testing can take a service down, so agree limits first and prove it with a measured response-time spike, not an outage.

Step 7: Injection, CSRF, and info leaks

GraphQL arguments flow into backend queries, so classic injection still applies – it just hides behind the schema:

  • SQL/NoSQL injection through arguments: user(id: "1 OR 1=1"), or operator injection into filters like { where: { role: { _eq: "admin" } } } on Hasura-style APIs.
  • CSRF: if the server accepts queries over GET or form-encoded POST, a cross-site request can trigger mutations. graphql-cop flags this class directly.
  • Info leaks: verbose errors, stack traces, and Apollo tracing/debug modes reveal internals. Field suggestions themselves are an info leak.

For a fast first pass across many of these at once, run an automated audit:

# Quick misconfig audit: introspection, batching, aliases, CSRF, debug modes
pip install graphql-cop
graphql-cop -t https://target.tld/graphql \
  --header '{"Authorization": "Bearer <token>"}'

Treat the output as leads, not findings – confirm each one by hand in Burp/InQL and show real impact before you report it.

Copy-paste GraphQL testing checklist

  1. Endpoint located (common paths, JS bundles, mobile traffic) and engine fingerprinted with graphw00f.
  2. Introspection tested; full schema dumped and loaded into InQL / Voyager.
  3. If introspection is off, clairvoyance run against field suggestions to rebuild the schema.
  4. Schema reviewed for sensitive fields, admin types, and unused mutations.
  5. BOLA/IDOR tested on queries AND mutations, including nested object relationships.
  6. Authorization probed as a low-privilege user against sensitive operations.
  7. Alias attack tried against OTP/login/coupon endpoints for rate-limit bypass.
  8. Batch (array) requests tried for rate-limit bypass and mass BOLA probing.
  9. Depth / circular / directive / field-duplication DoS tested within agreed limits.
  10. Argument injection (SQL/NoSQL/operator) and CSRF over GET tested.
  11. Info leaks checked: verbose errors, tracing/debug, field suggestions.
  12. Every finding confirmed manually with a clear PoC before reporting.

How to secure a GraphQL API (for developers)

GraphQL can be as safe as it is flexible – you just have to close the doors its flexibility opens:

  • Authorize every resolver and field. Treat each resolver as its own protected endpoint. Do not rely on the UI hiding an operation. This kills BOLA, the highest-impact class.
  • Disable introspection and GraphiQL in production – and turn off field suggestions, or clairvoyance rebuilds your schema anyway.
  • Cap query depth and cost. Add depth limits and a complexity/cost analysis rule so a single query cannot exhaust the server.
  • Rate-limit by operation, not by HTTP request. Count aliases and batched operations, or the throttle is trivially bypassed.
  • Disable or cap batching on sensitive mutations (login, OTP, payments).
  • Hide internal errors. No stack traces, no tracing/debug in prod.
  • Parameterize backend queries. GraphQL does not save you from SQL/NoSQL injection – the resolver still has to use safe queries.

For a deeper reference on the defensive side, see our GraphQL security attack and defense guide. And if you are hunting these on programs, this pairs with the bug bounty workflow – GraphQL BOLA and alias-based rate-limit bypass are consistently well-paid because scanners miss them.

FAQ

Is a GraphQL API safe if introspection is disabled?

No. Disabling introspection only raises the bar. If field suggestions are enabled, clairvoyance can rebuild the schema from error messages. And the real bugs – BOLA, alias abuse, DoS – do not need introspection at all once you know a few operation names.

Which tools should I start with?

graphw00f to fingerprint, InQL (Burp) to dump and generate operations, clairvoyance when introspection is off, and graphql-cop for a quick misconfig sweep. Add Altair or Voyager to explore the schema visually.

What is the highest-value GraphQL bug?

BOLA/IDOR in resolvers, usually. Reading or modifying other users’ data through an unprotected field or mutation is high to critical impact. Alias-based rate-limit bypass that enables OTP or 2FA brute force is a close second because it often leads to account takeover.

Are GraphQL APIs less secure than REST?

Not inherently – but they move authorization into every resolver and hand clients more control over query shape, so there are more places to get it wrong. Done right (resolver authz, depth/cost limits, batch-aware rate limiting), GraphQL is as safe as REST.

Is it safe to test for DoS?

Only with explicit permission and agreed limits. Prove the weakness with a controlled, measurable response-time increase rather than actually taking the service down. Many programs want a description and a small PoC, not a real outage.

Bottom line

GraphQL rewards testers who read the schema instead of crawling URLs. Fingerprint the engine, dump or rebuild the schema, then go straight for resolver-level BOLA and alias/batch abuse – that is where the well-paid, scanner-proof bugs live. For defenders, the whole story compresses to one line: authorize every resolver, cap query cost, and rate-limit by operation. Do that and GraphQL stays as powerful as it is safe.

Map a schema you are allowed to test, run the checklist end to end, and confirm every finding by hand before you write it up.

Leave a Reply