GraphQL security testing: a test plan with queries and fixes

GraphQL security testing checks one endpoint that accepts many query shapes. Testers find the endpoint, test introspection, check authorization on every object and field, try alias and array batching against login and OTP limits, probe depth and cost limits, then check injection, CSRF and error leakage. Fixes include per-field authorization, depth and cost limits, persisted queries and production configuration.

What does GraphQL security testing cover?#

GraphQL security testing checks an API that exposes one endpoint, usually /graphql, where the client writes the query. It covers the same flaws as REST testing (broken authorization, injection, brute force, denial of service) plus issues that come from the query language itself: introspection, aliases, batching, query depth and field suggestions. PortSwigger puts it this way: GraphQL vulnerabilities generally arise from implementation and design flaws, such as introspection left active.

Three facts about GraphQL shape the test plan. One endpoint serves every object, so authorization must be checked per object and per field. One HTTP request can hold many operations, so a limit that counts HTTP requests misses them. The client picks how deep and how wide a query goes, so the server must set the limits. For general API testing, see our API penetration testing service and the OWASP API Security Top 10 guide.

Test only systems you own or have written authorization to test.

How do you test a GraphQL API step by step?#

Work through ten steps in order. Each step lists the query to send and the result that counts as a finding. All examples use a fictional demo API.

Demo API for the examples below

This is a demonstration. Harbor Notes is a fictional note-taking app, its GraphQL endpoint is https://api.example.com/graphql, and no real customer system was tested. The tests use two test accounts, alice and bob.

1. Find the endpoint#

Send a universal query to the common paths. PortSwigger lists /graphql, /api, /api/graphql, /graphql/api and /graphql/graphql, and suggests appending /v1 if those fail.

POST /graphql HTTP/1.1
Host: api.example.com
Content-Type: application/json

{"query":"query{__typename}"}

A response containing {"data":{"__typename":"query"}} confirms GraphQL, because every endpoint has the reserved __typename field.

2. Check introspection#

{"query":"{__schema{queryType{name}}}"}

If this returns type names, introspection is on. PortSwigger advises that introspection be disabled in production. Repeat the probe with GET /graphql?query=... and with Content-Type: application/x-www-form-urlencoded, because a block may apply to one method only. If introspection is on, dump the full schema with InQL or a standard introspection query and read every query, mutation and "private" field.

3. Recover the schema when introspection is off#

Send a query with a misspelled field. A server that returns a message like Did you mean 'noteContent'? leaks names through suggestions. PortSwigger notes this feature comes from the Apollo platform. The clairvoyance tool automates schema recovery from these hints. The finding is the leak, so record which names came back and the request that returned them.

4. Test object-level authorization#

Log in as alice, take the ID of one of bob's notes from a second session, and request it:

{"query":"query{note(id:\"bob-note-id\"){id title body owner{email}}}"}

Safe result: an authorization error or null. Finding: Bob's note comes back to Alice. This is the GraphQL form of IDOR and maps to OWASP API1:2023, Broken Object Level Authorization. Run controls: a nonexistent ID and Alice's own note ID. Check mutations too: can Alice call deleteNote on Bob's note? Read the note back from Bob's session to prove the write.

5. Test field-level authorization#

Ask for fields the role should not see, such as user{email role passwordResetToken}, and compare anonymous, alice and an admin test account. The OWASP cheat sheet says to check access control for data access, queries and mutations.

6. Test alias batching on login and OTP#

Rate limiters often count HTTP requests. PortSwigger shows that aliases let one request carry many attempts. Use a test account and known wrong passwords, and keep the number small:

{"query":"mutation{a1:login(user:\"alice\",password:\"wrong-1\"){token} a2:login(user:\"alice\",password:\"wrong-2\"){token} a3:login(user:\"alice\",password:\"wrong-3\"){token}}"}

Finding: all three run, and the lockout counter or rate limiter allows more attempts than its stated limit. Test array batching the same way if the server accepts a JSON array of operations: [{"query":"..."},{"query":"..."}]. OWASP names batching attacks as a GraphQL-specific brute force method.

7. Test depth and cost limits#

Send a query that nests a circular relationship a few levels deeper than normal use, and increase the depth in steps while you watch response time. Stop at the first sign of slowdown, and agree the limits with the owner first, because this test is a denial of service test. OWASP's cheat sheet notes that depth and amount can be unlimited by default and may lead to a DoS. Request a very large page size (first:100000) on a list field and check whether the server caps it.

8. Test injection in arguments#

Send a single quote or a well-known time-delay string in string arguments that reach a database, and compare against controls. OWASP's cheat sheet lists SQL, NoSQL and OS command injection among common GraphQL attacks. See our guides to SQL injection and NoSQL injection for proof standards.

