Bypassing Brute-Force Protections with LOTS of GraphQL

Authored by Lachlan Davidson


Background

So GraphQL is pretty cool. One thing I think is particularly cool is the ability to make multiple queries in one HTTP request and specify exactly what you want.

GraphQL let's you do neat things like this in one request:

query {
    blogPost(id: 7) {
        title
        description
        tags
    }
    
    relatedPosts(id: 7) {
        title
        author
        tags
    }
    
    friends {
        name
        id
    }
}

But this has one or two things you might wanna keep in mind.

Bye-Bye Rate-Limiting, Hello DoS

So normally when making an API, you want to have some form of rate-limiting, right? This is obviously good to stop your server from getting overloaded, and particularily your database. For other functionality like authentication and trying to find secret unlisted content, you also likely want to mitigate brute-force attacks. However, with GraphQL, pure HTTP rate-limiting won't cut it. Consider the following:

query {
    blogPost(id: 1) { title }
    blogPost(id: 2) { title }
    blogPost(id: 3) { title }
    blogPost(id: 4) { title }
    blogPost(id: 5) { title }
    blogPost(id: 6) { title }
    (... 500 more items...)
}

This is going to thrash the database. It can be very easy to exhaust a connection pool or just slow everything down, without needing to generate a lot of HTTP traffic. Even if we're limited to 5 requests/second, we could still generate thousands of database operations per second.

This can also be problematic for login brute-force if protections are only implemented on a per-request level.

mutation {
    login(username: "bob", password: "password") { token }
    login(username: "bob", password: "password1") { token }
    login(username: "bob", password: "12345") { token }
    login(username: "bob", password: "P@ss40rd!") { token }
    login(username: "bob", password: "pa$$wrd") { token }
    login(username: "bob", password: "someb0dy0nc3t01dMe") { token }
    login(username: "bob", password: "hunter2") { token }
    (... 500 more items ...)
}

Takeaways?

This was obviously a quick one, but an important point nonetheless. Whilst allowing lots of operations in one HTTP request is a brilliant feature of GraphQL, it can make it particularly easy to DoS and starve resources.

So what should you do if you're a developer?

  • Ensure you have a maximum number of queries/mutations that can be performed in a single request.
  • Implement per-attempt brute-force restrictions, as opposed to per-request.

What should you do if you're a pentester?

  • Try thrashing GraphQL APIs with hundreds, if not thousands, of operations per request.
  • Attempt to bypass login brute-force restrictions by combing attempts into one request.