Test a GraphQL API in six steps: introspection, field suggestions, batching, denial of service through deep queries, authorization, and injection. Do them in that order, because each early step reveals the schema or the limits that make the later steps more precise. Use a test environment and test accounts.
Why GraphQL needs a different routine
A REST scan walks a list of paths. A GraphQL API usually has one path, often a single POST endpoint, and the real surface is the schema behind it. Per-endpoint checks can look clean while the schema exposes a lot. The OWASP Web Security Testing Guide has a dedicated GraphQL section for this reason.
The six checks
| Step | What you do | What you are looking for |
|---|---|---|
| 1. Introspection | Send an introspection query and map the schema | Full schema in a production-facing environment, plus sensitive operations such as queries that return tokens |
| 2. Field suggestions | Send a misspelled field name | Error text that suggests real field names even when introspection is off |
| 3. Batching | Send several operations in one request | Per-request rate limits or WAF rules that count the batch as one |
| 4. Denial of service | Send deeply nested or very large queries | No limit on depth, complexity or execution time |
| 5. Authorization | Run the same operations as different roles and as an unauthenticated caller | Sensitive queries and mutations reachable by the wrong user, objects readable by non-owners |
| 6. Injection | Test arguments and custom scalars | SQL, command or script injection through values passed to backends |
These steps follow the WSTG GraphQL page. It adds two extra checks worth keeping: retry a blocked operation under an alias to see whether a name-based denylist still runs the resolver, and review error messages for internal detail.
Working notes for each step
Introspection. If it works, export the schema and keep it. You will use it to list every query, mutation and type. OWASP's cheat sheet advises disabling introspection in production or public environments, and also notes that this does not fully stop field discovery.
Field suggestions. The cheat sheet suggests turning off "Did you mean" suggestions when introspection is off. If your test shows them, that is a concrete fix.
Batching. Test whether login or one-time password checks can be repeated inside one request. The cheat sheet says to prevent batching for sensitive operations and to cap the number of operations that can be batched.
Denial of service. Increase nesting in small steps on a staging API. The cheat sheet itself suggests testing whether a staging API can be slowed by a nasty query before investing in cost analysis. Do not do this against production.
Authorization. GraphQL does not enforce permissions by default. Check object access on nodes as well as edges. The cheat sheet cites a bug where edges were checked but nodes were not, and says to check for node or nodes fields that fetch objects by ID.
Injection. Resolvers pass values to databases and other services, so apply your normal injection tests to arguments.
Record the results as a table
Keep one row per operation and one column per role, with the status returned. Gaps in the pattern, for example an admin mutation that a normal user can run, show up immediately.
Worked example: a small shop schema
Take a shop API with one GraphQL endpoint. The schema has a products query, an order query that takes an ID, a me query, an updateProfile mutation, a cancelOrder mutation, and an internal allUsers query meant for admins. Walk through the six checks in order.
1. Introspection. The tester sends an introspection query as an anonymous caller. The full schema comes back, including allUsers and a field called apiToken on the user type. Two items to follow up: introspection is open, and a sensitive field exists. The tester saves the schema file, because every later step uses it.
2. Field suggestions. The team turns introspection off in staging and retests. The tester then asks for a field named apiTokn. The error says, "Did you mean apiToken?" Disabling introspection hid the schema document, but the error text still leaks the field name. The fix is to turn off suggestions as well.
3. Batching. The tester sends one request containing fifty login mutations with different passwords. Rate limiting counts HTTP requests, so the batch counts as one. This is the bypass the cheat sheet warns about. The fix is to forbid batching on login and to count operations, not requests.
4. Denial of service. The schema has cycles: a user has orders, an order has items, an item has a product, a product has reviews, and a review has an author who is a user again. The tester nests the query six levels deep on staging, then eight, then ten, watching response time. At ten levels the response takes several seconds and CPU rises. There is no depth limit. The fix is a depth limit and a timeout, with amount limits on list fields.
5. Authorization. Using the schema, the tester builds a table of operations and callers:
| Operation | Anonymous | Customer | Admin |
|---|---|---|---|
| products | Allowed | Allowed | Allowed |
| me | Denied | Allowed | Allowed |
| order(id) for own order | Denied | Allowed | Allowed |
| order(id) for another customer's order | Denied | Should be denied | Allowed |
| allUsers | Denied | Should be denied | Allowed |
| cancelOrder for another's order | Denied | Should be denied | Allowed |
The tester runs each as each caller. The customer account gets a 200 on allUsers, so a function level flaw exists. The order query for another customer's ID also returns data, so there is an object level flaw too.
6. Injection. The products query takes a search argument. The tester sends a single quote and typical injection strings. One returns a database error in the response, which suggests the argument reaches a query unsafely.
What the findings add up to
Six steps, seven findings: open introspection, a leaking suggestion, batch bypass of rate limits, no depth limit, a function level flaw, an object level flaw, and a probable injection. None needed advanced tooling. They needed the schema, three accounts, and an ordered routine. That is why the order matters: introspection gives the schema, the schema gives the operations table, and the table drives the authorization and injection checks.
Turning it into a routine
Keep the operations table in the repository. When a developer adds a query or mutation, the pull request adds a row with the expected access, and a test that checks it. That keeps step 5 from becoming a one-off exercise.
Try it yourself
Open the demo to see how an example API scan presents endpoints and findings. For a GraphQL API, imagine the rows being operations in a schema rather than URLs.
Click through it at your own pace, or open it full screen.
Quick check
If introspection is disabled, is the schema private?
Not necessarily. Field suggestions in error messages can reveal real names, and attackers can try guesses. Test for both.
Does an OpenAPI import work for GraphQL?
No. GraphQL needs its own schema as input, usually through introspection or a schema file.
Is GraphQL less secure than REST?
Not by design. It moves the risk: one endpoint, flexible queries, and no built-in permissions, so authorization, batching and cost limits need deliberate work.
Sources
Last reviewed: 19 October 2026.