skip to Main Content

[0x1 GraphQL Penetration Testing ] Operations, Types and Introspection

Kmsc Website 2022 06

GraphQL is an open source data formatting and query engine for APIs. In this post series we will cover some basic concepts that sets GraphQL apart and provide some tips and tricks to help penetration testers and security consultants make sense of the security risks that may present in GraphQL endpoints.

GraphQL got its start in 2012[1] at facebook,  developers needed a way to agree with backend and middleware devs on how data should be type’d, queried and packaged in a way that was consistent and easy to make sense of. From the growing support GraphQL enjoys, from large and prominent organisations like Twitter, Shopify, Github, Pintrest among many others[5], it seems Facebook achieved that design goal. Arguably most of the goals are achieved through its simple query language and Schema Definition Language (SDL), clients are able to adapt to the demands of their environment by querying precisely what they need from the back-end without needing to hold large lists of endpoints and routes. Back-end technology benefits by letting the query language handle strict typing, and formatting needs, the simple singular endpoint means that back-ends can expand, add data sources and change infrastructure without reworking presentation layer specifics every time.

Okay thats enough marketing, lets dig into the basic structure of a GraphQL, take a look at where developers step in and provide configuration and engineering.

Data Types

GraphQL achieves most of its magic through the simple, flexible and descriptive query language. Before we can construct queries we need to talk about the elements of the language, the types that we interrogate via the query language. GraphQL has a few types,  these are said to make up the “leafs” of the graph i.e. they terminate traversal of the graph (important to remember this because of some attack vectors we will explore later) basically the bottom level definitions.

The most elementary types are referred to as scalar types [1,2,3,4], namely:

  • Int – A signed 32 bit JSON integer. An interesting note, is that given a lot of HTTP interaction is string driven, some graphQL servers may “coerce” an integer out of whatever string is returned. This may make for interesting behaviour should attackers be able to force something to be interpreted as a signed int, i.e. a hash value, memory contents or something protected.
  • String – A JSON UTF-8 String, again the spec indicates there may be behaviour unique to each server regarding type coercion.
  • Boolean – A simple type taking either the value true or false, its also acceptable according to the spec to use integer values namely  and respectively “1” or “0”.
  • ID – A unique number used to cache an object. IDs must be serialisable as a string and not intended to be human readable.
  • Float – Double Precision Integers defined in IEEE 745.

Its important to know the range of types because it has a big impact on how data validation will work as well as how complex a graph for a given API will be. The types are important to developers naturally because they will be used to create a schema for their representative APIs here’s an example of a schema that makes use of each type:

explpoit.graphql

type Exploit {
    Id: ID!
    Author: String!
    TargetAsset: Asset!
    TargetHost: String!
    Report: CVE!
    Instances: Int!
}
type Asset {
   Id: ID!
   IPAddress: String
   SourceFile: String
   FullyQualifiedDomainName: String
   URL: String
}
type CVE {
   Id: ID!
   Number: String!
   DateReported: String!
   DateModified: String!
   AffectedProduct: String!
   AffectedVersions: [String!]!
}

You may notice that there are some exclamation marks following a few of the type fields, this indicates that the filed is non-nullable, meaning that it requires a value and can never be returned as “null”. Another extension of the null wrapper type (which is the correct term, null types “wrap” other types); is that of lists, we see the following definition for CVE.AffectedVersions:

AffectedVersions: [String!]!

This means that we expect an Array type, indicated by the square brackets [], additionally its an array of type string and that each string cannot be null [!] and even further that the list itself cannot be null indicated by []!

To give one an idea of how this forms a graph lets sketch this as one, see the simplified view of this below:

Exploit:
   |___ ID
   |___ String
   |___ String
   |___ Int
   |___ Asset
         |___ ID
         |___ String
         |___ String
         |___ String
         |___ String
   |___ CVE
         |___ ID
         |___ String
         |___ String
         |___ String
         |___ [String]
                 |____ String
                        ...

Pretty straightforward, we can have types composed of other objects, or scalar types. What I’ve omitted is extended in the SDL (which i recommend you check out to get a complete picture of the typing system), namely Enum types, Union types, Input types, Return types, these are extensions of the basic concepts and allow schema designers to place restrictions on where types are used in the conversation.  I think thats enough type talk to cover the basics, we can now look at the operation types.

