Skip to main content
Fleuret raises €4M to build Europe's AI pentester.

How to test a GraphQL API for security vulnerabilities

Yanis Grigy, CEO12 min read

Testing a GraphQL API for security vulnerabilities takes more than one introspection query and a checklist run once. You map the schema, prove object-level authorization at every resolver, probe inputs for injection and business-logic flaws, and stress the query limits so an expensive request cannot take the service down. Then you fix, retest and keep the evidence, because the schema will change again next sprint.

Key takeaways

  • GraphQL adds schema discovery and query-cost risks, and it still inherits classic API problems like injection and weak authorization.
  • The tests that matter most are object-level access control, input validation and demand controls: depth limits, rate limits and query cost analysis.
  • Disabling introspection is not enough. Field suggestions and other production defaults can still leak schema details.
  • The workflow that holds up is repeatable: discover, test, fix, retest, and keep the proof for later security or compliance reviews.

The trap is to treat GraphQL like REST with nicer queries. It is not. GraphQL gives clients more flexibility, and that flexibility expands what you have to enumerate, constrain and retest in production.

Start with the GraphQL risks that change your test plan

What is unique to GraphQL

GraphQL changes the attack surface in ways that matter to testers. You still look for generic API flaws such as SQL injection, but you also test schema discovery, field leakage, object-level authorization and denial of service through expensive queries. The first pass should cover introspection, field suggestions, broken authorization and query cost.

That order matters. Introspection and suggestions teach an attacker the schema. Authorization flaws let them reach data they should never see. Expensive nested queries can then turn a public endpoint into a resource drain. A GraphQL API often looks compact from the outside, usually a single endpoint, but the number of possible execution paths is far larger than the number of URLs suggests.

What is still plain API security

Part of the work does not change. Inputs still need validation. Authentication headers still need sane handling. Mutations still need anti-abuse checks. If a resolver passes user input into a database query, a shell command or a downstream service without validation, you can still end up with injection or business-logic abuse, even when the schema itself is clean.

So the test plan mixes GraphQL-specific checks with standard API testing. You are not replacing API security, you are adding schema-aware methods on top of it. Our guide to continuous API security assessment covers the broader API side.

Map the schema without leaking more than you should

Use introspection when it is available

Introspection can reveal the whole schema: types, fields, directives, queries, mutations and how they relate. In practice, inspect the responses around __schema and __typename to see what the API exposes, then note which operations only read and which change state. Look at query roots, object types, interfaces and custom directives, then trace the likely sensitive objects such as user, invoice, order or policy records.

That does not mean pasting a giant introspection dump into the report. It means using introspection to build a clean map of the attack surface. If GraphiQL or GraphQL Playground is exposed in production, record that as a finding too. Those interfaces are useful in development, and insecure defaults should not stay open in production.

Keep testing when introspection is disabled

Disabling introspection alone is not enough. Field suggestions can still leak valid names when a client misspells a field, which gives an attacker enough structure to iterate. Send near-miss queries, read the error text, and check whether the server reveals field names, type names or resolver hints.

GraphQL Voyager helps visualise a schema once you have legitimate access to it, but it does not replace testing the production endpoint. The goal is to understand what the server reveals under both normal and error conditions.

Test authorization at every resolver boundary

Check object-level access

GraphQL does not enforce permissions by default: the application has to implement access control. Every resolver boundary deserves a test, not only the top-level query. The reliable method is simple: log in as two roles, capture a valid query as the lower-privilege user, swap the object ID for one owned by the other user, then add fields the first role should not see.

Use a concrete object, such as an order or an invoice that belongs to another account. If the query still returns that record, or if hidden fields appear in the response, you have an authorization failure, even though the request was perfectly valid.

Check mutation abuse and role drift

Repeat the same pattern with mutations. Try to update an address, change a billing email, approve a refund or edit a patient note with a role that should only read. Then test role drift: once a session is established, do the permissions still hold when the user changes organisation, status or project membership?

A useful way to structure the review is three questions, in order:

  1. Can the caller see the object at all?
  2. Can the caller see these fields on that object?
  3. Can the caller perform this action on it?

That sequence catches many mistakes that happy-path testing misses. Broken object-level authorization is often easier to prove than to explain, so keep the request and the response together as the proof.

Probe inputs for injection and unsafe business logic

Treat every argument as untrusted

GraphQL is not immune to classic API issues. The OWASP Web Security Testing Guide still applies: test inputs for SQL injection, command injection, template injection and any downstream abuse a resolver can trigger. Strong typing helps, but it does not remove the risk, because a schema can accept values that become dangerous once they reach a resolver or a backend service.

Custom scalars, search filters, sort fields and nested input objects deserve special attention. A scalar named date may still accept malformed strings. A filter object may let the caller pick operators you never meant to expose. A sort field can sometimes reveal internal columns or trigger expensive database behaviour.

Vary the payload shape

A practical test is to run one mutation or search query with different payload shapes: the expected scalar type, then an oversized string, then an unexpected enum value, then a nested filter using operators the UI never exposes. Watch for validation failures, server errors, odd normalisation, and any change in business behaviour that shows the resolver trusted the input too much.

For each finding, record five things: the vulnerable argument, the resolver path, the expected behaviour, the actual behaviour and the business impact. That makes the retest far easier, and it turns the work into a repeatable process instead of notes nobody reopens.

Stress demand controls before attackers do

Depth, breadth and batching

