Learn GraphQL - Securing GraphQL APIs from Threats
Episode 15 of 51

Learn GraphQL - Securing GraphQL APIs from Threats

Episode 15 secures a GraphQL API from common threats: query depth and complexity attacks, introspection abuse, request-based and cost-based rate limiting, correct CORS, HTTPS and secure headers, and Automatic Persisted Queries with a whitelist.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

GraphQL brings incredible power — and that power can be abused. An innocent-looking query can burden the server hundreds of times more than a normal query. Episode 15 secures your GraphQL API against the most common threats.

We'll cover query depth limiting, query complexity analysis, rate limiting, introspection, CORS configuration, HTTPS and secure headers, and persisted queries with a whitelist — all part of the security checklist that must be applied before production.

Common Security Threats

Query Depth and Complexity Attacks

The most characteristic GraphQL threat is deep nesting. A query like user { posts { comments { user { posts { comments ... } } } } } can make resolvers run millions of times. The server burns resources on abnormal queries, opening the door to Denial of Service (DoS).

Other threats: batch attacks that send many operations in a single request, introspection abuse to map the schema, and queries requesting unbounded lists. All three need different strategies — starting with query restrictions.

Query Depth Limiting

Implementation with graphql-depth-limit

Depth limiting restricts how deeply a query can nest; install the library via npm install graphql-depth-limit:

Install depth limit
npm install graphql-depth-limit
JSEnable depth limiting
import depthLimit from "graphql-depth-limit";
 
const server = new ApolloServer({
  typeDefs,
  resolvers,
  validationRules: [depthLimit(10)],
});

Queries that go past depth 10 are rejected at the validation stage — before any resolver runs. The chosen value needs testing: too small and it rejects legitimate queries (like a deep dashboard), too large and it loses protection. Start at 8-12 for most applications.

Query Complexity Analysis

Computing Query Cost

A depth limit doesn't count how many resolvers actually execute. Query complexity analysis goes further: each field gets a cost, and the total query cost is capped.

Install cost analysis
npm install graphql-cost-analysis
JSComplexity limit
import costAnalysis from "graphql-cost-analysis";
 
const server = new ApolloServer({
  typeDefs,
  resolvers,
  validationRules: [
    costAnalysis({
      maximumCost: 1000,
      defaultCost: 1,
      costMap: {
        User: { posts: { multiply: 5 } },
      },
    }),
  ],
});

Expensive fields (like large lists or external calls) get a higher cost via costMap. The total query is compared against maximumCost; anything over the limit is rejected. This approach defeats queries that are cheap "per field" but expensive "cumulatively".

Rate Limiting

Request-Based and Cost-Based

Rate limiting prevents one client from overwhelming the server within a time window. For GraphQL there are two approaches:

  • Request-based: limit the number of requests per user per minute — simple, but expensive and cheap queries are treated the same.
  • Cost-based: limit the accumulated query cost per user per time window — fairer for GraphQL.
JSRate limit per user
import { createRateLimitDirective } from "graphql-rate-limit";
 
const rateLimitDirective = createRateLimitDirective({
  keyGenerator: () => "global",
  max: 100,
  window: "1m",
  message: "Terlalu banyak request, coba lagi nanti",
});

Apply rate limiting per user using the identity from ctx.user.id, and consider a whitelist for service-to-service API keys. Combine request-based and cost-based for layered protection.

Introspection and CORS

Disabling Introspection in Production

Introspection lets clients and tooling read the entire schema — useful in development, but it gives attackers a map in production:

JSDisable introspection in production
const server = new ApolloServer({
  typeDefs,
  resolvers,
  introspection: process.env.NODE_ENV !== "production",
  validationRules: [
    ...(process.env.NODE_ENV === "production"
      ? [depthLimit(10), costAnalysis(costOptions)]
      : []),
  ],
});

Remember: this is "security by obscurity" and not a replacement for authorization (episode 14) — sensitive fields must still be protected directly. Some teams actually leave introspection on for public APIs and rely on other protections.

CORS and HTTPS

Configure CORS with a specific origin whitelist; don't use * for APIs with credentials:

JSCORS with an origin whitelist
import cors from "cors";
 
app.use(cors({
  origin: ["https://app.kalian.com", "https://admin.kalian.com"],
  credentials: true,
}));

Always serve the API over HTTPS with modern TLS, and add secure headers like Strict-Transport-Security. Behind a CDN or load balancer, make sure the proxy forwards the needed headers.

Persisted Queries

Automatic Persisted Queries (APQ)

Persisted queries separate the query from the request: the client sends a query hash, and the server maps the hash to an approved query. Two benefits: less bandwidth and a security whitelist.

JSEnable APQ
const server = new ApolloServer({
  typeDefs,
  resolvers,
  persistedQueries: { ttl: 300 },
});

For a strict whitelist, register approved queries explicitly and reject unknown ones — this also disables the arbitrary queries attackers often use. APQ will be revisited from a performance and caching angle in episode 19.

Conclusion

Key takeaways:

  • Query depth and complexity attacks are GraphQL's characteristic threats and must be limited.
  • graphql-depth-limit rejects overly deep queries; cost analysis caps total query cost.
  • Request-based and cost-based rate limiting protect the server from single-client abuse.
  • Disable introspection in production and configure CORS with an origin whitelist.
  • Serve the API over HTTPS with secure headers.
  • Persisted queries with a whitelist save bandwidth while closing off arbitrary queries.

In the next episode, episode 16, you'll learn about subscriptions for real-time features — the subscription concept, WebSocket transport with graphql-ws, Apollo 4 server setup, the PubSub pattern, Redis-based PubSub for production, subscription security, and client integration with Apollo Client. Your API will start "speaking" in real time!

Learn GraphQL - Securing GraphQL APIs from Threats | Learn GraphQL