GraphQL Security

GraphQL's flexibility is also its main security challenge. A single endpoint accepts arbitrary queries: clients can nest relationships ten levels deep, request thousands of items, alias the same expensive field a hundred times, or send dozens of operations in one HTTP request. Authorization can't be handled per URL like in REST, because one query may touch dozens of types. Data exposure bugs and denial-of-service risks look different in GraphQL, and they need different defenses.

The fundamentals of API security still apply: authenticate, authorize every access, validate input, and rate-limit. GraphQL adds query cost analysis, persisted queries, and careful schema exposure on top.

TL;DR

Quick Example

Apollo Server hardening with depth and cost limits plus resolver-level authorization:

Core Concepts

Authentication vs Authorization

Broken object-level authorization (BOLA/IDOR), where a user fetches order(id: "someone-else's"), is the top API vulnerability in OWASP's API list, and GraphQL's node(id:) fields make it especially easy to probe. See OWASP Top 10.

Resource Exhaustion Attacks

Query Cost Analysis

Assign costs to fields (for example, list fields multiplied by first, expensive resolvers weighted higher), compute a query's total cost during validation, and reject queries over a budget. Cost also enables cost-based rate limiting: each client gets a budget of cost points per minute. Public GraphQL APIs such as GitHub and Shopify publish their cost models so clients can plan.

Persisted Queries

Introspection and Schema Exposure

Introspection lets anyone download the full schema. That's great for development tooling, and a map for attackers on private APIs. For internal or first-party-only APIs, disable introspection in production (or restrict it to authenticated staff), and turn off field suggestions ("Did you mean adminNotes?") that leak field names. Remember this is defense in depth: hiding the schema doesn't replace authorization.

Other Considerations

Best Practices

Centralize Authorization Logic

Put permission checks in a shared business layer or policy module (or schema directives like @auth backed by it), not scattered ad hoc through resolvers. Every resolver path to an object then enforces the same rules.

Use Allowlists for First-Party Clients

If only your own web and mobile apps call the API, trusted persisted queries remove the attack surface of arbitrary queries almost entirely, and improve caching and performance as a bonus.

Set Timeouts and Limits at Every Layer

Beyond validation rules: request body size limits, execution timeouts, database statement timeouts, and per-resolver timeouts for downstream calls.

Test Security Like Functionality

Write tests asserting that users can't access others' objects via every path (direct queries, node(id:), nested relationships), and that oversized or deeply nested queries are rejected. Tools like InQL, graphql-cop, and Escape help audit GraphQL endpoints.

Common Mistakes

Authorizing Only at the Top-Level Query

Authorize every type and sensitive field, not just entry points.

Relying on Hidden Introspection

Disabling introspection without authorization checks is security through obscurity: attackers can guess field names or extract them from client bundles. Treat it as a small extra layer, never the main one.

Rate Limiting Only HTTP Requests

One HTTP request can contain a batched array of 1,000 operations or one query with 1,000 aliases. Rate-limit and cost-limit per operation and per query cost.

FAQ

Is GraphQL less secure than REST?

Not inherently, but it shifts where risks sit. REST can lean on per-endpoint authorization and naturally bounded responses. GraphQL needs object- and field-level authorization, query cost controls, and deliberate schema exposure. Teams that apply REST habits unchanged often miss these.

Should I disable introspection in production?

For private APIs used only by your own clients, usually yes, or restrict it to authenticated internal users. For public APIs intended for third-party developers, keep it on; the schema is documentation. Either way, authorization must not depend on it.

How do I choose depth and complexity limits?

Measure your real client operations. Set depth slightly above your deepest legitimate query (often 7–10), and complexity at a comfortable multiple of your most expensive legitimate query. Log rejected queries to tune limits, and give clients clear error messages.

What are trusted documents?

A persisted-query allowlist: client operations are extracted at build time and registered with the server, which then executes only those operations by ID. It's the strongest defense against malicious query shapes for first-party APIs.

Related Topics

References