GraphQL

GraphQL Security Best Practices: A Practical Guide

Published: 19 September 2026

A GraphQL schema makes capabilities discoverable; the running resolver must still enforce every access and cost decision.

Start at the resolver, not the endpoint

GraphQL security best practices begin with the work performed for each field. A single HTTP path can invoke many resolvers and reach multiple services. Gateway authentication identifies a caller, but a resolver still needs to decide whether that caller may read the requested object and property. Model the chain from root query to nested field so a harmless-looking selection cannot cross a tenant boundary.

Write a policy for each sensitive object and action: who may query it, which fields are visible, and which mutations are allowed. Test the same object with an owner, another tenant, and a privileged operator. A schema directive can communicate an intended role, but it helps only if execution code applies it consistently. Treat schema review as design feedback, then validate the behaviour of the running API.

Make nested authorization explicit

A query for project { members { email } } can expose fields that a top-level member query protects correctly. Reuse authorization in the data access layer or resolver helpers rather than relying on the entry field alone. Filter collections before pagination and counts; otherwise a total or cursor can reveal records the caller cannot read. Test nested access through several root fields because GraphQL permits alternate paths to the same data.

For mutations, separate permission to invoke an operation from permission to act on a particular object. A user might be allowed to update an issue but not transfer it to another organisation or set an internal status. Accept only client-editable fields and derive owner or tenant IDs from trusted identity context. Batch loaders should include tenant or permission scope in their keys to avoid cross-request data reuse.

Budget the cost of a query

Depth alone does not measure cost. A shallow field that returns thousands of records may be expensive, while a deeper lookup with bounded edges can be cheap. Set page-size caps, maximum depth, maximum aliases, request bytes, and an operation cost budget based on measured resolver work. Include list multipliers and expensive fields in the model. Reassess weights when database access patterns change.

Try nested lists, repeated aliases, fragments, and batched operations in a controlled load test. Verify that the server rejects excessive work before dispatching costly resolvers, and that cancelled requests stop downstream calls. Apply caller-level quotas as a separate control against repeated moderate queries. Observe database time and downstream fan-out; a gateway count of HTTP requests understates GraphQL workload.

Choose an introspection policy deliberately

Introspection is useful to tooling and authorised developers, but public production access may reveal fields, types, and deprecations that make reconnaissance easier. Decide whether anonymous clients need it. If not, require appropriate access or restrict it in production; keep a secure schema distribution route for approved clients. Disabling introspection does not replace authorization because clients may already know field names.

Maintain one reviewed schema as the source of truth and compare it with the deployed graph. Remove deprecated fields only after assessing clients. Avoid putting secrets, internal hostnames, or sensitive examples in descriptions. If the graph is federated, record which subgraph owns a field and where authorization occurs. A directive in one subgraph can be bypassed if another path exposes the same backing data.

Handle errors and subscriptions safely

GraphQL can return data and errors in one response. Decide which partial results are acceptable and prevent errors from including stack traces, database queries, or hidden object identifiers. Give clients a stable error code and correlation ID while writing richer details to restricted logs. Test malformed inputs, resolver failures, and forbidden nested fields. Do not let a partial success silently disclose fields neighbouring a denied selection.

Subscriptions and long-lived connections need authentication at connection setup and an expiry or revalidation policy. Recheck object access when a client subscribes and when an event is delivered if permissions can change. Bound subscription counts, payload size, and fan-out. A one-time token check does not guarantee a stream stays authorised after a user loses access.

Test the graph users can actually reach

Create a small matrix of roles, tenants, root operations, sensitive nested fields, and expected outcomes. Add regression queries for alternate traversal paths and aliases. Pair these with cost tests at realistic data volume, since an empty staging database hides expensive resolver patterns. Include a malicious query budget case and a legitimate complex query so limits remain usable.

APISAST analyses OpenAPI and Swagger descriptions rather than native GraphQL schemas or resolver code. If a GraphQL service has an HTTP wrapper described in OpenAPI, a contract scan can review those documented routes, but it cannot assess GraphQL field authorization or complexity. Use GraphQL-aware schema checks and live tests for those controls, and keep the contract's claims aligned with deployed behaviour.

Example: an organisation member field

Imagine a query that returns organisation { projects { members { email } } }. A caller may belong to the organisation yet lack permission to see member emails for a private project. A root-level organisation check will not catch that distinction. Put the authorization decision near the project-member data access and test the field through every root path that can reach it. Include a list count test, because even a count may disclose activity.

Now add an alias that requests projects repeatedly with different filters. The same valid caller might force many expensive resolver calls in one HTTP request. Assign a cost to the list field, multiply it by the allowed page size, and reject a query above the budget before execution. Then run the query on realistic data to compare predicted and actual work. Authorization and demand control need separate evidence.

Protect persisted queries and caches

Persisted or trusted documents can simplify first-party client traffic and limit the shapes of queries accepted in production. Review a query before placing it in an allowlist, version its identifier with the client build, and define how a rollout handles old mobile clients. A persisted document is not automatically safe: its variables can still refer to another tenant's object or request an excessive page size. Validate arguments and permissions at execution time.

Caching can improve performance but can also mix data across users. Include tenant and authorization scope in cache keys for private results, or disable shared caching for fields that depend on identity. Test a response as one user, then request the same query and variables as another user. The second user must not receive the first user's cached private fields. Review data-loader lifetime so per-request caches are not accidentally reused globally. Review mutations with the same care: nested input objects can include fields intended only for internal workflows. Construct permitted changes server-side, validate state transitions, and test a caller who supplies an extra privileged field. A broad JSON scalar requires particularly careful validation.

Continue the work

Use these guides to put the checks into your API review process:

Primary reference: GraphQL security guidance.

APISAST reviews OpenAPI and Swagger contracts for documented design signals. Confirm authorization, enforcement, performance, and abuse controls against a running service.

More API security guides