Operation Types

GraphQL has 3 operation types Queries, Mutations and Subscriptions[7]. These make up the “CRUD” of whatever API(s) are being represented by the GraphQL instance, you can think of them as the proverbial POST, GET, PUT, DELETE etc operations standard to traditional REST APIs. Given a lot of your testing and prodding when it comes to GraphQL will be focused on drafting queries, its important to know a little about each operation type. One may never know what functionality hides behind an operation type for a given API.

Queries

The Query type, is the root[6,8] of every GraphQL type system,  which means every single instance of graphQL must implement a query type. It gives the graph a place to start from, the fields of the root query type make up the stuff you can ask about, this is where the unique structure of the type system kicks in, after the root query its anybodies guess what the type system looks like. Its important to understand the Query type “as the root” because it allows us to form introspection queries which we will learn more about in coming sections.

A query doesn’t need much to be a valid query, lets start with a very basic one, here’s what it looks like:

{

   query AQuery{
      users {
         id
         username
      }
   }

}

Very basic,  this query doesn’t have anything in its selection set, this is the part of the query in the inner most curl brackets. We also see the query has a name “AQuery”, this string can be anything you want it to be (as is with most QL instances), it doesn’t matter too much now but pay attention to this field; any bug hunter worth their salt will tell you its a prime candidate for reflection/injection attacks, especially if a server uses it as part of its error output. You may commonly find that GraphQL endpoints only accept JSON content-type requests, this means, when actually firing this off, you would need to format this as a json object as follows:

[{"query":"query AQuery{ users { id username} }"}]

Lets fire this off at a sample GraphQL application, we will of course be using curl as per hacker etiquette because terminals are cool and complicated sandboxes drool:

