GraphQL Security Testing: Introspection, Injection, and Authorization Flaws

GraphQL has grown from a Facebook internal tool to the API layer of choice for modern web applications. Its flexibility is the point — clients request exactly the data they need, in a single query, without multiple roundtrips. But that same flexibility creates attack surface that REST APIs simply don't have.

When you swap your REST endpoints for a single /graphql endpoint, you don't just change your API architecture. You change your attack surface, your authorization model requirements, and what your security tests need to cover.

This guide covers the GraphQL-specific vulnerabilities pentesters look for — and how to test for them systematically.


Step 1: Reconnaissance via Introspection

GraphQL has a built-in feature called introspection: the ability to query the schema itself to discover all types, fields, queries, and mutations the API supports. In development, this is invaluable. In production, it's a detailed roadmap for attackers.

An attacker's first move against any GraphQL endpoint is to run the full introspection query:

query IntrospectionQuery {
  __schema {
    queryType { name }
    mutationType { name }
    subscriptionType { name }
    types {
      ...FullType
    }
  }
}

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

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

fragment TypeRef on __Type {
  kind
  name
  ofType {
    kind
    name
    ofType {
      kind
      name
      ofType { kind name ofType { kind name ofType { kind name ofType { kind name } } } }
    }
  }
}

If introspection is enabled in production, this query returns your complete API schema — every type, every field, every mutation, including ones that aren't in your documentation or frontend.

What pentesters look for in the schema:

Even without introspection, attackers have options: GraphQL Clairvoyance and similar tools can reconstruct schemas by sending words from dictionary lists and observing error messages. If the API suggests corrections ("Did you mean adminUsers?"), the schema is partially discoverable.

Testing introspection status

# Quick check — if this returns schema data, introspection is enabled
curl -s -X POST https://target.com/graphql \
  -H 'Content-Type: application/json' \
  -d '{"query":"{__schema{types{name}}}"}'

Remediation: Disable introspection in production. Most GraphQL libraries support this with a single configuration flag. Provide schema documentation through a separate, access-controlled documentation system instead.


GraphQL Injection

GraphQL doesn't eliminate injection — it changes where and how it manifests. When GraphQL arguments are passed unsanitized to backend data stores, the traditional injection vulnerabilities appear.

SQL injection through GraphQL arguments

If a resolver passes query arguments directly to a database query without parameterization:

query {
  users(filter: "name = 'admin'--") {
    id
    email
    passwordHash
  }
}

The filter argument lands in a SQL query. Classic SQL injection techniques apply: UNION selects, time-based blind injection, error-based extraction.

What to test:

NoSQL injection (MongoDB)

Applications using MongoDB as a GraphQL backend are susceptible to NoSQL injection through operator injection:

query {
  login(username: {"$gt": ""}, password: {"$gt": ""}) {
    token
    user { id role }
  }
}

The MongoDB $gt operator with an empty string matches any non-null value — effectively bypassing authentication entirely.

Server-Side Request Forgery via GraphQL

GraphQL mutations that accept URLs (webhook registration, avatar upload from URL, integration configuration) are a vehicle for SSRF. The server fetches the URL you provide:

mutation {
  setWebhook(url: "http://169.254.169.254/latest/meta-data/iam/security-credentials/") {
    success
  }
}

If the server returns the response of the fetched URL, you've achieved SSRF with data exfiltration. Even blind SSRF (no response returned) can probe internal network topology.


Authorization in GraphQL: Where It Goes Wrong

Authorization is GraphQL's most consistently misunderstood security concern. In REST, you put authorization checks in the endpoint handler — one handler, one authorization check. In GraphQL, a single query can traverse multiple types, each potentially owned by a different resolver. Each resolver is responsible for its own authorization, and it's easy to miss one.

Field-level authorization gaps

Consider a schema where User has both public fields (username, bio) and private fields (email, phoneNumber). If authorization is checked at the query resolver level but not at the individual field resolver level:

query {
  users {
    username
    email      # should be private
    phoneNumber # should be private
  }
}

