Under attack? Get incident response help

Secure development

GraphQL API security: five tests to run before launch

Five GraphQL tests to run on staging: defaults, input validation, query cost, batching abuse and field-level authorisation, each with the fix.

Also in:TürkçeDeutschFrançaisEspañolItalianoPolskiNederlandsEesti

Illustration: GraphQL API security: five tests to run before launch

Run these five tests against a staging deployment of your GraphQL API before the endpoint is public. Configure staging as production will be, or the results mean little.

They follow the four areas in the OWASP GraphQL Cheat Sheet: input validation, expensive queries, access control and insecure default configuration.

1. Check the defaults

Excessive errors, introspection and GraphiQL are the usual insecure defaults. Test all three. Start with the smallest introspection query:

{__schema{types{name}}}

See whether the server answers with schema details or refuses. Then look for GraphiQL in a browser. Finally send a malformed query and a query with a bad argument, and read the error. Resolver names, stack traces or database messages in the response are a map for an attacker.

The fix is configuration. Turn off introspection and GraphiQL where the API is not meant to be explored, return generic errors, and keep the detail in server-side logs.

2. Push hostile input through every argument

The user supplies identifiers, and data fetchers then use them in HTTP requests, database queries and other calls. That is where injection happens. Test every argument of every query and mutation with the payloads you would use on any other API: SQL and NoSQL injection, OS command injection, SSRF and CRLF injection.

To make this scriptable, generate the argument lists from the schema while introspection is still on in staging, loop your payloads over them, and switch introspection off afterwards.

What to enforce:

  • Specific GraphQL types such as scalars and enums, and defined schemas for mutation input.
  • An allowlist of characters, not a denylist. Alphanumeric, non-unicode input is a good starting point because it rules out many attacks.
  • Parameterised statements for anything passed to an interpreter. If you use an ORM or ODM, use it as documented, since ORM injection is still possible.
  • Rejection of invalid input without revealing how the validation works.

3. Find the expensive query

An API is vulnerable if any resource limit is missing or set badly. The OWASP entry on unrestricted resource consumption lists execution timeouts, maximum allocable memory, file descriptors, processes, upload file size, records per page, operations per client request and spending limits at third-party providers.

To test, build the deepest query your schema allows by following relations in a loop. Adapt the names to your schema:

{ users { orders { customer { orders { customer { email } } } } } }

Keep adding levels until the server refuses or slows down. Then ask for a very large page size and send an oversized payload.

The cheat sheet's advice is to add checks that limit or prevent queries that are too expensive. Combine that with a page size cap, an execution timeout and maximum sizes for strings, arrays and uploads. Tune the values from a week of real traffic logs, so legitimate clients stay under them.

4. Batch your way past the rate limiter

Batching is brute force with the request count hidden. In the OWASP entry's second scenario, an upload mutation sits behind a rate limiter and a size check. The attacker sends one POST to /graphql containing an array of nearly a thousand operations. The limiter sees one request and the server runs out of memory. If your API accepts uploads, our article on designing and testing secure file uploads covers that side.

Try the same against a login or OTP mutation:

[
  {"query": "mutation {login(user: \"alice\", pass: \"guess1\") {token}}"},
  {"query": "mutation {login(user: \"alice\", pass: \"guess2\") {token}}"}
]

Check whether your limit counts every operation in the array, and whether failed attempts still reach your lockout logic. Then try any other way your server allows of packing several operations into one request.

Limit the number of operations per client request, and throttle how often one client can run a single sensitive operation, such as validating an OTP. Cap spend as well: in the entry's first scenario, a script triggers tens of thousands of paid SMS messages through a forgot-password flow. Set spending limits for third-party providers, and billing alerts where limits are not possible.

5. Ask for someone else's field

Authentication tells you who is calling. It does not decide which fields they may read. The GraphQL authorisation guide says execution should begin after authentication middleware confirms identity, and that the server must still decide, per requested field, whether the user may see it.

Test with two ordinary accounts. With account A's session, request account B's objects by ID, then reach them through a related object that both accounts can see. Adapt the names to your schema:

# Direct lookup: should be refused for account A
{ customer(id: "CUSTOMER_ID_OF_B") { email } }

# Same data through a shared parent: must also be refused
{ product(id: "SHARED_PRODUCT_ID") { orders { customer { email } } } }

If the first query is refused and the second returns B's email, the check sits on the top-level lookup and was never applied to the nested type. Repeat with no session at all. If the same data is served through REST or a mobile API, test those entry points too. Our article on testing every ID in your app for IDOR and BOLA covers the same approach beyond GraphQL.

Authorisation inside a field resolver is fine for prototypes, but you must duplicate it for every entry point. If the copies drift apart, users see different data depending on which API they use. Move the logic into the business logic layer so there is one source of truth. In the guide's example, postRepository.getBody receives the user and the post and returns the body only to the author. Pass a fully hydrated user object, not an opaque token or API key.

What a scanner will not tell you

Automated tools give breadth, but the OWASP Web Security Testing Guide points out that they are built for applications in general, not for your custom code and business rules. The most serious flaws are frequently intertwined with business logic.

Tests 1 to 3 are mostly scriptable. Test 5 needs manual effort, because someone has to understand which tenant owns which object, so it is the one most worth handing to an outside team. Our penetration testing service covers that.

Where to start

  1. Send the introspection query and open GraphiQL on the staging deployment. Switch off what answers, and retest with a malformed query to check the errors.
  2. List every query and mutation argument. Confirm each has a specific type or enum and an allowlist validator.
  3. Send the deeply nested query, a large page-size request and an oversized payload. Set the limits, then resend the queries and confirm each is refused.
  4. Send an array batch against your login and OTP mutations. Enforce a limit per request and on repeats of sensitive operations.
  5. Run the two-account test, including nested relations, on every type holding personal or commercial data. Move any check found in a resolver into the business logic layer.

How we can help

Penetration testing

We attack your networks, applications and people the way a real adversary would, then show you how to close each gap.

Contact us
AngelForce team

Written by the AngelForce team: security consultants with CEH, LPT Master and CISA certifications who test, monitor and defend systems for organisations across Europe.

Written with AI assistance from public sources listed below, and reviewed by the AngelForce team before publication.

Sources

  1. cheatsheetseries.owasp.org/cheatsheets/GraphQL_Cheat_Sheet.html
  2. api-security.owasp.org/editions/2023/en/0xa4-unrestricted-resource-consumption/
  3. graphql.org/learn/authorization/
  4. wstg.owasp.org/latest/

Articles about Secure development

Tell us what you need to protect

Describe your environment and what you want to achieve. We will reply by email.

Contact us
info@angelforce.eu +372 5705 2044

AngelForce OÜ · Tallinn, Estonia