keithmakan@GraphQLHunter % curl -vv -H "Content-Type: application/json" -d '[{"query":"query AQuery{ users { id username }}"}]' http://ec2-34-217-31-181.us-west-2.compute.amazonaws.com:5013/graphql
*   Trying 34.217.31.181:5013...
* Connected to ec2-34-217-31-181.us-west-2.compute.amazonaws.com (34.217.31.181) port 5013 (#0)
> POST /graphql HTTP/1.1
> Host: ec2-34-217-31-181.us-west-2.compute.amazonaws.com:5013
> User-Agent: curl/7.88.1
> Accept: */*
> Content-Type: application/json
> Content-Length: 43
> 

< HTTP/1.1 200 OK
< Content-Type: application/json
< Content-Length: 85
< Date: Wed, 26 Jul 2023 13:12:31 GMT
< 

* Connection #0 to host ec2-34-217-31-181.us-west-2.compute.amazonaws.com left intact
[{"data":{"users":[{"id":"1","username":"admin"},{"id":"2","username":"operator"}]}}]%

Something unique to GraphQL fields is that they also behave like functions, in that we can pass them arguments in order to specify which “users”, we would like to retrieve, this would look like the following:

keithmakan@GraphQLHunter % curl -vv -H "Content-Type: application/json" -d '[{"query":"query AQuery{ users(id :1) { id username }}"}]' http://ec2-34-217-31-181.us-west-2.compute.amazonaws.com:5013/graphql 
*   Trying 34.217.31.181:5013...
* Connected to ec2-34-217-31-181.us-west-2.compute.amazonaws.com (34.217.31.181) port 5013 (#0)
> POST /graphql HTTP/1.1
> Host: ec2-34-217-31-181.us-west-2.compute.amazonaws.com:5013
> User-Agent: curl/7.88.1
> Accept: */*
> Content-Type: application/json
> Content-Length: 50
> 

< HTTP/1.1 200 OK
< Content-Type: application/json
< Content-Length: 52
< Date: Wed, 26 Jul 2023 13:19:27 GMT
< 

* Connection #0 to host ec2-34-217-31-181.us-west-2.compute.amazonaws.com left intact
[{"data":{"users":[{"id":"1","username":"admin"}]}}]%                                                                                                       

Here we are asking the API to send us all the users with id 1 and to return the username and id again as before, from the response you can see that we got the admin user, typical id for the admin as I’m sure everyone would guess.  Queries can be batched, that means we can ask for more than one record to be returned in a single query, like so:

keithmakan@GraphQLHunter % curl -vv -H "Content-Type: application/json" -d '[{"query":"query AQuery{ users(id :1) { id username }, audits { id }, pastes { id }}"}]' http://ec2-34-217-31-181.us-west-2.compute.amazonaws.com:5013/graphql
*   Trying 34.217.31.181:5013...
* Connected to ec2-34-217-31-181.us-west-2.compute.amazonaws.com (34.217.31.181) port 5013 (#0)
> POST /graphql HTTP/1.1
> Host: ec2-34-217-31-181.us-west-2.compute.amazonaws.com:5013
> User-Agent: curl/7.88.1
> Accept: */*
> Content-Type: application/json
> Content-Length: 87
> 

< HTTP/1.1 200 OK
< Content-Type: application/json
< Content-Length: 184
< Date: Wed, 26 Jul 2023 13:28:34 GMT
< 
* Connection #0 to host ec2-34-217-31-181.us-west-2.compute.amazonaws.com left intact

[{"data":{"users":[{"id":"1","username":"admin"}],"audits":[{"id":"1"},{"id":"2"},{"id":"3"},{"id":"4"},{"id":"5"},{"id":"6"},{"id":"7"},{"id":"8"}],"pastes":[{"id":"2"},{"id":"1"}]}}]%   

Here we lopped on a query asking for all the audit id’s and all the pastes id’s and behold they are returned as a single record attached to the results of the admin query we sent in the previous example.

Queries can nest infinitely if not controlled for depth, they can also scale to millions of fields if not limited in this way. You should get the idea now, the query language formats and documents itself very neatly allowing you to quickly determine what is being asked for, allowing fine control of which data should be fetched in a way that can be adapted uniquely to each request and environment, there’s no need to fetch entire chunks of a database and then filter for what you actually need.

Another important detail to know about is how some GraphQL servers suggest field names when you get them wrong. Lets build a query that is deliberately wrong and see what the server does, check this out:

keithmakan@GraphQLHunter % curl -vv -H "Content-Type: application/json" -d '[{"query":"query AQuery{ uzers(id :1) { id username } }"}]' http://ec2-34-217-31-181.us-west-2.compute.amazonaws.com:5013/graphql
*   Trying 34.217.31.181:5013...
* Connected to ec2-34-217-31-181.us-west-2.compute.amazonaws.com (34.217.31.181) port 5013 (#0)
> POST /graphql HTTP/1.1
> Host: ec2-34-217-31-181.us-west-2.compute.amazonaws.com:5013
> User-Agent: curl/7.88.1
> Accept: */*
> Content-Type: application/json
> Content-Length: 58
> 

< HTTP/1.1 400 BAD REQUEST
< Content-Type: application/json
< Content-Length: 137
< Date: Wed, 26 Jul 2023 13:32:23 GMT
< 

* Connection #0 to host ec2-34-217-31-181.us-west-2.compute.amazonaws.com left intact

[{"errors":[{"message":"Cannot query field \"uzers\" on type \"Query\". Did you mean \"users\"?","locations":[{"line":1,"column":15}]}]}]%

Notice how helpful the server is by saying ‘Did you mean \”users‘, information like this will come in handy when we’re trying to figure the schema out. Until now we’ve assumed knowledge of the schema by magically knowing that there is a users field, a posts field etc. We’ll pick this topic up again in the section on introspection. Okay thats done and dusted lets talk about the more exciting operation types, mutations and subscriptions.

Mutations

Mutations are essentially the modification operations of the GraphQL operation spectrum. If you need something to change the active state of the of an API as in change, storage state typically it will be a mutation operation type that handles it. Mutations are optional[6] and typically will return some data after modification, convention has it that this should be the record  or the status of the update i.e. the time it  took, the size of the write, the endpoint it delegated it to etc etc.

Here’s what a typical mutation may look like:

'[
  {
     "query":
             "mutation exampleMutation($title: String, $content: String, $public: Boolean, $burn: Boolean) { 
                  createPaste (title: $title, content: $content, public: $public, burn: $burn) { 
                      paste {id title content} 
                  } 
              }",
     "variables":
                 {"title":"","content":"this is an example post","public":true,"burn":false}}
]'