An unauthenticated or low-privilege user enumerates all users with private contact information.

What pentesters test:

IDOR via GraphQL (Broken Object Level Authorization)

The BOLA/IDOR problem exists in GraphQL the same way it exists in REST. If an ID argument isn't validated against the requesting user's ownership:

query {
  order(id: "ord_9999") {    # this belongs to another user
    items { name price }
    paymentMethod { last4 type }
    shippingAddress { street city }
  }
}

Getting another user's order details, including payment and address data, is a significant privacy and compliance violation.

Mutation authorization gaps

Mutations often get less security testing than queries because they're less visible in normal application flow. Pentesters specifically probe:


Batching Attacks and Rate Limit Bypass

GraphQL's batching capability — the ability to send multiple queries in a single HTTP request — is a legitimate performance feature that attackers repurpose for brute-force and enumeration.

Query batching for credential brute-force

Standard rate limiting operates at the HTTP request level. If the API accepts batched queries:

[
  {"query": "mutation { login(email: \"admin@company.com\", password: \"password1\") { token } }"},
  {"query": "mutation { login(email: \"admin@company.com\", password: \"password2\") { token } }"},
  {"query": "mutation { login(email: \"admin@company.com\", password: \"password3\") { token } }"},
  ... (999 more)
]

One HTTP request carries 1,000 login attempts. Rate limiting fires based on request count, not attempt count. You've effectively bypassed it.

Alias-based batching

Even without array batching, GraphQL aliases allow multiple calls to the same resolver in one request:

query {
  a1: login(email: "admin@company.com", password: "password1") { token }
  a2: login(email: "admin@company.com", password: "password2") { token }
  a3: login(email: "admin@company.com", password: "password3") { token }
}

Same effect — rate limiting by HTTP request count is bypassed.

Field duplication / amplification

query {
  user(id: "1") {
    friend { friend { friend { friend { friend { friend {
      id name email posts { title body comments { body author { email } } }
    }}}}}}
  }
}

Deep nesting can cause exponential resolver calls, consuming backend resources disproportionate to the request size. This is sometimes called a "GraphQL bomb."

Remediation: Implement query depth limits, query complexity analysis, and request-level rate limiting that accounts for operation count within batched requests.


Information Disclosure via Error Messages

GraphQL APIs, especially in development mode or with verbose error handling, often return detailed error messages that reveal internal implementation details:

{
  "errors": [{
    "message": "Cannot query field 'adminToken' on type 'User'",
    "extensions": {
      "code": "GRAPHQL_VALIDATION_FAILED",
      "stacktrace": [
        "GraphQLError: ...",
        "at resolveField (/app/node_modules/graphql/execution/execute.js:461:18)"
      ]
    }
  }]
}

Stack traces reveal framework versions, file paths, and internal architecture. Suggestion errors reveal field names. Even suppressed stack traces may appear in server logs that are incorrectly exposed.

What pentesters probe:


Subscription Security

GraphQL subscriptions — real-time updates over WebSocket — introduce additional attack surface that's often overlooked in security reviews:


GraphQL Security Testing Toolkit

Tool Use Case
GraphQL Voyager Visualize introspection results as an interactive graph
InQL (Burp Suite plugin) Automatic query generation from introspection, security testing
GraphQL Clairvoyance Schema reconstruction when introspection is disabled
Altair GraphQL Client Manual exploration with authentication support
graphql-cop Automated GraphQL security audit (DoS, introspection, batching)
nuclei GraphQL templates Automated detection of introspection, common misconfigurations

A GraphQL Security Testing Checklist

Reconnaissance

Authorization

Injection

Rate limiting and DoS

Information disclosure

Ironimo's scanner includes nuclei templates for GraphQL security misconfigurations — introspection detection, batching attack surface identification, and common header/configuration issues. For GraphQL authorization testing (BOLA, field-level access), pair Ironimo's automated baseline with manual testing using the checklist above.

Start free scan
← Back to Blog