This checklist covers key GraphQL concepts such as queries, mutations, subscriptions, introspection, variables, fields, types, and arguments, along with related vulnerability assessment and penetration testing pointers.
Standard read operations used to fetch data.
query {
user(id: "123") {
name
email
}
}- Overfetching sensitive fields (
password,roles, etc.) - Field-level access control missing
- Injection in arguments
- Enumeration of all fields via introspection
Used for writing/updating data on the server.
mutation {
updateUser(id: "123", email: "evil@example.com") {
id
}
}- Privilege escalation (e.g., update roles without authorization)
- Mass assignment issues
- Missing CSRF protection
- Input validation bypass
Allow clients to receive real-time updates (WebSockets).
subscription {
messageAdded {
id
content
}
}- Access control on real-time channels
- Information leakage via live data streams
- Abuse of real-time subscriptions to cause DoS
- Token/session invalidation not enforced
Allows schema discovery by clients. Should be disabled in production.
query {
__schema {
types {
name
}
}
}query {
__schema {
queryType {
name
}
}
}- Check if introspection is enabled. If disabled, try opting for Suggestions
- Use introspection to discover undocumented APIs
- Use
__type,__schema,__typenameto enumerate system - Check for adding CRLF characters after
__schema,__type,__typenameas required, if these strings are blocked through regex
GraphQL engines often return helpful suggestions in error responses when queries include incorrect field names, types, or arguments. This is a developer convenience feature, but it can leak internal schema details in production environments.
{
useer(id: "123") {
name
}
}Response:
{
"errors": [
{
"message": "Cannot query field 'useer' on type 'Query'. Did you mean 'user'?"
}
]
}- Schema enumeration: Suggestions may leak valid field names, types, or enum values not otherwise discoverable.
- Bypass disabled introspection: Even if introspection is disabled, suggestion messages can expose internal structure.
- Fuzzing aid: Attackers can automate typos and use suggestions to discover valid schema paths.
- Error-based attacks: Suggestion messages can be used to refine malicious queries without triggering logs or alerts.
- Sanitize error messages in production to suppress or remove suggestion hints.
- Use centralized error handling with generic messages like:
"Invalid field"instead of"Did you mean ...?". - Ensure introspection and suggestion features are disabled or heavily restricted in production APIs.
- Monitor logs for repeated typo-like queries β a sign of automated fuzzing or discovery attempts.
query getUser($id: ID!) {
user(id: $id) {
name
}
}- SQLi, NoSQLi, SSTI via variables
- Type confusion attacks
- Log leakage via GET variables
- Excessive variable size (DoS)
{
user(id: 1) {
name
email
role
}
}- Overfetching sensitive fields
- Access control issues
- Hidden/internal fields accessible
type User {
id: ID!
name: String!
email: String!
}- Type introspection and enumeration
- Abuse of custom scalars/inputs
- Broken type enforcement (e.g.,
IDvsString)
{
searchUsers(name: "admin") {
id
}
}- Injection via argument values
- Malformed or excessive arguments
- Sending complex object input where primitive is expected
GraphQL directives like @include, @skip, @deprecated.
query {
user(id: "1") {
name
email @include(if: true)
}
}- Bypass logic using
@skip,@include - Check for custom directives
- Use to control branching behavior in query
Aliases allow clients to rename the result fields of a GraphQL query. Useful when you want to request the same field with different arguments multiple times.
query {
firstUser: user(id: "1") {
name
email
}
secondUser: user(id: "2") {
name
email
}
}- Bypass detection rules: Aliases can obfuscate malicious activity in logs and WAFs.
- RBAC Bypass: If access control is implemented based on field names, aliases may bypass them.
- Mass Enumeration: Abuse aliases to query multiple sensitive users in one request.
- Denial of Service: Use aliases to duplicate expensive queries and consume resources.
- Over-fetching: Hidden internal fields may be accessed multiple times under different aliases.
- Apply rate limiting and depth/complexity analysis after alias resolution.
- Normalize and log actual resolved queries to detect obfuscation.
- Implement field-level authorization after query alias expansion.
- Limit the number of unique aliases per request.
The schema defines the entire structure of a GraphQL API β the types, queries, mutations, subscriptions, and how they relate. It acts as the contract between the client and server.
schema {
query: Query
mutation: Mutation
subscription: Subscription
}A schema is typically composed of:
Querytype β for reading/fetching dataMutationtype β for writing/updating/deleting dataSubscriptiontype β for real-time data
- Introspection exposure: Ensure introspection is disabled in production or properly restricted.
- Leaky schema data: Hidden/internal types or fields may still be visible via introspection or error messages.
- Excessive exposure: Schema exposes mutations or queries not needed by the client.
- Unprotected inputs: Inputs (especially for mutations) allow arbitrary nested values.
- Lack of validation: Schema does not enforce strong types, allowing bypasses (e.g., accepting nulls or large objects).
- Weak custom scalars: Scalars like
JSON,DateTime, orUploadcan increase attack surface. - Naming conventions: Schema leaks functionality or role-based logic through poorly named types or fields (e.g.,
deleteUserAsAdmin).
- Disable introspection or restrict it to authenticated/internal users.
- Regularly audit schema exposure β trim unused or legacy fields/types.
- Use schema validation tools to enforce input constraints.
- Apply role-based access control at schema-resolved level.
- Limit maximum query depth and query complexity using libraries like:
graphql-depth-limitgraphql-query-complexity
- Log schema introspection attempts and unusual introspection queries.
GraphQL supports two kinds of comments:
These are prefixed with # and are ignored by the server.
{
# Fetching user data
user(id: "123") {
name
email
}
}- Log pollution: Comments might appear in logs and can leak internal client info or sensitive content.
- Obfuscation: Comments can be used to bypass simple WAF regex or signature-based detections.
- Developer leakage: Commented-out lines might reveal old or sensitive logic.
- Fingerprinting: Unique or consistent comment formats may identify internal tools or client libraries.
- Normalize queries to remove comments before logging or processing.
- Configure WAF and detection tools to ignore or strip comments before analysis.
- Educate developers not to include sensitive details in inline comments.
These are enclosed in triple quotes (""") and used to describe schema types, fields, or arguments.
"""
A user of the system with roles and permissions
"""
type User {
"""
Unique ID of the user
"""
id: ID!
"""
User's login email address
"""
email: String!
}- Overly descriptive fields: Docstrings may disclose internal implementation details or assumptions.
- Leaked internal logic: Comments may reference roles, permissions, or internal rules.
- Hidden endpoints: Docstrings may reference deprecated or undocumented operations that are still functional.
- Discovery aid: Docstrings combined with introspection can help attackers understand business logic deeply.
- Review and sanitize all schema documentation strings before deployment.
- Avoid references to internal systems, logic, or decision-making criteria.
- Disable or limit introspection in production to reduce visibility of doc comments.
- Use tools to lint or audit schema doc comments for sensitive disclosures.
Send multiple operations in one request.
[
{ "query": "{ user(id:1){name} }" },
{ "query": "{ user(id:2){name} }" }
]- Abuse batching to brute-force IDs
- DoS via large batch payloads
- RBAC bypass with varied batch contexts
- Disable introspection in production
- Apply field-level RBAC
- Limit query depth and complexity
- Use whitelisting for allowed operations
- Implement proper input validation and sanitization
- Protect endpoints with authN/authZ mechanisms
- Rate limit GraphQL requests
- Log and monitor GraphQL usage
| What to check | How |
|---|---|
| GraphQL endpoint exposed at predictable path | /graphql, /gql, /api/graphql, etc. |
| GraphiQL or Playground exposed | Visit endpoint in browser β check if introspection is open |
| Introspection enabled | Query schema with __schema, __type, __typename |
| Documentation exposure | Try accessing /graphql, check for docs tab |
- Identify GraphQL endpoint paths:
/graphql,/gql,/api/graphql, etc. - Look for open GraphiQL/Altair/Playground interfaces.
- Use introspection to enumerate schema (if enabled):
query IntrospectionQuery {
__schema {
types {
name
fields {
name
}
}
}
}query Introspection {
__schema {
types {
name
kind
fields {
name
type {
name
kind
}
}
}
}
}#Full introspection query
query IntrospectionQuery {
__schema {
queryType {
name
}
mutationType {
name
}
subscriptionType {
name
}
types {
...FullType
}
directives {
name
description
args {
...InputValue
}
onOperation #Often needs to be deleted to run query
onFragment #Often needs to be deleted to run query
onField #Often needs to be deleted to run query
}
}
}
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
}
}
}
}- Check for WAF/CDN protections or rate limits.
- Try queries without tokens.
- Try expired/forged tokens.
- Try different roles (admin/read/write) to test field-level access.
- Check RBAC and ABAC configurations.
| Check | Method |
|---|---|
| Missing/broken auth on sensitive queries/mutations | Try accessing as anonymous/low-privileged user |
| Role-based access controls (RBAC) | Check if read users can access admin fields |
| IDOR via object access | Try accessing other usersβ id, email, profile, etc. |
| API tokens/leakage | Tokens returned in response (JWT, session) |
| Session fixation/rotation issues | JWT reuse across users or roles |
Authorization,X-Hasura-Role,X-Forwarded-For, custom headers- Test for header injection and trust chaining
query getUserInfo {
user(id: "1") {
name
email
}
}{
"id": "1"
}- Compare behavior, bypass filters
query: read-onlymutation: data changesubscription: WebSocket stream (real-time)
query {
searchUsers(query: "admin@example.com") {
id
}
}mutation {
resetPassword(email: "attacker@evil.com") {
success
}
}subscription {
userCreated {
id
name
}
}- Monitor real-time data leakage
fragment commonFields on User {
id
email
}
query {
user(id: 1) {
...commonFields
}
}- Test for field exposure via fragments
| Directive | Description |
|---|---|
@include(if:) |
Conditionally include a field |
@skip(if:) |
Conditionally skip a field |
@deprecated |
Marks a field as deprecated |
| Custom Directives | App-defined logic and functionality |
query example($shouldSkip: Boolean!) {
users {
id
name @skip(if: $shouldSkip)
}
}| Tool | Description |
|---|---|
| Headers | Authorization, X-User-Id, X-Hasura-Role, X-Forwarded-For, X-Auth-Token |
| Query | Try mutation instead of query, malformed syntax |
| Variables | Modify JSON variable payloads to bypass checks |
| Fragments | Abuse fragment spreading for circular queries |
| Directives | Try @skip, @include, etc. for conditional abuse |
| Check | Expected |
|---|---|
| CORS misconfigurations | Only allow trusted origins |
| Strict Transport Security | Strict-Transport-Security: max-age=63072000; includeSubDomains; preload |
| Content Security Policy | Should prevent XSS and code injection |
| TLS only | GraphQL should never be exposed on HTTP |
| No caching of sensitive responses | Cache-Control: no-store |
| Test | Method |
|---|---|
| Overexposed fields | Look for fields like password, token, secret, jwt, debug, logs |
| Email leakage | Try listing all user emails |
| Internal fields exposed | e.g., _internalId, _debugInfo, stackTrace |
| Misconfigured introspection | Disable in production or limit to trusted users |
| Injection Type | Test |
|---|---|
| SQL injection | Inject into string fields |
| NoSQL injection | Payloads like {"$ne": null} |
| Command injection | Inject OS commands in resolver fields |
| HTML/JS Injection | Test if values are reflected (XSS) |
| GraphQL injections | Break query syntax or inject nested fragments |
{
search(term: "{\"$ne\": null}") {
results
}
}| Test | Payload or Technique |
|---|---|
| Overly permissive queries | Query nested deeply to test recursive access |
| Query batching / DoS | Send multiple queries in one body |
| Aliasing abuse | Multiple aliases for the same field |
| Mutation abuse | Trigger user update, reset, or delete without auth |
| Query injection / Resolvers bypass | Inject __typename, try bypassing resolver logic |
| Rate limiting | Fuzz large query volume, see if rate limiting kicks in |
query {
a: user(id:1) { email }
b: user(id:2) { email }
c: user(id:3) { email }
}- Deep query nesting
- Recursion with aliases
- Query batching and overfetching
| Test | Details |
|---|---|
| Deep nesting | GraphQL allows recursive nesting which may overload server |
| Circular fragments | Can crash poorly configured services |
| Alias + recursion | Amplify queries for performance impact |
query {
a: user { friends { id } }
b: user { friends { id } }
...
}query {
user {
friends {
friends {
friends {
friends {
id
}
}
}
}
}
}- SQLi / NoSQLi / OS Command Injection
- SSTI / Template injection
- XSS in error fields
- Look for fields like
password,token,apiKey,stacktrace - Try leaking stack traces via malformed queries
- Introspection enabled in production
- Playground/GraphiQL open
- Verbose errors
- CORS misconfigured
- Disable introspection in production
- Implement RBAC/ABAC properly
- Enforce HTTPS and secure headers
- Disable unused mutations/subscriptions
- β Disable introspection in production
- β Enforce strict RBAC on fields and resolvers
- β Validate input types server-side (not just GraphQL types)
- π Rate-limit or throttle excessive queries
- β Set query depth and complexity limits
- β Disable playground / graphiql in prod
- β Sanitize and validate all user input
- β Use parameterized queries in resolvers
- π§Ύ Audit for over-permissive schemas
| Tool | Description |
|---|---|
| InQL | Burp plugin for introspection & fuzz |
| GraphQLmap | SQLmap-style GraphQL testing |
| Graphw00f | GraphQL fingerprinting |
| Altair | UI for testing queries |
| gql-fuzzer | Fuzzing complex queries |
| Postman | Manual query crafting |
| Graphql Visualizer | http://nathanrandal.com/graphql-visualizer/ |
| Clairvoyance | Get schema even if introspection is disabled |
[
{ "query": "{ user(id:1) { email } }" },
{ "query": "{ user(id:2) { email } }" }
]query {
a: user(id: 1) { id }
b: user(id: 1) { email }
}- Mutation returning unknown field errors
- Guess types via response structure
- Universal query -
query{__typename} - Common endpoints
- /graphql
- /api
- /api/graphql
- /graphql/api
- /graphql/graphql
- HTTP methods - POST
- Check for versioning in endpoints - /v1, /graphql/v1, ...
- Payloads - https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/GraphQL%20Injection