We can see in the above mutation, an example of variable usage, where we tell GraphQL that there’s an array of “variables” which will fill in for the place holders marked with a “$”. This is a nice convention to use since it cleans up the mutation itself from being muddled with quotation escapes and input encodings, it separates the input from its format and allows the mutation to self document in a neat way.  Hitting an actual server with this we see the following behaviour:

keithmakan@GraphQLHunter % curl -vv -H "Content-Type: application/json" -d '[{"query":"mutation exampleMutation($title: String, $content: String, $public: Boolean, $burn: Boolean) { createPaste (title: $title, content: $content, public: $public, burn: $burn) { paste {id title content} } }","variables":{"title":"","content":"this is an example post","public":true,"burn":false}}]' http://ec2-34-217-31-181.us-west-2.compute.amazonaws.com:5013/graphql

*   Trying 34.217.31.181:5013...
* Connected to ec2-34-217-31-181.us-west-2.compute.amazonaws.com (34.217.31.181) port 5013 (#0)
> POST /graphql HTTP/1.1
> Host: ec2-34-217-31-181.us-west-2.compute.amazonaws.com:5013
> User-Agent: curl/7.88.1
> Accept: */*
> Content-Type: application/json
> Content-Length: 304
> 

< HTTP/1.1 200 OK
< Content-Type: application/json
< Content-Length: 95
< Date: Wed, 26 Jul 2023 14:37:52 GMT
< 

* Connection #0 to host ec2-34-217-31-181.us-west-2.compute.amazonaws.com left intact

[{"data":{"createPaste":{"paste":{"id":"13","title":"","content":"this is an example post"}}}}]% 

The selection set for a mutation defines what you want to return once the mutation is complete, this is particularly interesting because in reflection attacks we might want to take advantage of this. One should also pay careful attention to the content-type we are allowed to specify for mutation types since they do modify state and if the Content-Type header is not strictly enforced it could constitute a number of vulnerability, most obviously CSRF since we would be able to force a server to fulfil a mutation request under the form encoded content type. More on this in later posts about GraphQL.

Lets talk about subscriptions.

Subscriptions

Subscriptions a little like RSS feeds but instead they were created by Mark Zuckerberg not Aaron Schwartz (RIP). Subscriptions when initiated often open up a WebSocket connection and allow a client to listen for incoming event data. Essentially a client sends of a request to subscribe to a given event and then waits for pings from the server returning some event data.

Introspection and sniffing Fields

Introspection allows GraphQL clients to dynamically learn about the type system implemented for a given instance. Its analogous to querying the INFORMATION_SCHEMA table in a MySQL instance. Now introspection may not always be enabled but by paying attention to a subtle changes in the responses one can learn about the fields valid to a schema, I refer to this as sniffing fields.

Lets take a look at what an introspection query for the all the query types looks like:

{
    query introSpec {
        __schema {
            queryType {
               name
             }
        }
   }
}

Some new things here,  the field __schema with its weird looking underscores lets us know that this an introspection query, we are asking about the root schema. Within this field we are asking for the queryTypes and of those we want to the names of these fields.

What happens if we fire this off at an actual server? check it out:

keithmakan@GraphQLHunter % curl -vv -H "Content-Type: application/json" -d '[{"query":"query{ __schema { queryType{ fields{ name}}}}"}]' http://ec2-34-217-31-181.us-west-2.compute.amazonaws.com:5013/graphql             
*   Trying 34.217.31.181:5013...
* Connected to ec2-34-217-31-181.us-west-2.compute.amazonaws.com (34.217.31.181) port 5013 (#0)
> POST /graphql HTTP/1.1
> Host: ec2-34-217-31-181.us-west-2.compute.amazonaws.com:5013
> User-Agent: curl/7.88.1
> Accept: */*
> Content-Type: application/json
> Content-Length: 59
> 

< HTTP/1.1 200 OK
< Content-Type: application/json
< Content-Length: 302
< Date: Wed, 26 Jul 2023 14:55:12 GMT
< 

* Connection #0 to host ec2-34-217-31-181.us-west-2.compute.amazonaws.com left intact
[{"data":{"__schema":{"queryType":{"fields":[{"name":"pastes"},{"name":"paste"},{"name":"systemUpdate"},{"name":"systemDiagnostics"},{"name":"systemDebug"},{"name":"systemHealth"},{"name":"users"},{"name":"readAndBurn"},{"name":"search"},{"name":"audits"},{"name":"deleteAllPastes"},{"name":"me"}]}}}}]% 

We see here that the server responds by telling us about all the different queries we can try. So we just learned what the fields of the queryType was, which is pastes, paste, systemUpdate, systemDiagnostics, systemDebug, systemHealth, users, etc. In previous examples we’ve used these before so this example has been foreshadowed a little, but luckly in have an interesting case from a real server (not some good old Damn Vulnerable app), that actually allows us to sniff field names (essentially guess them) without the help introspection.

Lets look at what happens when we query a server with introspection turned off, here ya go:

keithmakan@GraphQLHunter % curl -vv -H "Content-Type: application/json" -d '[{"query":"{ __schema { queryType { name } }}"}]' https://xxxx/graphql 
...

* using HTTP/2
* h2h3 [:method: POST]
* h2h3 [:path: /graphql]
* h2h3 [:scheme: https]
* h2h3 [:authority: xxxx]
* h2h3 [user-agent: curl/7.88.1]
* h2h3 [accept: */*]
* h2h3 [content-type: application/json]
* h2h3 [content-length: 48]
* Using Stream ID: 1 (easy handle 0x122011400)

> POST /graphql HTTP/2
> Host: xxx
> user-agent: curl/7.88.1
> accept: */*
> content-type: application/json
> content-length: 48

> 

* We are completely uploaded and fine

< HTTP/2 400 
< date: Wed, 26 Jul 2023 15:02:38 GMT
< content-type: application/json; charset=utf-8
< content-length: 341
< x-dns-prefetch-control: off
< expect-ct: max-age=0
< strict-transport-security: max-age=15552000; includeSubDomains
< x-download-options: noopen
< x-content-type-options: nosniff
< x-permitted-cross-domain-policies: none
< referrer-policy: strict-origin-when-cross-origin
< x-xss-protection: 0
< access-control-allow-origin: *
< cache-control: no-store
< etag: W/"155-u18GRdie2iuncp2bZ4GuPkPVdjA"
< cf-cache-status: DYNAMIC
< server: cloudflare
< cf-ray: 7ecd86534ce44ed2-JNB
< alt-svc: h3=":443"; ma=86400
< 

[{"errors":[{"message":"GraphQL introspection is not allowed by Apollo Server, but the query contained __schema or __type. To enable introspection, pass introspection: true to ApolloServer in production","locations":[{"line":1,"column":3}],"extensions":{"validationErrorCode":"INTROSPECTION_DISABLED","code":"GRAPHQL_VALIDATION_FAILED"}}]}

* Connection #0 to host xxx left intact

Okay so it appears we are being naughty, by using a query that includes __schema, or __type, it looks like its no dice for us on this one. But what happens if we try and guess a field? I’m going to use the introspection query here as an example, I happen to know that the fields “kind” and “fields” are correct subs for the queryType type, but i want to know if the server does anything different when i give it an incorrect type, like say for instance i ask for “fieldas” instead of fields? Does it correct me, show me the correct introspection form even though its scolding me about using introspection? lets see:

keithmakan@Keiths-MacBook-Pro GraphQLHunter % curl -vv -H "Content-Type: application/json" -d '[{"query":"{ __schema { queryType { fields { name } }}}"}]' https://xxxx/graphql
...

> POST /graphql HTTP/2
> Host: xxxx
> user-agent: curl/7.88.1
> accept: */*
> content-type: application/json
> content-length: 58
> 

* We are completely uploaded and fine

< HTTP/2 400 
< date: Wed, 26 Jul 2023 15:27:52 GMT
< content-type: application/json; charset=utf-8
< content-length: 341
< x-dns-prefetch-control: off
< expect-ct: max-age=0
< strict-transport-security: max-age=15552000; includeSubDomains
< x-download-options: noopen
< x-content-type-options: nosniff
< x-permitted-cross-domain-policies: none
< referrer-policy: strict-origin-when-cross-origin
< x-xss-protection: 0
< access-control-allow-origin: *
< cache-control: no-store
< etag: W/"155-u18GRdie2iuncp2bZ4GuPkPVdjA"
< cf-cache-status: DYNAMIC
< server: cloudflare
< cf-ray: 7ecdab4debfc4ed5-JNB
< alt-svc: h3=":443"; ma=86400

< 

[{"errors":[{"message":"GraphQL introspection is not allowed by Apollo Server, but the query contained __schema or __type. To enable introspection, pass introspection: true to ApolloServer in production","locations":[{"line":1,"column":3}],"extensions":{"validationErrorCode":"INTROSPECTION_DISABLED","code":"GRAPHQL_VALIDATION_FAILED"}}]}

* Connection #0 to host xxxx left intact   

Okay so straight forward response, we don’t really see anything too enlighthing just yet (or idk maybe you already guessed whats going on :) but lets try and deliberately specify and erroneous field, see how the server responds then:

keithmakan@Keiths-MacBook-Pro GraphQLHunter % curl -vv -H "Content-Type: application/json" -d '[{"query":"{ __schema { queryType { fieldas { name } }}}"}]' https://xxxx/graphql

...

* We are completely uploaded and fine

< HTTP/2 400 
< date: Wed, 26 Jul 2023 15:31:38 GMT
< content-type: application/json; charset=utf-8
< content-length: 518
< x-dns-prefetch-control: off
< expect-ct: max-age=0
< strict-transport-security: max-age=15552000; includeSubDomains
< x-download-options: noopen
< x-content-type-options: nosniff
< x-permitted-cross-domain-policies: none
< referrer-policy: strict-origin-when-cross-origin
< x-xss-protection: 0
< access-control-allow-origin: *
< cache-control: no-store
< etag: W/"206-N6ArGdxk6rmS0eYVdpCnhzRFDUw"
< cf-cache-status: DYNAMIC
< server: cloudflare
< cf-ray: 7ecdb0cf0b414ebc-JNB
< alt-svc: h3=":443"; ma=86400
< 

[{"errors":[{"message":"GraphQL introspection is not allowed by Apollo Server, but the query contained __schema or __type. To enable introspection, pass introspection: true to ApolloServer in production","locations":[{"line":1,"column":3}],"extensions":{"validationErrorCode":"INTROSPECTION_DISABLED","code":"GRAPHQL_VALIDATION_FAILED"}},{"message":"Cannot query field \"fieldas\" on type \"__Type\". Did you mean \"fields\"?","locations":[{"line":1,"column":26}],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}}]}

Woaw, it actually gave us a suggestion even though it has introspection turned off, as far as im concerned this is enough info to constitute an introspection, i mean its informing us of the right query form anyway! Lets try to exploit this by try to guess a valid field name, here are a few tries i fired off:

keithmakan@GraphQLHunter % curl -H "Content-Type: application/json" -d '[{"query":"{ node(id: \"-1\"){ id }}"}]' https://xxxx/graphql 
[{"errors":[{"message":"Cannot query field \"node\" on type \"Query\".","locations":[{"line":1,"column":3}],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}}]}

keithmakan@GraphQLHunter % curl -H "Content-Type: application/json" -d '[{"query":"{ schedule(id: \"-1\"){ id }}"}]' https://xxxx/graphql
[{"errors":[{"message":"Cannot query field \"schedule\" on type \"Query\".","locations":[{"line":1,"column":3}],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}}]}

keithmakan@GraphQLHunter % curl -H "Content-Type: application/json" -d '[{"query":"{ post(id: \"-1\"){ id }}"}]' https://xxxx/graphql
[{"errors":[{"message":"Cannot query field \"post\" on type \"Query\".","locations":[{"line":1,"column":3}],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}}]}

keithmakan@GraphQLHunter % curl -H "Content-Type: application/json" -d '[{"query":"{ doc(id: \"-1\"){ id }}"}]' https://xxxx/graphql 
[{"errors":[{"message":"Cannot query field \"doc\" on type \"Query\".","locations":[{"line":1,"column":3}],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}}]}

keithmakan@GraphQLHunter % curl -H "Content-Type: application/json" -d '[{"query":"{ docu(id: \"-1\"){ id }}"}]' https://xxxx/graphql
[{"errors":[{"message":"Cannot query field \"docu\" on type \"Query\".","locations":[{"line":1,"column":3}],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}}]}

keithmakan@GraphQLHunter % curl -H "Content-Type: application/json" -d '[{"query":"{ docum(id: \"-1\"){ id }}"}]' https://xxxx/graphql
[{"errors":[{"message":"Cannot query field \"docum\" on type \"Query\". Did you mean \"document\"?","locations":[{"line":1,"column":3}],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}}]}

Boooyah, we found the document queryType, with introspection turned off! From here we can try a word list to guess more fields, shouldn’t be too hard all we need to do is build a simple bash for loop and pull in a wordlist from good ‘ol SecLists.

Here’s what my script looked like:

keithmakan@GraphQLHunter % cat try_word.sh 

#!/bin/bash
word=$1
curl -q -H "Content-Type: application/json" https://xxxx/graphql -d@- <<EOF
[
{
 "query":"query { ${word}{ id } }"
}
]
EOF


keithmakan@GraphQLHunter % for word in `cat ~/Documents/Tools/SecLists/Miscellaneous/wordlist-skipfish.fuzz.txt | grep -v "~" | grep -v "\." | sort -R`
do
echo "[*] $word";./try_word.sh $word 2> /dev/null
done | grep "Did you mean"             

[{"errors":[{"message":"Cannot query field \"sport\" on type \"Query\". Did you mean \"space\"?","locations":[{"line":1,"column":9}],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}}]}
[{"errors":[{"message":"Cannot query field \"saves\" on type \"Query\". Did you mean \"spaces\", \"space\", or \"tasks\"?","locations":[{"line":1,"column":9}],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}}]}
[{"errors":[{"message":"Cannot query field \"frames\" on type \"Query\". Did you mean \"spaces\"?","locations":[{"line":1,"column":9}],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}}]}
[{"errors":[{"message":"Cannot query field \"parse\" on type \"Query\". Did you mean \"space\" or \"spaces\"?","locations":[{"line":1,"column":9}],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}}]}
[{"errors":[{"message":"Cannot query field \"styles\" on type \"Query\". Did you mean \"spaces\"?","locations":[{"line":1,"column":9}],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}}]}
[{"errors":[{"message":"Cannot query field \"imp\" on type \"Query\". Did you mean \"me\"?","locations":[{"line":1,"column":9}],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}}]}
[{"errors":[{"message":"Cannot query field \"basic\" on type \"Query\". Did you mean \"tasks\"?","locations":[{"line":1,"column":9}],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}}]}
[{"errors":[{"message":"Cannot query field \"map\" on type \"Query\". Did you mean \"me\"?","locations":[{"line":1,"column":9}],"extensions":{"code":"GRAPHQL_VALIDATION_FAILED"}}]}

Not too shabby, we got a couple new fields! Okay i think thats it for this post, stay tuned for a follow up on some of the toolage out there on GraphQL, I’ll explain now to enumerate endpoints and perform some automated schema extraction for those of us who don’t like writing our own bash scripts ;)

References and Reading

  1. https://en.wikipedia.org/wiki/GraphQL
  2. https://spec.graphql.org/June2018/
  3. https://graphql.org/graphql-js/basic-types/
  4. https://www.digitalocean.com/community/conceptual-articles/understanding-the-graphql-type-system
  5. https://landscape.graphql.org/
  6. https://graphql.org/learn/schema/
  7. https://spec.graphql.org/draft/#sec-Language.Operations
  8. https://spec.graphql.org/draft/#sec-Root-Operation-Types
  9. https://www.apollographql.com/docs/react/data/subscriptions/

Keith is the founder of KMSecurity (Pty) Ltd. and a passionate security researcher with a storied career of helping clients all over the world become aware of the information security risks. Keith has worked for clients in Europe, the Americas and Asia and in that time gained experience assessing for clients from a plethora of industries and technologies. Keith’s experience renders him ready to tackle any application, network or organisation his clients need help with and is always eager to learn new environments. As a security researcher Keith has uncovered bugs in some prominent applications and services including Google Chrome Browser, various Google Services and components of the Mozilla web browser.

Back To Top