Safeguard
Security Guides

GraphQL API Security: Introspection, Depth Limits, and Authorization

GraphQL's flexibility is its attack surface. Nested queries, introspection, and per-field authorization all fail differently than REST. Here's how to secure them.

Daniel Osei
Security Researcher
5 min read

GraphQL gives clients a single endpoint and the freedom to ask for exactly the data they want, in whatever shape and depth they choose. That flexibility is precisely why GraphQL security fails differently from REST. A single crafted query can request data ten relationships deep and force your resolvers to fan out into thousands of database calls. A schema left introspectable hands an attacker a complete map of your data model. And because authorization in GraphQL has to happen per field and per object rather than per route, the classic broken-object-level-authorization flaw is easy to reintroduce in a resolver you forgot to guard. This guide covers the GraphQL-specific controls that a REST-oriented security checklist will miss.

Should introspection be enabled in production?

Not for public, unauthenticated APIs. Introspection lets any client query the schema itself — every type, field, and argument — which is invaluable in development and a reconnaissance gift in production. It hands an attacker your entire data model without guessing. Disable it in production for external APIs, or gate it behind authentication:

// Apollo Server: disable introspection outside development
const server = new ApolloServer({
  schema,
  introspection: process.env.NODE_ENV !== "production",
});

Disabling introspection is defense-in-depth, not access control — a determined attacker can still probe field names. It raises the cost of reconnaissance; it does not replace authorization.

How do you stop a single query from taking down the server?

By bounding how expensive any one query can be, along two axes: depth and complexity. Depth limiting rejects queries nested beyond a threshold, which defeats the cyclic-relationship attack where posts { author { posts { author { ... } } } } recurses until your database melts. Complexity analysis assigns a cost to each field and rejects queries whose total exceeds a budget — better than depth alone because a shallow query can still request thousands of expensive items.

import depthLimit from "graphql-depth-limit";

const server = new ApolloServer({
  schema,
  validationRules: [depthLimit(7)], // reject queries deeper than 7 levels
});

Add a complexity plugin on top, set per-field costs, and cap paginated fields with a maximum page size so first: 1000000 can't be requested at all.

Why doesn't route-based authorization work in GraphQL?

Because there are no routes to attach it to — one endpoint serves everything. Authorization has to move into the resolvers, checked per object and per field. This is where broken object-level authorization (the #1 item on the OWASP API Security Top 10) creeps back in: a user(id: $id) resolver that returns any user by ID without checking whether the caller is allowed to see that user is a textbook BOLA hole.

const resolvers = {
  Query: {
    invoice: async (_, { id }, { currentUser }) => {
      const invoice = await db.invoice.findById(id);
      // Object-level check: does this user own this invoice?
      if (invoice.ownerId !== currentUser.id) {
        throw new ForbiddenError("Not authorized");
      }
      return invoice;
    },
  },
};

Enforce authorization in the resolver closest to the data, and consider field-level guards for sensitive fields (a User.email a caller shouldn't see) rather than relying on the query shape.

Is query batching a denial-of-service risk?

It can be, and it also amplifies other attacks. GraphQL supports sending an array of operations in one request, and aliasing lets a single query invoke the same expensive field many times under different names. Attackers use both to multiply work — and to bypass naive rate limiting that counts requests rather than operations. Cap the number of batched operations, limit aliases of the same field, and rate-limit on query cost rather than request count. Persisted queries (allowlisting a fixed set of hashed queries) eliminate arbitrary query submission entirely for first-party clients.

Does GraphQL make injection go away?

No. Resolvers still talk to databases, and a resolver that interpolates an argument into a SQL string or NoSQL query is injectable exactly like any other backend code. GraphQL's type system validates shapes, not safety — a String argument can still contain ' OR 1=1 --. Use parameterized queries in every resolver, validate and normalize inputs, and never trust a value just because it passed schema validation. Because these behaviors only manifest at runtime, a dynamic scan that submits real queries against the endpoint is the most direct way to confirm your resolvers hold.

GraphQL security checklist

ControlPurpose
Disable/gate introspection in prodReduce schema reconnaissance
Depth + complexity limitsPrevent expensive-query DoS
Per-object authorization in resolversStop BOLA
Field-level guards on sensitive fieldsLimit over-exposure
Cap batching + aliasesPrevent amplification
Cost-based rate limitingBeats request-count limits
Persisted queries for first-party clientsEliminate arbitrary queries
Parameterized queries in resolversPrevent injection
Continuous SCA on server dependenciesSecure the library layer

The library layer

Your GraphQL server runs on a stack of dependencies — the server library, schema tools, data-loader utilities — and every one is potential supply-chain risk. The 2025 npm incidents (the Shai-Hulud worm across 500+ packages per CISA's September alert; trojanized chalk and debug) show why. Pair the schema-level controls above with software composition analysis across the full dependency graph.

How Safeguard Helps

Safeguard resolves the complete dependency tree behind your GraphQL server and runs reachability analysis so you patch the CVEs your resolvers actually touch. Griffin, our AI analysis engine, flags behavioral anomalies in new package versions before they land, and auto-fix opens tested pull requests with the minimal safe upgrade. The schema discipline — introspection, depth and complexity limits, per-object authorization — stays your responsibility, and a dynamic scan helps you verify it end to end.

Secure your GraphQL API and its dependencies — start for free, read the documentation, or see how Safeguard compares.

Never miss an update

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