Safeguard
Application Security

What is GraphQL Security

GraphQL's flexibility creates unique risks—excessive data exposure, DoS via nested queries, and broken field-level authorization. Here's how to defend it.

Bob
Application Security Engineer
7 min read

GraphQL security is the set of practices that protect GraphQL APIs from the vulnerability classes the query language introduces by design: excessive data exposure through flexible field selection, denial-of-service through unbounded nested queries, broken authorization at the field and resolver level, and information leakage through schema introspection. Facebook built GraphQL internally starting in 2012, open-sourced it in 2015, and handed governance to the newly formed GraphQL Foundation under the Linux Foundation in November 2019. That flexibility is exactly why GraphQL needs its own security model: a single /graphql endpoint replaces dozens of REST routes, so traditional per-endpoint rate limiting, WAF rules, and access-control checks stop mapping cleanly onto the traffic. A REST API firewall rule for /users/:id doesn't know what to do with a query that asks for user(id: 1) { orders { payments { cardNumber } } } in a single POST body. Securing GraphQL means securing the schema, the resolvers, and the query execution engine — not just the transport.

What makes GraphQL security different from REST API security?

GraphQL security differs from REST because one endpoint and one HTTP verb (almost always POST /graphql) serve every operation, collapsing the surface that REST spreads across many URLs and methods into a single query language that clients control. In REST, an API gateway can apply distinct rate limits, auth scopes, and logging to GET /users, POST /orders, and DELETE /accounts/:id because each is a separate route. In GraphQL, all three might be different fields or mutations resolved inside the same POST request, so authorization has to be enforced per-field inside resolvers rather than per-URL at the edge. This also means a client can request 50 nested relationships in one call that would have taken 50 separate REST requests — each of which the backend would previously have had a chance to rate-limit individually.

What are the most common GraphQL vulnerabilities?

The most common GraphQL vulnerabilities are excessive data exposure, broken object-level authorization, injection through resolver arguments, and denial-of-service via query complexity — four of which map directly onto categories in the OWASP API Security Top 10 (2023 edition): API1 Broken Object Level Authorization, API3 Broken Object Property Level Authorization, API4 Unrestricted Resource Consumption, and API8 Security Misconfiguration. Excessive data exposure happens when a resolver returns a full internal object (say, a User type with passwordHash or internalNotes fields) and relies on the client to simply not ask for those fields, rather than stripping them server-side. Injection occurs the same way it does in REST: a resolver argument gets concatenated into a raw SQL, NoSQL, or shell command without parameterization — GraphQL's type system validates that an argument is a String, but does nothing to validate what's inside that string.

How does GraphQL introspection expose your API?

GraphQL introspection exposes your API by letting anyone send a __schema query and get back every type, field, argument, and deprecated field in the system — effectively a machine-readable map of your entire data model. Introspection is enabled by default in nearly every GraphQL server implementation, including Apollo Server, GraphQL Yoga, and graphql-java, because it's what powers developer tools like GraphiQL and Postman's schema autocomplete. Left enabled in production, it hands attackers a complete blueprint: field names like isAdmin, internalId, or ssn reveal exactly what to query for, and deprecated-but-still-resolvable fields often expose legacy logic nobody remembers is reachable. Open-source tools such as InQL (a Burp Suite extension) and GraphQL Voyager exist specifically to pull down a schema via introspection and visualize the entire attack surface in minutes. Even when a team disables introspection, tools like Clairvoyance reconstruct a working approximation of the schema by brute-forcing field names against error messages — so schema disclosure has to be treated as a defense-in-depth layer, not a single control.

Why are nested queries and batching a denial-of-service risk?

Nested queries and batching create a denial-of-service risk because GraphQL has no built-in limit on query depth, breadth, or the number of resolver calls a single request can trigger. A query that nests a self-referential type — for example, friends { friends { friends { friends { name } } } } } five or six levels deep against a social-graph schema — can force the server to execute thousands of database calls from one HTTP request, and the resolver count grows exponentially with each added level. Batching compounds this: GraphQL's spec-compliant support for sending an array of operations in one POST body lets an attacker submit hundreds of login mutations in a single request, bypassing a rate limiter that was configured to count "requests" rather than "operations." This aliasing/batching technique has been publicly documented against production login and password-reset endpoints, since request-based throttles see one connection where the application sees 500 credential-stuffing attempts. Without query cost analysis or depth limiting, a $0.03 API call can turn into a multi-second, resource-heavy database fan-out that a handful of concurrent requests will use to take a service down.

How do broken authorization checks happen in GraphQL resolvers?

Broken authorization checks happen in GraphQL resolvers when a developer secures the top-level query field but forgets that every nested field resolves independently and needs its own check. A common pattern: query { organization(id: 5) { name, members { email, salary } } } might correctly verify the requesting user can view organization(id: 5), but the members.salary resolver runs separately and often just trusts that if you got this far, you're allowed to see it — this is the field-level version of broken object-level authorization (BOLA), and it's exploitable specifically because GraphQL lets a client traverse relationships REST would never expose in one call. Public bug bounty disclosures against GraphQL-based platforms including Shopify's Admin API have documented exactly this pattern: authorization enforced on the entry-point object but not on nested associations reachable through it. Because a single query can pivot across a dozen relationships, one missing @auth directive on a nested resolver can expose an entire adjacent dataset.

How can you secure a GraphQL API in production?

You secure a GraphQL API in production by disabling introspection outside development, enforcing query depth and cost limits, moving authorization into every resolver rather than only the entry point, and using persisted queries to stop arbitrary client-authored requests from reaching your backend at all. A practical baseline: cap query depth at 6–8 levels, assign a numeric cost to each field and reject queries whose total exceeds a budget (Shopify and GitHub both publish and enforce cost-based limits on their public GraphQL APIs), set per-operation timeouts so one slow nested resolver can't hold a connection pool open, and require allow-listed persisted queries for any public-facing client so the server only ever executes queries it has pre-approved. Layer in resolver-level authorization checks (not just root-field checks), parameterized data access in every resolver to prevent injection, and structured logging that records the full query shape — not just the endpoint — so a 500-field batch request shows up in monitoring instead of looking like a single innocuous POST.

How Safeguard Helps

Safeguard extends supply chain and application security coverage to the GraphQL layer by treating your schema, resolvers, and the packages behind them as part of one connected attack surface. Reachability analysis traces whether a vulnerable dependency used inside a resolver — say, a flawed JSON parser or ORM sitting behind a members field — is actually invoked by a reachable query path, cutting through alert noise on GraphQL services that pull in dozens of transitive packages. Griffin AI reviews resolver code and schema definitions to flag missing field-level authorization, unbounded recursive types, and introspection left enabled in production configs, then opens auto-fix PRs with the depth limit, cost directive, or authorization check applied inline. SBOM generation and ingest give security teams a verified inventory of every GraphQL server library and its transitive dependencies across services, so when a CVE lands in Apollo Server, graphql-java, or a resolver's underlying database driver, Safeguard can tell you within minutes which deployed schemas are exposed and which are not.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.