# GraphQL security testing: a test plan with queries and fixes

Source: https://penetrationtestservices.com/vulnerabilities/graphql-security-testing/
Publisher: Excession Labs (penetrationtestservices.com)
Last updated: 2026-11-09

A 10-step GraphQL security testing plan: find the endpoint, check introspection, test authorization, batching, depth limits and CSRF, then fix with code.

> **Short answer:** 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](/api-penetration-testing/) service and the [OWASP API Security Top 10 guide](/guides/owasp-api-security-top-10/).

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](/guides/idor-testing/) 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](/vulnerabilities/sql-injection/) and [NoSQL injection](/vulnerabilities/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](https://nvd.nist.gov/vuln/detail/CVE-2022-37734)). 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

```javascript
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.

```javascript
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

```python
import strawberry
from strawberry.extensions import DisableIntrospection, QueryDepthLimiter

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

### Java with graphql-java

```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](/api-penetration-testing/) service covers GraphQL APIs, and the [web application penetration test](/web-application-penetration-testing/) covers the front end that calls them. Related: [CORS misconfiguration](/vulnerabilities/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

- [OWASP GraphQL Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/GraphQL_Cheat_Sheet.html)
- [PortSwigger Web Security Academy: GraphQL API vulnerabilities](https://portswigger.net/web-security/graphql)
- [OWASP API Security Top 10 2023: API4 Unrestricted Resource Consumption](https://owasp.org/API-Security/editions/2023/en/0xa4-unrestricted-resource-consumption/)
- [OWASP API Security Top 10 2023: API1 Broken Object Level Authorization](https://owasp.org/API-Security/editions/2023/en/0xa1-broken-object-level-authorization/)
- [Apollo Server documentation: ApolloServer API (introspection, csrfPrevention, hideSchemaDetailsFromClientErrors)](https://www.apollographql.com/docs/apollo-server/api/apollo-server)
- [GraphQL.js documentation: Going to production](https://www.graphql-js.org/docs/going-to-production/)
- [GraphQL Yoga documentation: Introspection](https://the-guild.dev/graphql/yoga-server/docs/features/introspection)
- [Strawberry GraphQL documentation: Disable Introspection](https://strawberry.rocks/docs/extensions/disable-introspection)
- [Strawberry GraphQL documentation: Query Depth Limiter](https://strawberry.rocks/docs/extensions/query-depth-limiter)
- [GraphQL Java documentation: Instrumentation](https://www.graphql-java.com/documentation/instrumentation/)
- [Hasura documentation: Disable GraphQL introspection](https://hasura.io/docs/2.0/security/disable-graphql-introspection/)
- [NVD: CVE-2022-37734 (graphql-java)](https://nvd.nist.gov/vuln/detail/CVE-2022-37734)
- [InQL (Doyensec), GitHub](https://github.com/doyensec/inql)
- [graphql-cop (Dolev Farhi), GitHub](https://github.com/dolevf/graphql-cop)
- [clairvoyance (Nikita Stupin), GitHub](https://github.com/nikitastupin/clairvoyance)
