GraphQL
Queries are the part of the graphQL request that are followed by braces {
and can have OPTIONALLY parameters written as (parameter_name: "value")
.
Queries can be made of fields and other sub-queries.
Information Disclosure
Identifying the GraphQL Engine
As a first step, we will identify the GraphQL engine used by the web application using the tool graphw00f. Graphw00f will send various GraphQL queries, including malformed queries, and can determine the GraphQL engine by observing the backend's behavior and error messages in response to these queries.
After cloning the git repository, we can run the tool using the main.py
Python script. We will run the tool in fingerprint (-f
) and detect mode (-d
). We can provide the web application's base URL to let graphwoof attempt to find the GraphQL endpoint by itself:
. Additionally, it provides us with the corresponding detailed page in the GraphQL-Threat-Matrix, which provides more in-depth information about the identified GraphQL engine:
Lastly, by accessing the /graphql
endpoint in a web browser directly, we can see that the web application runs a graphiql interface. This enables us to provide GraphQL queries directly, which is a lot more convenient than running the queries through Burp, as we do not need to worry about breaking the JSON syntax.
Introspection
Introspection is a GraphQL feature that enables users to query the GraphQL API about the structure of the backend system. As such, users can use introspection queries to obtain all queries supported by the API schema. These introspection queries query the __schema
field.
For instance, we can identify all GraphQL types supported by the backend using the following query:
Now that we know a type, we can follow up and obtain the name of all of the type's fields with the following introspection query:
Furthermore, we can obtain all the queries supported by the backend using this query:
Knowing all supported queries helps us identify potential attack vectors that we can use to obtain sensitive information. Lastly, we can use the following "general" introspection query that dumps all information about types, fields, and queries supported by the backend:
Or everything in 1 line:
The result of this query is quite large and complex. However, we can visualize the schema using the tool GraphQL-Voyager. For this module, we will use the GraphQL Demo. However, in a real engagement, we should follow the GitHub instructions to host the tool ourselves so that we can ensure that no sensitive information leaves our system.
In the demo, we can click CHANGE SCHEMA
and select INTROSPECTION
. After pasting the result of the above introspection query in the text field and clicking on DISPLAY
, the backend's GraphQL schema is visualized for us. We can explore all supported queries, types, and fields:
Insecure Direct Object Reference (IDOR)
Identifying IDOR
To identify issues regarding broken authorization, we first need to identify potential attack points that would enable us to access data we are not authorized to access. Enumerating the web application, we can observe that the following GraphQL query is sent when we access our user profile (EXAMPLE):
As we can see, user data is queried for the username provided in the query. While the web application automatically queries the data for the user we logged in with, we should check if we can access other user's data. To do so, let us provide a different username we know exists: test
.
Note that we need to escape the double quotes inside the GraphQL query to not break the JSON syntax:
Exploiting IDOR
To demonstrate the impact of this IDOR vulnerability, we need to determine what data we can access without authorization. To do so, we are going to use the following introspection queries to determine all fields of User
type:
As we can see from the result, the User
object contains a password
field that, presumably, contains the user's password:
Let us adjust the initial GraphQL query to check if we can exploit the IDOR vulnerability to obtain another user's password by adding the password
field in the GraphQL query:
From the result, we can see that we have successfully obtained the user's password.
Injection Attacks
SQL Injection
Since GraphQL is a query language, the most common use case is fetching data from some kind of storage, typically a database. As SQL databases are one of the most predominant forms of databases, SQL injection vulnerabilities can inherently occur in GraphQL APIs that do not properly sanitize user input from arguments in the SQL queries executed by the backend.
Therefore, we should carefully investigate all GraphQL queries, check whether they support arguments, and analyze these arguments for potential SQL injections.
Using the introspection query discussed earlier and some trial-and-error, we can identify that the backend supports the following queries that require arguments:
post
user
postByAuthor
To identify if a query requires an argument, we can send the query without any arguments and analyze the response. If the backend expects an argument, the response contains an error that tells us the name of the required argument. For instance, the following error message tells us that the postByAuthor
query requires the author
argument:
After supplying the author
argument, the query is executed successfully
We can now investigate whether the author
argument is vulnerable to SQL injection. For instance, if we try a basic SQL injection payload, the query does not return any result:
Let us move on to the user
query. If we try the same payload there, the query still returns the previous result, indicating a SQL injection vulnerability:
If we simply inject a single quote, the response contains a SQL error, confirming the vulnerability:
Since the SQL query is displayed in the SQL error, we can construct a UNION-based SQL injection query to exfiltrate data from the SQL database. Remember that the database might contain data that we cannot query from the GraphQL API. As such, we should check for any sensitive data in the database that we can access.
To construct a UNION-based SQL injection payload, let us take another look at the results of the introspection query:
The vulnerable user
query returns a UserObject
, so let us focus on that object. As we can see, the object consists of six fields and a link (posts
)
The fields correspond to columns in the database table. As such, our UNION-based SQL injection payload needs to contain six columns to match the number of columns in the original query. Furthermore, the fields we specify in our GraphQL query correspond to the columns returned in the response. For instance, since the username
is a UserObject's
third field, querying for the username
will result in the third column of our UNION-based payload being reflected in the response.
As the GraphQL query only returns the first row, we will use the GROUP_CONCAT function to exfiltrate multiple rows at a time. This enables us to exfiltrate all table names in the current database with the following payload:
The response contains all table names concatenated in the username
field:
Since this is a SQL injection vulnerability just like in any other web application, we can use all SQL payloads and attack vectors to enumerate column names and finally exfiltrate data. For more details on exploiting SQL injections check SQL Injection Fundamentals
Cross-Site Scripting (XSS)
XSS vulnerabilities can occur if GraphQL responses are inserted into the HTML page without proper sanitization. Similar to the above SQL injection vulnerability, we should investigate any GraphQL arguments for potential XSS injection points. However, in this case, both queries do not return an XSS payload:
XSS vulnerabilities can also occur if invalid arguments are reflected in error messages. Let us look at the post
query, which expects an integer ID as an argument. If we instead submit a string argument containing an XSS payload, we can see that the XSS payload is reflected without proper encoding in the GraphQL error message:
However, if we attempt to trigger the URL from the corresponding GET-parameter by accessing the URL /post?id=<script>alert(1)</script>
, we can observe that the page simply breaks, and the XSS payload is not triggered.
Denial-of-Service (DoS) & Batching Attacks
Depending on the GraphQL API's configuration, we can create queries that result in exponentially large responses and require significant resources to process. This can lead to high hardware utilization on the backend system, potentially leading to a DoS scenario that limits the service's availability to other users.
Denial-of-Service (DoS) Attacks
To execute a DoS attack, we must identify a way to construct a query that results in a large response. Let's look at the visualization of the introspection results in GraphQL Voyager
. We can identify a loop between the UserObject
and PostObject
via the author
and posts
fields:
We can abuse this loop by constructing a query that queries the author of all posts. For each author, we then query the author of all posts again. If we repeat this many times, the result grows exponentially larger, potentially resulting in a DoS scenario.
Since the posts
object is a connection
, we need to specify the edges
and node
fields to obtain a reference to the corresponding Post
object. As an example, let us query the author of all posts. From there, we will query all posts by each author and then the author's username for each of these posts:
This is an infinite loop we can repeat as many times as we want. If we take a look at the result of this query, it is already quite large because the response grows exponentially larger with each iteration of the loop we query:
Making our initial query large will slow down the server significantly, potentially causing availability issues for other users. For instance, the following query crashes the GraphiQL
instance:
Batching Attacks
Batching in GraphQL refers to executing multiple queries with a single request. We can do so by directly supplying multiple queries in a JSON list in the HTTP request. For instance, we can query the ID of the user admin
and the title of the first post in a single request:
The response contains the requested information in the same structure we provided the query in:
Batching is not a security vulnerability but an intended feature that can be enabled or disabled. However, batching can lead to security issues if GraphQL queries are used for sensitive processes such as user login. Since batching enables an attacker to provide multiple GraphQL queries in a single request, it can potentially be used to conduct brute-force attacks with significantly fewer HTTP requests. This could lead to bypasses of security measures in place to prevent brute-force attacks, such as rate limits.
For instance, assume a web application uses GraphQL queries for user login. The GraphQL endpoint is protected by a rate limit, allowing only five requests per second. An attacker can brute-force user accounts with only five passwords per second. However, using GraphQL batching, an attacker can put multiple login queries into a single HTTP request. Assuming the attacker constructs an HTTP request containing 1000 different GraphQL login queries, the attacker can now brute-force user accounts with up to 5000 passwords per second, rendering the rate limit ineffective. Thus, GraphQL batching can enable powerful brute-force attacks.
Mutations
What are mutations?
Mutations are GraphQL queries that modify server data. They can be used to create new objects, update existing objects, or delete existing objects.
Let us start by identifying all mutations supported by the backend and their arguments. We will use the following introspection query:
Or in 1 line:
From the result, we can identify a mutation registerUser
, presumably allowing us to create new users. The mutation requires a RegisterUserInput
object as an input:
We can now query all fields of the RegisterUserInput
object with the following introspection query to obtain all fields that we can use in the mutation:
From the result, we can identify that we can provide the new user's username
, password
, role
, and msg
As we identified earlier, we need to provide the password as an MD5-hash. To hash our password, we can use the following command:
With the hashed password, we can now finally register a new user by running the mutation:
The result contains the fields we queried in the mutation's body so that we can check for errors:
We can now successfully log in to the application with our newly registered user.
Exploitation with Mutations
To identify potential attack vectors through mutations, we need to thoroughly examine all supported mutations and their inputs. In this case, we can provide the role
argument for newly registered users, which might enable us to create users with a different role than the default role, potentially allowing us to escalate privileges.
We have identified the roles user
and admin
from querying all existing users. Let us create a new user with the role admin
and check if this enables us to access the internal admin endpoint at /admin
. We can use the following GraphQL mutation:
In the result, we can see that the role admin
is reflected, which indicates that the attack was successful
Tools of the Trade
We have already discussed tools that can help us in the enumeration phase: graphw00f and graphql-voyager. We will now discuss further tools to help us attack GraphQL APIs.
GraphQL-Cop
We can use the tool GraphQL-Cop, a security audit tool for GraphQL APIs. After cloning the GitHub repository and installing the required dependencies, we can run the graphql-cop.py
Python script:
We can then specify the GraphQL API's URL with the -t
flag. GraphQL-Cop then executes multiple basic security configuration checks and lists all identified issues, which is a great baseline for further manual tests:
InQL
InQL is a Burp extension we can install via the BApp Store
in Burp. After a successful installation, an InQL
tab is added in Burp.
Furthermore, the extension adds GraphQL
tabs in the Proxy History and Burp Repeater that enable simple modification of the GraphQL query without having to deal with the encompassing JSON syntax:
Furthermore, we can right-click on a GraphQL request and select Extensions > InQL - GraphQL Scanner > Generate queries with InQL Scanner
:
Afterward, InQL generates introspection information. The information regarding all mutations and queries is provided in the InQL
tab for the scanned host:
This is only a basic overview of InQL's functionality. Check out the official GitHub repository for more details.
GraphQL Vulnerability Prevention
After discussing how to attack different vulnerabilities that arise from misconfigured GraphQL implementations, let's discuss mitigations to prevent these vulnerabilities.
Vulnerability Prevention
Information Disclosure
General security best practices apply to prevent information disclosure vulnerabilities. These include preventing verbose error messages and instead displaying generic error messages. Furthermore, introspection queries are potent tools for obtaining information. As such, they should be disabled if possible. At the very least, whether any sensitive information is disclosed in introspection queries should be checked. If this is the case, all sensitive information needs to be removed.
Injection Attacks
Proper input validation checks need to be implemented to prevent any injection-type attacks such as SQL injection, command injection, or XSS. Any data the user supplies should be treated as untrusted before appropriate sanitization. The use of allowlists should be preferred over denylists.
Denial-of-Service (DoS)
As discussed, DoS attacks and the amplification of brute-force attacks through batching are common GraphQL attack vectors. Proper limits need to be implemented to mitigate these types of attacks. This can include limits to the GraphQL query depth, limits to the maximum GraphQL query size, and rate limits on the GraphQL endpoint to prevent many subsequent queries in quick succession. Additionally, batching should be turned off in GraphQL queries if possible. If batching is required, the query depth needs to be limited.
API Design
General API security best practices should be followed to prevent further attacks, such as attacks against improper access control (for instance, IDOR) or attacks resulting from improper authorization checks on mutations. This includes strict access control measures according to the principle of least privileges. In particular, the GraphQL endpoint should only be accessible after successful authentication, if possible, according to the API's use case. Furthermore, authorization checks must be implemented; preventing actors from executing queries or mutations they are not authorized to.
For more details on securing GraphQL APIs, check out OWASP's GraphQL Cheat Sheet.
Last updated