GraphQL.org's security guidance recommends depth limiting, breadth and batch limiting, pagination, rate limiting, and trusted documents (persisted queries). These controls stop a client from asking for too much in one request, or from making one request do the work of many. Test each of them, because they protect against different failure modes.

A nested request like user { orders { items { product { reviews { author { profile } } } } } } gets expensive fast. It looks harmless, but each level can multiply resolver work. Aliases do the same thing: one query asking for a: account(id: 1), b: account(id: 2), c: account(id: 3) can trigger repeated resolver calls while the payload stays short. Aliasing is also a classic way to brute-force a login or a one-time code inside a single HTTP request, past a rate limiter that only counts requests.

Cost analysis, pagination and timeouts

The OWASP GraphQL Cheat Sheet adds amount limiting and query cost analysis, and both are worth testing directly. Send a request for more items than pagination should allow, or a query whose computed complexity should exceed the policy. Then check that the server rejects it cleanly, not after burning CPU for several seconds.

Run the checks in this order:

  • Depth limits on nested selections.
  • Alias and batch limits on repeated operations.
  • Pagination limits on list fields.
  • Cost or complexity thresholds on large queries.
  • Timeouts and rate limits on abusive traffic patterns.

There is a trade-off. Introspection, flexible queries and rich nesting help legitimate clients, but they must be bounded in production. If the API serves mobile apps, partner integrations and internal tools, the limits have to protect all of them without punishing every normal request. The stakes justify being strict: according to IBM's Cost of a Data Breach Report 2025, the global average cost of a breach was USD 4.44 million.

Pick the right emphasis before you test

Different GraphQL APIs need different levels of pressure. A small internal service can get a lighter pass. A production API with external clients needs deeper authorization and demand testing.

SituationPrioritiseDe-emphasise
Internal API behind single sign-onResolver authorization, role drift, input validationPublic schema exposure and anonymous access checks
External API for customers or partnersIntrospection leakage, object-level access, query cost controlsDeep infrastructure testing outside the API boundary
Regulated production API with audit needsEvidence, retest, change tracking, control verificationOne-off manual checks with no retained proof

The last row is where the workflow changes. Once you need evidence for an audit, a test cycle with dated proof and a recorded retest is far easier to defend than a folder of screenshots.

Build a repeatable GraphQL test workflow

Manual checks you always need

Follow the same order every time: schema discovery, authorization tests, input abuse, demand-control tests, then regression retest. Manual review still matters for business logic, object-level authorization and anything that depends on context a tool cannot infer, such as a mutation that is harmless in one tenant and dangerous in another.

Burp Suite helps with request tampering, replay and header manipulation, but the method matters more than the tool. Capture one good request, mutate it systematically, and write down what changed. That discipline is what turns a pentest into evidence.

What to automate between releases

Automate the stable checks: schema discovery against known environments, repeated authorization probes, common injection payloads, and demand-control tests on depth, aliases and pagination. Keep humans on the cases that need judgment, especially where a resolver combines authentication, business rules and downstream side effects. Our comparison of API penetration testing tools goes through what each category of tool actually covers.

The retest matters as much as the finding. A vulnerability that stays open for weeks after the report is not a point-in-time issue, it is part of how the API runs.

Where Fleuret fits in GraphQL security testing

Fleuret runs agentic pentests on web apps, REST and GraphQL APIs and external infrastructure, with results in hours. On a GraphQL API, that means the authorization, injection and query-limit checks above, each confirmed finding delivered with a replayable proof of concept: the exact query or mutation, and the response that proves the impact.

Findings are pushed to Jira, Linear or Slack so the fix lands in the team's normal workflow. Once it is shipped, a retest replays the original request to confirm the hole is closed. For regulated teams, findings come with a DORA and NIS2 mapping and a signed report on demand, which covers the evidence chain auditors ask for: detection, proof, ticket, fix, retest.

Manual review still has a place for the cases that need a human decision: whether an authorization edge case is acceptable, or whether a resolver's business behaviour is correct.

Use this remediation checklist after every finding

After you find a weakness, fix the control and verify it in production-like conditions. The usual checklist:

  • Restrict introspection, and disable schema-leaking field suggestions where possible.
  • Enforce authorization in the resolvers, not only at the gateway.
  • Validate every input, including custom scalars and filter objects.
  • Set depth and amount limits, and apply query cost analysis.
  • Paginate list fields and rate limit requests, including aliased operations.
  • Serve over HTTPS with sensible timeouts.

OWASP's guidance covers input validation and complexity controls. GraphQL.org adds pagination, limits, persisted queries, HTTPS and timeouts.

For each fix, retest the exact query or mutation that failed, not a new version that looks safer. Keep the original request, the failing response, the remediation change and the passing retest, so the same chain can be reused at the next audit.

What to do next

Start with one production-like schema, one low-privilege account and one high-risk mutation. Build the test set around the controls that fail most often: authorization, input validation and query limits. Then keep the evidence and the retests in a repeatable cycle rather than a spreadsheet.

Fleuret runs agentic pentests on GraphQL and REST APIs, with replayable PoCs, Jira tickets and a retest after the fix. See Fleuret in action.

Sources


Share this postShare on LinkedIn
TRY IT ON YOUR APP

Scan your own code, free.

The Free plan runs SAST, DAST, SCA and secret scanning for 2 users and 10 repos, no call needed. Several apps or an audit deadline? Book a demo instead.