September 25, 2026
Complete Guide on GraphQL Testing — Part II
If you’ve not already gone through the previous article that goes through the basics in detail, I would highly recommend going through it…

By Rahul Singh Chauhan
5 min read
If you've not already gone through the previous article that goes through the basics in detail, I would highly recommend going through it here.
GraphQL Schema
Before looking at GraphQL exploitation techniques, it is important to understand what a GraphQL schema is.
A GraphQL schema defines the structure of data and operations that a GraphQL API supports. It acts as a contract between the client and the server, describing what can be queried, what can be modified, what arguments can be supplied, and what data types can be returned.
Unlike REST APIs, where different URLs commonly represent different resources, GraphQL typically exposes a single endpoint such as: /graphql
The schema then defines the operations that are available through this endpoint.
For example, a simple schema might contain:
type Query {
users: [User]
user(id: ID!): User
}
type Mutation {
createUser(name: String!, email: String!): User
}type Query {
users: [User]
user(id: ID!): User
}
type Mutation {
createUser(name: String!, email: String!): User
}From this schema, we can determine several things.
The Query type defines operations used to retrieve data. In this example, the API supports users and user.
The Mutation type defines operations that can modify data. Here, the API exposes a createUser operation.
For example, the following request would retrieve a user's details:
query {
user(id: "123") {
id
name
email
}
}query {
user(id: "123") {
id
name
email
}
}Attacks
Discovering the GraphQL Engine
We can catch some easy bugs, CVEs by first discovering the GraphQL engine. Just like websites can hosted using a web server, graphQL is served through a GraphQL Engine. So, if we are able to discover the underlying engine, it would become easy for us to search for existing CVEs or any other behavior that can be exploited later. We'll learn about one such technique later that can help you potentially bypass OTP restrictions, bruteforce using a single request and cause denial of service.
You can discover the GraphQL engine by querying the GraphQL engine in a way that errors are triggered. This will help you get the GraphQL engine. In almost all of the assessments, I've seen companies use Apollo GraphQL as the GraphQL Engine.
You can also automate this step by using InQL. InQL is a burp extension that can help you automate GraphQL testing.
As visible from the screenshot, this GraphQL engine being used is graphene.
As evident from the screenshot, this engine has no support for query depth limit, which means we can query upto a very large depth using a query such as below to achieve denial of service. An example on how to exploit this is given below under Circular Recursion.
Discovering the Schema
Now that we know what attacks can be tried on our GraphQL endpoints, we can move to discover the schema. There are two common ways to discover the schema.
- Introspection
- Suggestions
While not all GraphQL engines support suggestions, most of them have introspection enabled by default.
Using did you mean ... Suggestions
GraphQL implementations often provide helpful error messages when a client references a field, type, or operation that does not exist. For example, sending:
query {
user {
id
}
}query {
user {
id
}
}
The suggestion is intended to improve developer experience, but from a penetration tester's perspective it can become a schema discovery mechanism. This error tells the user that user doesn't exist but users does. So, now we know the name of the query. Easy isn't it?
Introspection Query
GraphQL has another, much more powerful schema-discovery mechanism: introspection.
Manual Approach
An introspection query can ask the GraphQL server about its own schema. For example, a simple query can enumerate the fields available on the root Query type:
{"query": "query IntrospectionQuery{__schema{queryType{name}mutationType{name}subscriptionType{name}types{...FullType}directives{name description locations args{...InputValue}}}}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 ofType{kind name}}}}}}}}"}{"query": "query IntrospectionQuery{__schema{queryType{name}mutationType{name}subscriptionType{name}types{...FullType}directives{name description locations args{...InputValue}}}}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 ofType{kind name}}}}}}}}"}
But wait a minute, the response looks pretty overwhelming. How do I make sense out of it?
There are multiple tools that can help you achieve this. One such tool is Voyager. It runs on the client side, so, you don't even have to worry about your data reaching someone's server.
For example, pasting the above response on Voyager, gives us a clean UI providing us with the queries supported by the server.
The entries under Query are the operations supported by the server. Let's take a moment to understand.
We can now use this to create our own query. For example, the pastes query acceptsPasteObject as it's fields. So, within pastes we can request for id, title, content, public, userAgent, etc. as output.
Automated Approach
But this can be overwhelming provided that you are working on an enterprise endpoint. So, instead what we can do is use InQL.
InQL can help you get the all the queries and mutations accepted by the endpoint (provided that introspection is enabled) . All you need is a endpoint and the request headers as provided below (Note that the request is a post request but I've kept the body empty intentionally).
Click the Analyse button when all is configured.
Note that InQL returns all the queries, mutation operations supported by the endpoint.
I've been able to discover admin endpoints using this approach which did not have proper authorization in place. So, as a lower privilege user I was able to perform queries which were only restricted to admin level users.
Circular Recursion
If the schema contains circular relationships and the server does not enforce sufficient query-depth or complexity limits, an attacker can construct a deeply nested query that repeatedly traverses the same relationships.
For example, if a User has friends and each friend is also a User, an attacker could request:
{
user {
friends {
friends {
friends {
friends {
# ...and so on
}
}
}
}
}
}{
user {
friends {
friends {
friends {
friends {
# ...and so on
}
}
}
}
}
}So, if the GraphQL engine does not restrict the depth, one can query this infinitely and cause a denial of service.
Can you spot the same using the output in Voyager above? If not, InQL can also help you achieve this. Just tap on Cyclic Detection and it'll provide you with the queries that are affected by it.
More advanced attacks in the upcoming articles.
Hope you enjoyed reading the article. Please consider subscribing and clapping for the article.
In case you are interested in CTF/THM/HTB writeups consider visiting my YouTube channel.