GraphQL security: test what your API lets clients ask
GraphQL gives clients one endpoint and a language to request exactly the data they want. That flexibility is the point, and it is also where risk hides. GraphQL security testing means checking what your API lets clients ask, before you ship it. This guide follows the OWASP GraphQL Cheat Sheet. The goal is defensive: understand what your schema exposes, confirm your authorization rules hold, and make sure one request cannot run away with your server.
Run everything here against your own staging environment, with permission, as part of building and hardening your product. OWASP lists the attacks to plan for: injection, denial of service, broken authorization including IDOR, batching attacks, and insecure default settings.

What to send on staging, and what should stop it. Simplified from the OWASP GraphQL Cheat Sheet.
Why GraphQL security is different from REST
In a REST API, each route is a door. You can list the doors, lock each one, and log who walks through. In GraphQL there is usually a single door, often /graphql, and the client writes the request. The server decides what to return by running resolvers, one per field.
That shifts where problems appear:
- Authorization moves into resolvers. A check at the endpoint does not protect a nested field several levels deep.
- Cost is set by the client. One request can ask for a list, then a list inside each item, then another.
- Rate limits count requests, not work. One HTTP request can carry many operations.
- Error messages reveal structure. Helpful hints about field names tell more than you intend.
So you cannot test GraphQL by hitting a list of URLs. You review it the way you would review your own feature spec: does each field do only what it should, for only the people it should serve?
Map what clients can ask: introspection and schema discovery
Introspection is a built-in GraphQL feature that returns the whole schema. During development it powers your tooling. In production it can hand anyone the full list of types, fields, arguments and mutations, including admin operations you never linked in the UI.
Your first test is simple: ask your production endpoint for its schema and see if it answers. OWASP notes that introspection and the GraphiQL explorer are often on by default with no sign-in, and recommends turning introspection off in production unless the API is meant for outside clients. If it returns everything, note that as a finding. If introspection is disabled, that is good, but it is not the whole story. Your frontend bundle usually ships every query and fragment the app uses, so the schema is rarely fully secret. Treat the field list as known and secure each field on its own merits. OWASP adds that attackers can still guess fields, helped by GraphQL's own "did you mean" suggestions.
The output of this step is a written inventory: every query, every mutation, every sensitive field, and a note on who should be allowed to use each one. That inventory is the map for the rest of your testing.
Test field-level authorization and object access
This is where most real GraphQL issues live. The pattern is called insecure direct object reference, or IDOR, and it happens when a resolver returns an object without checking that the current user owns it.
Test it with two of your own accounts. Sign in as account A. Take an object identifier that belongs to account B, such as an order or a document, and request it as account A. The correct result is an authorization error or empty response. If account A sees account B's data, the resolver trusts the identifier instead of the session.
Repeat the check on nested fields. A parent object may be guarded while a child field, reached through a relationship, is not. Walk every edge in your schema that connects one user's data to another's; OWASP's advice is to check authorization on both nodes and edges. The same discipline applies to admin-only fields: confirm a normal account cannot read them even when it names them directly.
Test query depth, complexity, and nested query abuse
Because the client sets the shape, a single request can be deliberately expensive. If your types reference each other, a query can nest the same relationship over and over, each layer multiplying the work your database does.
To test this safely, send progressively deeper queries against staging and watch response time and database load. If a deep, repeated nesting ties up your server, you need a defense. OWASP lists depth limits, amount limits, pagination, timeouts, query cost analysis and rate limiting per IP or user. Common ones are a maximum query depth, a cost or complexity limit that scores a query before running it, and a cap on how many items a list field returns. Pick sensible limits, apply them, then confirm an over-budget query is rejected before execution rather than after.
Test batching, aliases, and rate limit bypasses
GraphQL lets a client run several operations in one HTTP request, and it lets the same field be requested many times under different aliases. Both are legitimate. Both can defeat a rate limiter that counts HTTP requests.
The classic case is login. If your limiter allows, say, ten requests per minute, an attacker can still try many passwords by aliasing the login field dozens of times inside one request. Test it: send a single request that attempts the same sensitive operation under multiple aliases against a test account, and confirm your protections count operations, not requests. If your server accepts an array of operations in one call, apply the same check there.
OWASP calls this a batching attack and names its two uses: brute force, and enumerating objects such as users and IDs in very few requests.
Test mutations for hidden writable fields and injection
Mutations are where clients change state, so they deserve the hardest look. Two problems recur.
First, hidden writable fields. If your update mutation accepts a broad input type, a client may be able to set a field you assumed only the server controls, such as a role, an owner, a balance or an internal flag. Test by adding those fields to your own mutation and checking whether they take effect. This is the GraphQL version of the mass assignment problem, and the fix is an explicit allowlist of fields a client may write.
Second, injection. GraphQL validates types, but it does not sanitize the values inside strings. If a resolver drops an argument into a database query, a shell command or another API call without parameterizing it, the usual injection risks apply. Review every resolver that passes client input into a downstream call. OWASP's rule: pass input to other interpreters only through safe APIs such as parameterized statements.
A repeatable GraphQL security test checklist
Write this down and run it every release, not just once:
- Try introspection on production. Build your field inventory and note who should reach each field.
- With two accounts, test object access on every query and nested relationship.
- Confirm admin-only fields reject normal accounts that name them directly.
- Send progressively deeper and wider queries. Confirm depth, complexity and list-size limits reject over-budget requests before execution.
- Attempt aliased and batched operations against sensitive actions. Confirm limits count operations.
- Probe mutations for fields a client should not write. Lock inputs to an allowlist.
- Review resolvers that pass input into databases, shells or other services.
- Send a broken query and check that production returns no stack trace and no debug output.
- Retest after every fix, because one change can reopen another path.
Keep the checklist next to your other security tests in CI, so each release reruns it.
Frequently asked questions
Should I disable GraphQL introspection in production?
Turning it off reduces casual discovery and is a reasonable default. Do not treat it as a secret, though, since your frontend usually exposes the queries it uses. Secure each field with real authorization regardless.
How do I limit GraphQL query depth and cost?
Set a maximum nesting depth, score each query with a complexity or cost budget before you run it, and cap how many items a list field can return. Reject anything over budget before execution so the expensive work never happens.
Do JWTs and CSRF protection still matter for GraphQL APIs?
Yes. GraphQL still rides on HTTP, so token handling and request forgery are live concerns. Pair this review with a JWT security test and a CSRF protection test.
Get started
Send the introspection query to production today, and run one object-access test with two accounts.

whitehatstoic builds web and mobile apps from database to deploy and tests them to protect them. Its cybersecurity and AI safety testing covers web app and API review, prompt injection tests for AI systems, a written report with fixes, and a retest after those fixes land. Each engagement is scoped after a short call. No test proves an API is free of every weakness, but a written report gives you specific findings and the fixes to close them.
Building something that has to be safe? Book a meeting with whitehatstoic: tell us the product, the deadline and your biggest worry, and we reply with a plan and a price.
Comments
No comments yet.
Sign in or make an account to comment.