GraphQL Security Checklist: Introspection, Query Depth, Batching, Authorization, and DoS

Secure GraphQL APIs by controlling introspection, query depth, cost, batching, resolver authorization, error detail, uploads, and resource consumption.

Back to Blog

GraphQL Security Checklist: Introspection, Query Depth, Batching, Authorization, and DoS

GraphQL gives clients unusual control over the shape of a request. That flexibility is useful, but it also changes the security questions compared with a conventional REST API. A single endpoint can expose many object types, nested relationships, mutations, and expensive resolver paths. If authorization and resource controls are bolted on after the schema is designed, the API can look tidy while still allowing data leakage or denial of service.

A practical GraphQL security review should focus on five areas: what the schema reveals, how deeply clients can query, how batching is handled, whether authorization is enforced at every resolver that needs it, and whether one request can consume unreasonable amounts of CPU, memory, database work, or downstream API calls. OWASP's GraphQL guidance highlights the same themes: validate input, limit expensive queries, enforce access control, and disable insecure defaults that are not required in production.

Introspection is not the main security boundary

GraphQL introspection allows clients to ask the server about its schema. In development, that is extremely useful. In production, some organizations disable introspection for public APIs to reduce unnecessary information disclosure. That can make casual reconnaissance harder, but it does not replace authorization.

An attacker can still infer operations from frontend code, mobile applications, error messages, public documentation, historical traffic, or predictable naming. If a sensitive field is accessible to the wrong user, hiding the schema does not make the field safe. The right question is therefore not simply whether introspection is enabled. It is whether the API exposes more schema detail than production clients need and whether all resolvers independently enforce the right access rules.

Disable GraphiQL and development consoles unless they are intentionally public

Interactive GraphQL consoles such as GraphiQL or similar explorers make experimentation easy. They may expose schema browsing, autocomplete, documentation, saved queries, or request history. On an internal developer environment that may be acceptable. On a public production host, it can unnecessarily simplify reconnaissance.

If your organization keeps a production explorer, protect it with real authentication and decide which users actually need it. Do not rely on an obscure URL or robots.txt entry. Treat it like any other administrative interface.

Limit query depth

A GraphQL query can nest through relationships repeatedly. Even without a circular schema, legitimate-looking nesting can create disproportionate backend work:

query {
  organization {
    projects {
      members {
        projects {
          members {
            projects {
              id
            }
          }
        }
      }
    }
  }
}

A depth limit rejects queries that exceed a chosen nesting level. The correct value depends on the schema. A blanket limit of three may break normal clients, while a limit of twenty may provide little protection if each resolver fans out aggressively. Measure real production queries, identify the deepest legitimate flows, and set a limit that leaves reasonable headroom. Revisit it when the schema changes.

Depth is not enough: use query cost analysis

Two queries with the same depth can have very different costs. One may fetch a single account and five small fields. Another may ask for thousands of records, nested relationships, computed fields, and resolver calls to third-party services.

Cost analysis assigns weight to fields or operations so the server can reject requests whose estimated work exceeds a threshold. Expensive fields such as exports, search, analytics, image processing, or external API lookups should carry more weight than scalar metadata. Pagination limits also matter. A query like users(first: 100000) should not be allowed merely because it is shallow.

Batching can bypass simple rate limits

GraphQL servers may allow multiple operations in one HTTP request. That can be useful for legitimate clients, but it can also defeat rate limiting that counts requests instead of operations. If a login mutation, OTP verification, password-reset check, or expensive search can be repeated hundreds of times inside one batch, a gateway limit based only on HTTP requests may provide almost no protection.

Count the work the application performs, not only the number of requests. Set limits on the number of operations per batch. Apply separate throttling to sensitive mutations. Consider disabling batching entirely where the client does not need it.

Authorization belongs at the object and field layer

A common mistake is protecting the GraphQL endpoint but not the underlying objects. The user is authenticated, so the server lets the query execute and trusts client-supplied IDs. Authorization should answer whether the current user can read this object, see this field, perform this mutation in the object's current state, and act within the correct tenant.