9. Test CSRF#

PortSwigger explains that CSRF over GraphQL arises when the endpoint does not validate the content type and has no CSRF tokens. application/json POSTs are safe if the content type is checked. A state-changing mutation that works over GET or x-www-form-urlencoded is a finding. Confirm in a real browser with a test account, and mark the result browser-observed.

10. Check errors and debug features#

Trigger a validation error and a resolver error. Look for stack traces, SQL text, file paths and internal service names. Check whether GraphiQL or another IDE is reachable without authentication, as OWASP warns.

What differs between GraphQL variants and REST?#

The table maps each GraphQL feature to its risk and the test that finds it.

Feature Risk Test Fix
Introspection Full schema disclosure __schema probe over POST and GET Disable in production, or for unauthenticated users
Field suggestions Schema leaks when introspection is off Misspelled field Hide suggestions in errors
Aliases Many operations in one request Alias-batched login Limit aliases, count operations, per-account lockout
Array batching Same effect as aliases JSON array of operations Disable batching or cap it for sensitive operations
Nested queries CPU and memory exhaustion Increasing depth Depth and cost limits, timeouts
Single endpoint Per-object authorization gaps Cross-account object IDs Authorize in each resolver or in the data layer
GET and form POST CSRF Mutation via GET Accept only JSON, add CSRF protection

Which real CVEs show GraphQL denial-of-service and parsing issues?#

CVE-2022-37734 is one dated example. NVD published it on 12 September 2022: graphql-java versions before 19.0 are vulnerable to a denial of service in which a malicious GraphQL query consumes CPU resources. The fixed versions are 19.0 and later, 18.3, and 17.4 (NVD). The lesson for defenders is to run current library versions and still set your own depth and cost limits, since a library fix covers one query shape and not the class.

How do you secure a GraphQL API?#

Authorize in the data layer, set depth and cost limits, limit operations per request, and disable introspection and suggestions in production. The OWASP cheat sheet lists input validation, DoS prevention, access control and secure configuration as the main areas.

Framework defaults#

Check these against the version you run. Settings change between releases.

Server Introspection default What to set
Apollo Server (Node.js) On, unless NODE_ENV is production Set introspection: false explicitly; set hideSchemaDetailsFromClientErrors: true (default false) to remove "did you mean" hints; csrfPrevention is on by default
GraphQL.js Available unless you add the rule Add NoSchemaIntrospectionCustomRule as a validation rule; limit complexity; use trusted documents
GraphQL Yoga Available until you add the plugin Add the useDisableIntrospection plugin; Yoga docs also cover disabling field suggestions
Strawberry (Python) Available until you add the extension Add the DisableIntrospection and QueryDepthLimiter extensions
graphql-java Available until you restrict it Add MaxQueryDepthInstrumentation and the complexity instrumentation
Hasura On by default, per Hasura docs Disable introspection per role under Security; the Hasura page is titled for Cloud Professional and Enterprise editions

Node.js with Apollo Server#

import { ApolloServer } from "@apollo/server";
import depthLimit from "graphql-depth-limit";

const server = new ApolloServer({
  typeDefs,
  resolvers,
  introspection: false,
  hideSchemaDetailsFromClientErrors: true,
  validationRules: [depthLimit(7)],
});

The OWASP cheat sheet names graphql-depth-limit for JavaScript depth limiting. Pick a depth that fits your real queries.

Per-field authorization in resolvers#

Check the owner in the resolver or, better, in the data-access function every resolver shares.

const resolvers = {
  Query: {
    note: async (_, { id }, ctx) => {
      if (!ctx.user) throw new GraphQLError("Unauthenticated");
      const note = await db.notes.findById(id);
      if (!note || note.ownerId !== ctx.user.id) return null; // same answer for missing and forbidden
      return note;
    },
  },
};

Python with Strawberry#

import strawberry
from strawberry.extensions import DisableIntrospection, QueryDepthLimiter

schema = strawberry.Schema(
    Query,
    extensions=[DisableIntrospection(), QueryDepthLimiter(max_depth=7)],
)

Java with graphql-java#

GraphQL graphQL = GraphQL.newGraphQL(schema)
    .instrumentation(new MaxQueryDepthInstrumentation(7))
    .build();

Persisted queries and operation limits#

GraphQL Yoga's documentation says that to stop unknown actors from reverse-engineering the schema and running arbitrary operations, persisted operations are highly recommended. GraphQL.js documents trusted documents the same way: when you only allow trusted documents, introspection control and complexity limits matter less. For public APIs that must accept arbitrary queries, count operations as well as HTTP requests. OWASP lists three options against batching: a code-level object request limit, no batching for sensitive objects such as passwords and OTPs, and a cap on queries that run at once.

