Back to blog
API Security4.07.2026

GraphQL API Security: Introspection, DoS, BOLA and How to Harden Resolvers

GraphQL replaces dozens of REST endpoints with a single, flexible query surface. That flexibility is exactly what breaks the security assumptions most teams carry over from REST. A single POST to /graphql can traverse arbitrary object graphs, request nested relationships, and batch hundreds of operations in one request. If your authorization, rate-limiting, and cost controls were designed per-endpoint, they do not apply here. This post walks the main GraphQL-specific risks and the server-side controls that actually mitigate them.

Introspection exposure

GraphQL ships with a self-describing schema. A single __schema introspection query returns every type, field, argument, mutation, and deprecated field the server knows about. For an attacker this is a free, authoritative map of your entire attack surface — including internal admin mutations and fields you assumed were obscure.

Introspection is a legitimate developer tool, but in production it hands reconnaissance to anyone. Disable it in production builds (for example introspection: false in Apollo Server) and disable the GraphQL Playground/GraphiQL IDE on public deployments. Note that disabling introspection is defense-in-depth, not authorization — tools like Clairvoyance can reconstruct schemas through field suggestions, so turn off suggestion messages too and never rely on schema secrecy as a security boundary.

Query depth and complexity: unauthenticated DoS

Because clients choose the query shape, they can craft deeply nested, cyclic queries that explode server work. If Author has posts and Post has an author, an attacker recurses author→posts→author→posts to arbitrary depth, forcing exponential resolver execution and database load from a single small request. This maps to OWASP API4:2023 Unrestricted Resource Consumption and is a classic asymmetric DoS.

Layer these controls:

  • Depth limiting — reject queries beyond a fixed nesting depth (e.g. graphql-depth-limit set to 7–10).
  • Query complexity analysis — assign a cost to each field and reject queries above a budget before execution (graphql-cost-analysis, graphql-query-complexity).
  • Pagination caps — enforce a maximum first/last value; never allow unbounded list arguments.
  • Timeouts and amount limits — cap execution time and total nodes returned.
  • Persisted queries — allow only an approved all/safe-list of query documents in production, eliminating arbitrary query shapes entirely.

Batching abuse

GraphQL supports sending an array of operations in one HTTP request, and aliasing lets a client repeat the same field many times under different names. Both defeat naive per-request rate limits. A single request containing 1,000 aliased login mutations turns your brute-force protection into a no-op:

  • mutation { a: login(pw:"1"){token} b: login(pw:"2"){token} ... }

Mitigate by limiting operations per request, capping the number of aliases for sensitive fields, and rate-limiting by operation cost rather than by HTTP request count. Apply object-level throttling to authentication and other abuse-prone mutations.

BOLA / object-level authorization in resolvers

Broken Object Level Authorization (OWASP API1:2023) is the most damaging and most common GraphQL flaw. A query like { invoice(id: "1042") { total, customer { email } } } will happily return another tenant's invoice if the resolver fetches by ID without checking ownership. GraphQL makes this worse because authorization must be enforced at every resolver on every path — a field reachable through three different queries needs the check in all three reachable spots.

Do not authorize at the HTTP layer. Enforce field- and object-level authorization inside resolvers or a dedicated authorization layer: validate that the authenticated principal may access the specific object instance, not just the type. Use the data-loader/context to carry the identity, and prefer a policy engine (graphql-shield, OSO, or explicit guards) so checks are centralized and testable. Assume every field is directly reachable and default-deny.

Injection through resolvers

GraphQL itself does not sanitize anything — it passes arguments straight to your resolvers, which often build SQL, NoSQL, or OS commands. SQL injection (CWE-89), NoSQL injection, and SSRF all live in resolver code. Because the query language looks structured, developers wrongly assume inputs are safe. Always use parameterized queries/prepared statements, validate and coerce custom scalars, and treat every argument as untrusted. Watch out for filter/where arguments that pass raw operator objects into a Mongo/ORM query.

Errors and information disclosure

Verbose GraphQL errors leak stack traces, database driver messages, and internal field paths. Mask errors in production, log the detail server-side, and return generic messages to clients. Combined with disabled introspection, this denies attackers the reconnaissance loop.

A practical hardening checklist

  • Disable introspection and IDE in production; disable field suggestions.
  • Enforce depth + complexity limits and pagination caps before execution.
  • Limit batching and aliases; rate-limit by cost, not request count.
  • Object-level authz in every resolver, default-deny, centralized policy.
  • Parameterized queries and strict input validation in resolvers.
  • Mask errors, and consider persisted/allow-listed queries for the strongest posture.

GraphQL is not inherently insecure, but it moves the security boundary from the endpoint into the resolver graph. Model that graph, test each reachable path for authorization and cost, and treat the schema as a live attack surface rather than documentation. When we assess GraphQL APIs we rate findings against CVSS and OWASP API Security Top 10 so remediation is prioritized by real business impact.

graphqlapi-securityowasp-apidosbolaauthorization