Join our newsletter

What Makes Defending GraphQL APIs Challenging to Security Engineers

What Makes Defending GraphQL APIs Challenging to Security Engineers

Dolev Farhi·

While GraphQL has been around for at least seven years since the first reference implementation graphql-js was released to the public in July 2015, many security professionals are not yet caught up with it. It’s not enough to know how GraphQL works and how it can be attacked, what also matters a lot is whether security professionals have the necessary tools to identify suspicious queries, exploitation attempts, and solutions to protect against GraphQL-tailored attacks.

Whether you have a Web Application Firewall (Cloudflare, Modsecurity, or other) monitoring your application for security events, or you’ve built an in-house monitoring solution based on standard access logs to detect suspicious behavior in web applications, you have probably developed some dependency on key indicators such as:

  • HTTP methods (verbs)
  • HTTP response status codes
  • Sensitive API Routes
  • API parameters

These indicators are helpful when you want to monitor for specific events, such as:

  1. Clients hammering your registration endpoint
  2. Clients attempting to login unsuccessfully multiple times
  3. Clients attempting to perform account enumeration
  4. Clients tampering with parameters of interest

Monitoring for such events in REST APIs is pretty trivial and requires a few things to be in place: it requires the protected application to follow RFC2616 when it comes to the returned HTTP status codes so the signals make sense to clients as well as the security engineering team, and it requires the security team to have a pretty good idea (and an inventory) of their APIs.

Traditional monitoring systems use HTTP components for monitoring. Here is an example of a pseudo-monitoring rule that could be beneficial to the security team in particular:

alert if (http.method=GET and response.status_code=403 and http.uri=”/v1/user/<id>/profile”) 

This example rule relies on the HTTP method (GET), a response code (403 Forbidden) and a specific route (/v1/user/id/profile), which will mostly be available in the case of REST APIs, but what about GraphQL?

Inigo Observeability.png

GraphQL uses a single GraphQL route (/graphql is pretty common). The reason routes don’t matter in GraphQL APIs is because client intentions are all based on the query payload. While it’s true GET and POST are in use in GraphQL APIs, it does not reflect what the client is ultimately trying to do, this is all derived from the query.

Going back to the case of monitoring APIs for security events, this design of GraphQL renders traditional security monitoring approaches pretty useless. Monitoring rules as shown above will no longer work, and different tools will be required to achieve the objective.

Not only is there an issue with the single route, GraphQL APIs don’t depend on normal HTTP status codes. It’s often the case that a GraphQL API will return a 200 OK to any query, while any errors will be reflected by the response payload coming back from the server using a dedicated errors JSON key with the error message relevant to what went wrong.

For instance, take a look at the example query below that allows a user to fetch their own user details by passing a numerical user ID:

query {
   user(id: 1000) {
      email
      name
      address
  }
}

If the calling client has authorization to view user ID 1000 and that ID exists, then such query would result in a response such as:

{
  "data": {
    "user": [
      {
        "email": "[email protected]",
        "name": "test user",
        "address": "123 Hollywood Blvd",
      }
    ]
 }
}

If the calling client does not have the right authorization level, a GraphQL response might look like the following:

{
  "errors": [
    "message": “You are not authorized to view this user.”
 ]
}

If the user ID does not exist on the system, a GraphQL response might look like the following:

{
  "errors": [
    "message": “User with ID 1000 does not exist”
 ]
}

The HTTP code to all three conditions could be a consistent 200 OK from the GraphQL server, which is why GraphQL is more challenging to monitor than traditional REST APIs. If your monitoring solutions are based on standard HTTP access logs, be prepared to see a lot of log lines that look like the following:

1.2.3.4 - - [02/Aug/2022:18:27:46 +0000] "POST /graphql HTTP/1.1" 200 - "-"
"curl/7.43.0" - 539
1.2.3.4 - - [02/Aug/2022:18:27:46 +0000] "POST /graphql HTTP/1.1" 200 - "-"
"curl/7.43.0" - 539
1.2.3.4 - - [02/Aug/2022:18:27:46 +0000] "POST /graphql HTTP/1.1" 200 - "-"
"curl/7.43.0" - 539

You can’t do a whole lot with this. For monitoring security events, you will need a view into the query payload, as well as the response payload, and a solution that can contextualize these events for you. To make matters worse, GraphQL allows batching multiple queries into a single HTTP request, therefore allowing threat actors to hide multiple actions in a single HTTP packet:

query {
  alias1000: user(id: 1000) {
      email
      name
      address
  }
  alias1001: user(id: 1001) {
      email
      name
      address
  }
  alias1002: user(id: 1002) {
      email
      name
      address
  }
}

When such a query is made, you will only see a single incoming HTTP request, despite the fact that 3 queries were made.

If you thought it stops here, we’ve got some news for you: it is also possible to batch queries using an array against some GraphQL servers that support it, like so:

import requests

queries = [
  {
    "query":"query { user(id: 1000) { email name address }}",
    "query":"query { user(id: 1001) { email name address }}",
    "query":"query { user(id: 1002) { email name address }}"
  }
]

r = requests.post('http://test.inigo.local/graphql', json=queries)

print(r.json())

Inigo can help provide you with an observability layer tailored to GraphQL that handles these cases too.