Do not assume that hiding a field from the frontend prevents access. A determined client can submit its own query. Sensitive fields may need their own resolver-level checks even when the parent object is visible to a broad audience.

Mutations need state-aware authorization

GraphQL mutations can hide business logic behind a single endpoint. Validate not only whether the user has permission to call the mutation, but whether the requested state transition is valid. A user may be allowed to cancel their order, but not after shipping. A manager may approve an expense, but not their own. A tenant administrator may invite users, but only into their own tenant.

These are application rules. GraphQL does not supply them automatically.

Validate input beyond GraphQL's type system

GraphQL types prevent some malformed input, but they do not validate business meaning. A string can still contain an unsafe URL. An integer can still be outside a meaningful range. An enum can still represent a state transition the user is not allowed to make.

Apply server-side allowlists, length limits, numeric ranges, format validation, and contextual validation. Do not pass GraphQL input directly into SQL, shell commands, file paths, template engines, or URL fetchers.

Control production error detail

Development error responses often include stack traces, resolver names, file paths, SQL fragments, or internal service information. Production clients usually need a stable error code and a safe message, not the internal exception. Log detailed errors on the server with a correlation identifier and return only the information required for the client to handle the failure.

File uploads need normal upload controls

If the GraphQL stack supports uploads, apply the same controls you would use on a dedicated upload endpoint: size limits, type validation, storage outside executable paths, randomized filenames, antivirus or content scanning where appropriate, and strict authorization. Do not assume multipart support in the GraphQL framework automatically makes content safe.

Monitor resolver cost and failure patterns

Rate limits are easier to tune when you know what legitimate traffic looks like. Track query duration, resolver counts, database calls, response size, error rates, and expensive operation names. Alert on sudden increases. Persisted queries can reduce the set of accepted production operations in tightly controlled architectures, although they are not appropriate for every public GraphQL API.

Test authorization with at least two identities

One account cannot prove isolation. Create controlled test users with different roles or tenants and attempt the same queries from each. Change object IDs, request restricted fields, invoke mutations, and verify that authorization decisions remain correct when the request is sent directly rather than through the intended user interface.

Keep the tests safe and authorized. The goal is to confirm that the server rejects invalid access, not to collect another customer's real data.

Consider downstream amplification

A GraphQL resolver may call another API, execute a search, render a document, or start a background job. One small query can therefore cause large work outside the GraphQL process. Cost models should account for those downstream effects, including services that charge per request.

Put spending limits or billing alerts around expensive third-party integrations. A denial-of-service weakness can also become a cost-amplification weakness.

How Vulnify relates to GraphQL security

Vulnify's deeper website scanning includes GraphQL-related checks within its supported assessment logic. That can help identify externally observable GraphQL exposure and common security weaknesses. The public security tools are also useful for surrounding controls such as TLS, headers, exposed paths, and technology discovery.

A scanner cannot prove your field-level authorization or understand every business rule in a custom schema. Use automated testing to identify public risk, then use authenticated tests and code review for resolver authorization, tenant isolation, query-cost rules, and business state transitions.

GraphQL production review checklist

  • Decide whether introspection and GraphiQL are needed in production.
  • Require authentication where the schema or operation is not public.
  • Enforce object, field, mutation, and tenant authorization.
  • Apply depth, pagination, and query-cost limits.
  • Limit or disable batching where it can magnify sensitive operations.
  • Rate limit operations, not only HTTP requests.
  • Validate input values beyond GraphQL types.
  • Restrict production error detail.
  • Apply normal upload controls to GraphQL file uploads.
  • Measure expensive queries and resolver behavior.
  • Retest authorization after schema changes.

GraphQL security is about controlled flexibility

GraphQL's strength is that clients can ask for exactly what they need. The server has to make sure that flexibility does not become unlimited access or unlimited work. Schema hiding can reduce noise, but strong authorization and resource controls are the real security boundaries.

If you can answer which fields a user may access, how expensive a query may become, how many operations can be batched, and what the server does when those limits are exceeded, you are much closer to a defensible production GraphQL deployment.