GraphQL

Securing GraphQL APIs with static analysis

Control introspection, prevent over-fetching, and harden schemas before runtime.

Published: February 3, 2026 • Author: APISAST

Published: 3 February 2026 · Updated: 19 September 2026

GraphQL changes the threat model

A GraphQL query can select nested fields through one HTTP endpoint, so path-level gateway rules have little context about the work a resolver performs. A published schema or introspection may help clients discover fields, but hiding a field name is not an access control. Schema linting can flag missing conventions before release; authorization and query-cost limits must still be enforced by the running service.

Static analysis techniques

  • Require auth directives on root queries and mutations; flag anonymous access.
  • Set query depth and complexity limits; reject lists without pagination.
  • Decide whether production introspection is appropriate for your audience; protect data independently of that choice.
  • Validate input types to avoid injection; avoid generic JSON scalars for IDs.
  • Mark personally identifiable fields and ensure role-based guards exist.

Use a GraphQL-aware schema linter and resolver tests for these checks. APISAST's API security scanner supports OpenAPI and Swagger contracts for REST APIs, not native GraphQL schemas.

Combine SAST with runtime controls

Enforce persisted queries, apply rate limits per token, and log resolver-level metrics. Feed anomalies back into the spec so future versions ship with tighter rules. When introducing breaking schema changes, pair them with deprecations and changelog entries.

Review permissions at every resolver

A root query can authenticate a user and still expose a nested object from another tenant. Mark sensitive fields and mutations in the schema review, then check that each resolver enforces the caller's permissions on the actual resource. A directive can make the intended policy visible, but only tests or a code review can show that every resolver uses it. Avoid relying on client-side query restrictions: callers can send their own operations.

Build a test matrix with two users, different roles, and objects in separate tenants. Request the same field through a direct query, nested relationship, alias, and fragment. Test list filters and mutations as well as object lookups. Return safe error information when access is denied, and ensure a count or cursor does not disclose another tenant's records.

Bound the cost of client-selected work

A short GraphQL document can request many nested lists and trigger repeated database calls. Set limits on depth, list size, and estimated field cost; measure the actual load produced by realistic operations. Batch data access where it reduces repeated resolver work, and apply a quota to the caller or tenant. An HTTP request count alone may not reflect the cost of a deeply nested operation or a batch of queries.

Validate the limits with known expensive queries, not only a schema linter. Exercise aliases, fragments, and variable values that change list sizes. Document pagination for every collection field and provide a useful error when the cost limit is exceeded. Keep observability at the operation and resolver level so a traffic spike can be traced without logging tokens or personal data.

Use schema linting for the right questions

A GraphQL schema linter can check naming, deprecations, documentation, and custom directives your team uses for policy. It cannot prove that a resolver checks object ownership or that a depth limit is active. Put schema linting in CI to catch inconsistent contracts, and put authorization and load tests beside it. For a service that exposes both REST and GraphQL, maintain separate tests for each interface even if they share backend data.

APISAST accepts OpenAPI and Swagger files for REST contracts. A GraphQL schema must be reviewed with GraphQL-aware tools; converting a schema to OpenAPI would lose important query and resolver behaviour. Use the REST versus GraphQL comparison to plan the shared security baseline and the style-specific checks.

Best practices

When a field is deprecated, keep its authorization rule active until it is removed. A dormant field can still be called by an old client or an attacker who knows its name. Review persisted queries when a permission changes, because an approved query shape may still request data the caller no longer owns. Log operation names and failure rates, then investigate unusual resolver paths without storing request tokens or private response bodies.

Retest the old query before removing the field.

  • Use SDL linting in CI alongside OpenAPI security scans for REST components.
  • Document deprecations and removal timelines to avoid zombie fields.
  • Provide example queries that respect least privilege and pagination.
Back to blog