What evidence proves a GraphQL finding?#

A GraphQL finding needs the request, the response and a control. For authorization, show Alice's request for Bob's object, the returned data, a control with a nonexistent ID, and a read-back from Bob's session for writes. For introspection, save the probe and the schema size. For batching, record the number of attempts the server allowed against the stated limit. Our reports carry timestamps and a SHA-256 manifest for key evidence, and note controls that held, such as a field that correctly returned an error.

Useful tools: Burp Suite's built-in GraphQL support, InQL as a Burp extension, graphql-cop for a quick set of checks, and clairvoyance for schema recovery. Run any scanner only inside the written scope and rate limits.

GraphQL security testing checklist#

  1. Find the endpoint with query{__typename}.
  2. Test introspection over POST, GET and form-encoded bodies.
  3. Test for field suggestions when introspection is off.
  4. Test object-level authorization for every query and mutation with two accounts.
  5. Test field-level authorization across roles.
  6. Test alias and array batching on login, OTP and reset flows.
  7. Test depth, cost and page-size limits with agreed limits.
  8. Test injection in arguments that reach a database or shell.
  9. Test CSRF over GET and form-encoded POST.
  10. Check errors, GraphiQL and debug output for leaks.

When should you get a professional test?#

Get a test when your GraphQL API holds customer data, has several roles, or exposes login and payment flows. The hard findings are authorization gaps that depend on your data model, and they need two or more accounts and a read-back to prove. Our API penetration testing service covers GraphQL APIs, and the web application penetration test covers the front end that calls them. Related: CORS misconfiguration.

Questions

Is GraphQL less secure than REST?

Neither is secure by default. GraphQL moves risk to new places: one endpoint serves every query, clients choose the shape of the response, and one request can hold many operations. Authorization and rate limits must work per object and per operation. REST APIs with weak authorization have the same core flaws.

Should I turn off GraphQL introspection in production?

For an internal or first-party API, yes. OWASP's GraphQL Cheat Sheet suggests disabling introspection system-wide for internal APIs, and for public APIs, disabling it for unauthenticated or unauthorized users. Introspection off hides the schema but does not replace authorization checks.

What is a GraphQL batching attack?

A batching attack sends many operations in one HTTP request, either with aliases or an array of queries. A rate limiter that counts HTTP requests sees one request. OWASP lists batching as a GraphQL-specific brute force method, and PortSwigger shows aliases bypassing request-based limits.

Does disabling introspection stop attackers from learning the schema?

No. Servers such as Apollo can return suggestions like 'Did you mean' in error messages, which leak field names. Testers use these hints and tools such as clairvoyance to rebuild a schema. Disable suggestions too, and rely on authorization instead of schema secrecy.

Can GraphQL be vulnerable to SQL injection?

Yes. GraphQL does not sanitize arguments. If a resolver builds a SQL or NoSQL query from an argument by string concatenation, the injection flaw exists exactly as in REST. OWASP's cheat sheet lists SQL, NoSQL and OS command injection among the common GraphQL attacks.

Sources

  1. OWASP GraphQL Cheat Sheet (checked 9 November 2026)
  2. PortSwigger Web Security Academy: GraphQL API vulnerabilities (checked 9 November 2026)
  3. OWASP API Security Top 10 2023: API4 Unrestricted Resource Consumption (checked 9 November 2026)
  4. OWASP API Security Top 10 2023: API1 Broken Object Level Authorization (checked 9 November 2026)
  5. Apollo Server documentation: ApolloServer API (introspection, csrfPrevention, hideSchemaDetailsFromClientErrors) (checked 9 November 2026)
  6. GraphQL.js documentation: Going to production (checked 9 November 2026)
  7. GraphQL Yoga documentation: Introspection (checked 9 November 2026)
  8. Strawberry GraphQL documentation: Disable Introspection (checked 9 November 2026)
  9. Strawberry GraphQL documentation: Query Depth Limiter (checked 9 November 2026)
  10. GraphQL Java documentation: Instrumentation (checked 9 November 2026)
  11. Hasura documentation: Disable GraphQL introspection (checked 9 November 2026)
  12. NVD: CVE-2022-37734 (graphql-java) (checked 9 November 2026)
  13. InQL (Doyensec), GitHub (checked 9 November 2026)
  14. graphql-cop (Dolev Farhi), GitHub (checked 9 November 2026)
  15. clairvoyance (Nikita Stupin), GitHub (checked 9 November 2026)