January 28, 2025
How you can secure your GraphQL API against common vulnerabilities, and how to approach testing GraphQL as a hacker
Ahmed Qaramany
GraphQL is a query language and runtime for APIs, designed to give clients precise control over the data they request from a server. Unlike SQL (which interacts directly with databases), GraphQL operates as an API layer between clients and backend services (e.g., databases, REST APIs, microservices). It allows clients to define exactly what data they need, reducing over-fetching or under-fetching of information common in REST APIs.
GraphQL acts as an API that allows data transfer between a client and a server, serving as an alternative to REST APIs. Its main benefit is that it can fetch all required data in a single request, allowing the server to structure the response. It helps clients such as mobile apps and websites request only the specific data they need instead of receiving fixed responses like in REST APIs. This makes responses smaller and faster.
GraphQL Key Components
Both GraphQL and REST API are ways to get and send data from a server, but they work differently.
Imagine you’re working on a project, and you need to fetch information about a user and their posts. You want it to be fast, clean, and efficient. Do you go with the traditional REST API or the flexible GraphQL? Let’s explore both options and understand how they stack up against each other.
You’re building a user profile page. For it to work, you need:The user’s basic details (like name and email).
With a REST API, this might mean multiple requests to different endpoints. But with GraphQL, a single query can fetch everything you need. Let’s see how this plays out.
With a REST API, this might mean multiple requests to different endpoints. But with GraphQL, a single query can fetch everything you need. Let’s see how this plays out.
REST API: The Traditional Approach
In the REST world, APIs are built around endpoints, each representing a resource. To get user details and their posts, you’d typically make two separate requests:
We'll send the first request which is fetching user details
Endpoint: GET /users/1
HTTP Response:
{
"id": 1,
"name": "John",
"email": "[email protected]"
}
Then we'll send another request Fetch User’s Posts
Endpoint: GET /users/1/posts
[
{ "id": 101, "title": "First Post" },
{ "id": 102, "title": "Second Post" }
]
While this approach works, notice the overhead: multiple requests, more network traffic, and potential delays.
Response:
Let's now take a look at GraphQL, The Flexible Solution
GraphQL offers a different approach. Instead of multiple requests, you can craft a single query that gets exactly what you need. Here’s how it looks:
GraphQL Query:
{
user(id: 1) {
name
email
posts {
title
}
}
}
GraphQL Response:
{
"user": {
"name": "John",
"email": "[email protected]",
"posts": [
{ "title": "First Post" },
{ "title": "Second Post" }
]
}
}
As you can see, tith GraphQL, the client controls what data is fetched, resulting in a more efficient and streamlined experience.
To fully understand GraphQL, let’s start with the basics and build upward. By the end of this section, you’ll have a clear picture of how GraphQL works and why it’s so powerful.
1. Scalar Types: The Building Blocks of Data
At its core, GraphQL works with scalar types, which represent the most basic pieces of data. These are the types of individual fields in a GraphQL schema.
Common Scalar Types:
3.14
)."John Doe"
).true
, false
).Example
type User {
id: ID
name: String
age: Int
isActive: Boolean
}
Here, every field in the User type is a scalar type.
2. Object Types: Combining Scalars
An object type is a collection of related fields. It’s the backbone of GraphQL schemas, representing real-world entities.
Example:
type User {
id: ID
name: String
email: String
posts: [Post]
}
In this example:
User
type combines scalar fields (id
, name
, email
) with a relationship (posts
).[Post]
means the posts
field returns a list of Post
objects.
3. Schema: The Blueprint of the API
The schema is where everything comes together. It defines the structure of your API: the types, fields, and their relationships.
Example Schema:
type User {
id: ID
name: String
email: String
posts: [Post]
}
type Post {
id: ID
title: String
content: String
author: User
}
The schema shows that:
User
has multiple posts.Post
has an author
of type User
.
4. Queries: Fetching Data
A query is how you request data from a GraphQL API. It’s flexible—you can specify exactly what you need, down to the field level.
Example query
{
user(id: 1) {
name
email
posts {
title
}
}
}
Response:
{
"user": {
"name": "John",
"email": "[email protected]",
"posts": [
{ "title": "First Post" },
{ "title": "Second Post" }
]
}
}
As you can see, In GraphQL, the client has full control over what data to request. Unlike REST APIs, where the server dictates the structure of the response, GraphQL allows you to specify the exact fields you want in your query. This eliminates over-fetching (getting unnecessary data) and under-fetching (not getting all the data you need).
5. Mutations: Modifying Data
While queries fetch data, mutations are used to modify it. You can create, update, or delete records using mutations.
Example Mutation:
mutation {
createPost(title: "New Post", content: "This is a new post", authorId: 1) {
id
title
}
}
Response:
{
"createPost": {
"id": "101",
"title": "New Post"
}
}
Mutations often take input parameters (e.g., title
, content
) and return the modified data.
6. Resolvers: Connecting to the Database
Resolvers are the functions that fulfill queries and mutations. They act as a bridge between the GraphQL schema and the actual data source, like a database or an API.
Example Resolver in JavaScript:
const resolvers = {
Query: {
user: (parent, args, context) => {
return database.getUserById(args.id);
}
},
Mutation: {
createPost: (parent, args, context) => {
return database.createPost(args);
}
}
};
Resolvers:
id
, title
).7. Relationships: Linking Types
GraphQL allows types to reference each other, creating relationships.
type User {
id: ID
name: String
posts: [Post]
}
type Post {
id: ID
title: String
author: User
}
In this schema:
User
has multiple posts
.Post
has an author
of type User
.This makes it easy to navigate between related data in queries:
{
user(id: 1) {
name
posts {
title
author {
name
}
}
}
}
8. Fragments: Reusing Query Parts
When you need the same fields in multiple queries, fragments save you from repeating yourself.
fragment UserDetails on User {
name
email
}
query {
user(id: 1) {
...UserDetails
posts {
title
}
}
}
Fragments make queries cleaner and easier to maintain.
Now, we’ll start exploring the methodology you can follow for testing GraphQL security. We’ll also share useful tips and tricks for hunting vulnerabilities and Pentesting GraphQL. We’ll cover every thing but we'll try to cover the most common and effective techniques, and at the end of this article, we’ll provide additional resources.
What is GraphQL Introspection?
GraphQL introspection is a powerful feature of the GraphQL specification that allows clients to query the schema itself. It provides metadata about the API, including all available types, queries, mutations, and their relationships. This functionality is similar to how REST APIs often use documentation tools like Swagger or Postman to expose API details, but with GraphQL, it’s baked into the protocol itself.
Introspection is accessed by sending a special query to the GraphQL server, often using the __schema
and __type
system fields. For example, you can query for all the available types or explore the details of a specific type.
Example of an Introspection Query:
{
__schema {
types {
name
fields {
name
}
}
}
}
Response Example:
{
"data": {
"__schema": {
"types": [
{
"name": "User",
"fields": [
{ "name": "id" },
{ "name": "name" },
{ "name": "email" }
]
},
{
"name": "Post",
"fields": [
{ "name": "id" },
{ "name": "title" },
{ "name": "content" }
]
}
]
}
}
}
This response reveals the structure of the API, showing entities like User and Post and their respective fields.
Mapping the API Surface Introspection acts as a discovery tool, allowing you to map the entire attack surface of the GraphQL API. By querying the schema, you can uncover:
This makes it easier to understand the API’s structure, locate sensitive operations, and identify potential vulnerabilities.
Hint: Introspection queries may sometimes be disabled in the production environment. To work around this, try locating the target's staging or development environment and run the query there. It's more likely to work in those environments.
Information Gathering
To find exposed GraphQL instances, it is a good idea to include certain paths when performing a directory brute force attack. These paths help locate GraphQL endpoints that may be publicly accessible. Some common GraphQL paths to check include:
/graphql
/graphiql
/graphql.php
/graphql/console
/api
/api/graphql
/graphql/api
/graphql/graphql
/v1/graphql
/v2/graphql
/graphql/v1
/graphql/v2
/gql
/graphql-playground
/playground
/altair
/query
/graphql/query
/graphql-explorer
/api/v1/graphql
/api/v2/graphql
/public/graphql
/private/graphql
/internal/graphql
graphw00f is a tool for GraphQL fingerprinting during recon. It helps identify the GraphQL engine, check security defenses, and detect misconfigurations by sending different queries. This allows penetration testers to find weaknesses and plan better attacks efficiently.
suppose you are testing a target website with a GraphQL API at https://target.com/graphql
. You want to identify its GraphQL engine and check for security weaknesses.
graphw00f -t https://target.com/graphql
Example for the output:
[*] Checking if GraphQL is available at https://target.com/graphql...
[*] Found GraphQL...
[*] Attempting to fingerprint...
[*] Discovered GraphQL Engine: (HyperGraphQL)
[!] Attack Surface Matrix: https://github.com/dolevf/graphw00f/blob/main/docs/hypergraphql.md
[!] Technologies: Java
[!] Homepage: https://www.target.com
[*] Completed.
Basic Introspection Query
To list all the types available in the schema:
query {
__schema {
types {
name
fields {
name
}
}
}
}
This query fetches:
types
: All types (including queries, mutations, and objects).name
: Name of each type.fields
: The fields each type contains.This means the schema contains:
Query
type with getUser
and getPosts
fields.User
type with id
, name
, and email
fields.Using this query we can dump the GraphQL Schema:
{
__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
}
}
}
}
}
}
}
}
If GraphQL Introspection is enabled in production, it significantly helps attackers in the following ways:
Schema Enumeration & API Mapping
we can use some powerful tools to dump the schema and deals with its queries and mutations
InQL is a tool that integrates with Burp Suite to help test GraphQL APIs. It provides functionalities similar to Postman.
When GraphQL Introspection Query is enabled, it allows you to explore the structure of the GraphQL API, including available types, queries, and mutations.
This is where InQL becomes useful, it automatically retrieves and analyzes this information, making it easier to discover endpoints, understand API behavior, and test queries. As a result, InQL is valuable for both security testing and API exploration.
GraphQL Voyager is a visualization tool that helps you explore GraphQL APIs by generating an interactive graphical representation of the API schema.
When GraphQL Introspection Query is enabled, it allows you to inspect the structure of the GraphQL API, including available types, queries, and mutations.
This is where GraphQL Voyager becomes useful—it automatically fetches and visualizes this information, making it easier to understand relationships between data, explore endpoints, and analyze API structure. As a result, GraphQL Voyager is a valuable tool for API exploration, documentation, and security testing.
Now that you have the complete schema, including queries and mutations, you can test them like any other API for potential vulnerabilities. We will explore this further in the upcoming sections.
This is not the end. As we discussed, the Introspection Query allows us to retrieve the GraphQL schema, see all available queries and mutations, and test them easily. However, if introspection is disabled, there are other ways to bypass this restriction.
Some GraphQL security filters block requests containing "__schema"
or "__type"
using strict regex matching. However, these filters may not handle slight modifications properly.
How to Bypass:
{ "query": "query{ __schema { queryType { name } } }" }
or
{ "query": "query{\n__schema { queryType{name}}}" }
Many GraphQL APIs are designed to accept POST requests by default. However, some APIs also support GET requests. If a security mechanism only blocks introspection for POST requests, switching to GET might work.
How to Bypass:
https://target.com/graphql?query={__schema{queryType{name}}}
Some GraphQL implementations return detailed error messages when you send an invalid query, which can help discover available fields, types, and mutations.
How to Bypass:
{ "query": "query { randomField }" }
"randomField"
does not exist, but "userProfile"
does, this helps reveal part of the schema.Some GraphQL APIs have auto-suggestions enabled in their responses. This means if you send an invalid field, the API might return a list of similar or valid fields.
How to Exploit:
There are two ways to do this:
What is Clairvoyance?
Command Example:
clairvoyance <URL/graphql> -w <dictionaryFile> -o Clairvoyance_Schema_output.json
If you prefer a manual approach, you can analyze responses and tweak queries in Burp Suite to extract field suggestions.
{ "query": "{ user { id, name } }" }
user
, but they are not publicly documented.Even if introspection is disabled, the frontend still needs to send GraphQL queries. You can capture these requests.
How to Bypass:
We have discussed the two possible cases when working with GraphQL applications, whether introspection is enabled or disabled. In the next sections, we will explore common bugs and vulnerabilities that can be tested.
GraphQL allows clients to send multiple queries in a single request. To differentiate responses, GraphQL provides aliases, which let users rename their queries. This can be exploited to bypass rate limits if the system enforces limits based on query names rather than the number of actual queries executed.
Example
query {
getUser(id: "123")
}
query {
user1: getUser(id: "123")
user2: getUser(id: "123")
user3: getUser(id: "123")
user4: getUser(id: "123")
user5: getUser(id: "123")
user6: getUser(id: "123")
user7: getUser(id: "123")
user8: getUser(id: "123")
}
The aliases are the labels (user1, user2, user3, etc.)
before each getUser query.
This technique highlights a common rate-limiting bypass in GraphQL APIs due to alias exploitation.
Mitigation:
GraphQL allows users to request detailed and complex data, which is useful for applications but can also be exploited by attackers to overload the server. Some common ways attackers can abuse GraphQL to cause disruptions include:
A Denial of Service (DoS) attack happens when an attacker floods a website, server, or network with excessive requests, making it slow or completely unavailable for real users.
Attackers can create highly complex queries with multiple nested layers, forcing the server to use excessive CPU and memory, which can cause it to slow down or crash.
Example of a Deeply Nested Query Attack:
query {
user {
friends {
friends {
friends {
friends {
id
}
}
}
}
}
}
This query repeatedly requests "friends of friends," which can overwhelm the server.
For example:
in a social media app with three users:
User ID Friends
1 ---> 2, 3
2 ---> 1, 3
3 ---> 1, 2
If this query is processed:
The query keeps running in an endless loop, consuming server resources.
Mitigation:
Attackers can request massive amounts of data in a single query. Asking for thousands of records at once can overload the server's memory and processing power.
Example of a Large List Attack:
query {
users(first: 10000) {
id
name
email
}
}
This query attempts to fetch 10,000 users in one request, which can cause delays or server crashes.
Attackers can repeat the same field multiple times in a query. Even if the fields are simple, excessive repetition can overload the server.
Example of a Field Duplication Attack:
query {
user {
id
id
id
id
# Repeated thousands of times...
}
}
This forces the server to process the same field repeatedly, wasting resources.
GraphQL allows aliases to rename fields in the response. Attackers can use aliases to request the same field multiple times under different names, forcing the server to process the same data repeatedly.
Example of an Alias Abuse Attack:
query {
user1: user(id: "1") { name }
user2: user(id: "1") { name }
user3: user(id: "1") { name }
# Repeated thousands of times...
}
This query requests the same user data multiple times using different aliases, consuming server resources.
Attackers can send multiple queries in a single request, overwhelming the server by making it process too many operations at once.
Example of a Batch Query Attack:
query {
first: user(id: "1") { name }
second: user(id: "2") { name }
third: user(id: "3") { name }
# Repeated hundreds or thousands of times...
}
This overloads the server by sending many queries in one request.
Alias Abuse:
Batch Queries:
Key Difference:
GraphQL’s introspection feature allows clients to explore the schema. Attackers can exploit this by repeatedly querying the schema, consuming server resources.
Example of Introspection Abuse:
query {
__schema {
types {
name
fields {
name
}
}
}
}
Repeatedly running this query can slow down the server and expose sensitive API details.
Information disclosure occurs when an attacker gains access to sensitive data or system details that should not be exposed. In GraphQL, this can happen in several ways, especially if the API is not properly secured. Below are the key areas to focus on during pentesting:
Attackers can send queries with multiple fields to guess the structure of the schema. By analyzing the responses, they can piece together the schema.
Example:
query {
__typename
user {
id
name
email
posts {
title
content
}
}
product {
id
name
price
}
}
If the server responds with errors or partial data, the attacker can infer the schema structure.
Attackers can guess common field names (e.g., id, name, email) and send queries to see if they exist.
Example:
query {
user {
id
username
password
}
}
If the server returns data for password
, it indicates a serious information disclosure vulnerability.
When a GraphQL server is misconfigured, it may return detailed error messages that reveal sensitive information, such as stack traces, database queries, or server paths.
Sending Malformed Queries
Attackers can send intentionally malformed queries to trigger errors and extract information. Malformed queries are incorrectly structured or invalid GraphQL queries that do not adhere to the GraphQL syntax or schema rules.
Example of a Malformed Query:
query {
user(id: "1" {
name
}
}
Response:
{
"errors": [
{
"message": "Syntax Error: Expected Name, found {",
"locations": [{ "line": 2, "column": 15 }],
"stack": "Error: Syntax Error\n at GraphQLParser.parse (...)"
}
]
}
If the server returns stack traces or internal details, it can help attackers understand the backend implementation.
Examples of Malformed Queries
query {
user(id: "1" { # Missing closing parenthesis
name
}
}
query {
user(id: "1") {
fullName # Field "fullName" does not exist in the schema
}
}
query {
user(id: 1) { # ID should be a string, not a number
name
}
}
query {
user(id: "1") {
name
posts { # Missing fields inside "posts"
}
}
query {
user(id: "1") {
...userFields # Fragment "userFields" is not defined
}
}
Some GraphQL servers enable query tracing, which adds performance metrics (e.g., execution time) to responses. Attackers can use this information to infer server behavior or identify bottlenecks.
Example of Query Tracing in Response:
{
"data": {
"user": {
"name": "John Doe"
}
},
"extensions": {
"tracing": {
"execution": {
"resolvers": [
{
"path": ["user"],
"duration": 120
}
]
}
}
}
}
If tracing is enabled, attackers can analyze the extensions
field to gather insights into the server’s performance and structure.
The extensions
field is an optional part of a GraphQL response that provides additional metadata about the query execution.
Examples for extensions
Field
"extensions": {
"executionTime": "120ms",
"queryComplexity": 50
}
"extensions": {
"warnings": ["Field 'fullName' is deprecated, use 'name' instead"]
}
"extensions": {
"apiVersion": "1.0",
"cache": "HIT"
}
"extensions": {
"deprecations": [
{ "field": "User.email", "reason": "Use 'contactEmail' instead" }
]
}
GraphQL queries are typically sent over POST requests, but some servers also support GET requests. If sensitive data (e.g., Personally Identifiable Information or PII) is exposed over GET requests, it can be logged in server logs, browser history, or proxies.
Example of a GET Request with PII:
https://example.com/graphql?query={user(id:"1"){name,email,address}}
If the server responds with sensitive data, it can be easily intercepted or leaked.
Many GraphQL servers come with built-in development tools like GraphiQL or GraphQL Playground. If these tools are left enabled in production, attackers can use them to explore the schema, run queries, and extract sensitive data.
Example:
https://example.com/graphql
in a browser may reveal an interactive GraphQL IDE.Some GraphQL APIs have default queries or mutations that return sensitive information. Attackers can exploit these to extract data without needing to understand the full schema.
Example:
query {
allUsers {
id
name
email
passwordHash
}
}
If such a query exists and is accessible, it can lead to massive data leaks.
Authentication and authorization testing for GraphQL APIs is a critical part of pentesting because misconfigurations in these areas can lead to unauthorized access, data breaches, or privilege escalation. Below is a breakdown of key points, along with explanations and examples.
The first step is to check if the API enforces authentication. Attackers can try accessing the API without providing any authentication tokens or credentials.
Example Query Without Authentication:
query {
user(id: "1") {
id
name
email
}
}
Expected Behavior:
Unauthorized
or Forbidden
, authentication is working correctly.Even if authentication is enforced, attackers may try to access restricted fields by exploiting alternate paths or nested fields.
adminPanel
, password
, email
).Example Query to Access Restricted Fields:
query {
user(id: "1") {
id
name
email
passwordHash
}
}
Expected Behavior:
passwordHash
, it indicates an authorization flaw.Access Denied
, authorization is working correctly.GraphQL APIs typically use POST requests, but some servers also support GET requests. Attackers can test if the API behaves differently with these methods, potentially bypassing security controls.
Example GET Request:
https://example.com/graphql?query={user(id:"1"){name,email}}
Example POST Request:
{
"query": "{ user(id: \"1\") { name email } }"
}
Expected Behavior:
Attackers can try to brute-force mutations or queries that accept secrets (e.g., tokens, passwords) using various techniques.
Aliases allow attackers to send multiple queries in a single request, which can be used to brute-force secrets.
Example:
query {
attempt1: login(username: "admin", password: "password1") { token }
attempt2: login(username: "admin", password: "password2") { token }
attempt3: login(username: "admin", password: "password3") { token }
}
Expected Behavior:
Attackers can send an array of queries in a single request to brute-force secrets.
Example:
[
{ "query": "mutation { login(username: \"admin\", password: \"password1\") { token } }" },
{ "query": "mutation { login(username: \"admin\", password: \"password2\") { token } }" }
]
Expected Behavior:
Cross-Site Request Forgery (CSRF) is a web security vulnerability that forces an authenticated user to perform unintended actions on a web application. While GraphQL APIs primarily rely on JSON-based requests, they can still be vulnerable to CSRF attacks under certain conditions, especially when misconfigured.
Unlike traditional REST APIs, which use explicit endpoints, GraphQL operates on a single endpoint (/graphql
) that processes various queries and mutations. This behavior can introduce CSRF risks if the API:
application/x-www-form-urlencoded
or text/plain
content types.Many developers assume that GraphQL is inherently protected from CSRF because it commonly uses JSON (application/json
), which cannot be sent via HTML forms. However, bypasses are possible under the following conditions:
Exploiting x-www-form-urlencoded
Requests
Some GraphQL implementations allow requests to be sent using application/x-www-form-urlencoded
, which an attacker can abuse by crafting a malicious form submission.
x-www-form-urlencoded
, the attacker can encode the GraphQL mutation into a URL-encoded format and submit it via an HTML form.You can test it via:
Content-Type
to application/x-www-form-urlencoded
and reformatting the body.To prevent CSRF attacks against GraphQL APIs, developers should implement multiple layers of security:
application/json
and rejects x-www-form-urlencoded
or text/plain
.SameSite=Strict
or SameSite=Lax
to prevent cookies from being sent in cross-origin requests.Access-Control-Allow-Origin: *
.X-Frame-Options: DENY
or CSP policies to prevent clickjacking-based CSRF.This now all about testing GraphQL, and there’s still more to learn about testing GraphQL. But this is a good starting point. You can now try testing GraphQL in real situations and keep learning more as you go.