Skip to content

GraphQL Security — Depth Limiting, Cost Analysis, and Protection

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you will learn about Graphql Security. We cover key concepts, practical examples, and best practices to help you master this topic.

GraphQL APIs face unique security challenges — a single malicious query can overload your server through deep nesting, expensive field requests, or introspection attacks.

What You'll Learn

You will learn how to protect GraphQL APIs with depth limiting, query cost analysis, Rate Limiting, introspection control, CSRF protection, and persistent query allowlists.

Why Security Matters

GraphQL's flexibility is also its vulnerability. A client can request deeply nested data (10+ levels), request expensive fields repeatedly, or introspect the entire schema to find weaknesses. Without protection, a single { threats { device { threats { device { ... } } } } } can crash your server. DodaTech's Durga Antivirus Pro implements three layers of GraphQL security — depth limits, cost analysis, and rate limiting — handling 50,000 requests/minute without incident.

flowchart TB
    A["Incoming Query"] --> B{"Depth Limit\n(max 7 levels)"}
    B -->|"Exceeded"| C["Reject: Query too deep"]
    B -->|"OK"| D{"Cost Analysis\n(max 1000)"}
    D -->|"Exceeded"| E["Reject: Query too expensive"]
    D -->|"OK"| F{"Auth Check"}
    F -->|"Unauthorized"| G["Reject: Login required"]
    F -->|"OK"| H["Execute Query"]
    style H fill:#bbf7d0,stroke:#16a34a
    style C fill:#fca5a5,stroke:#dc2626
    style E fill:#fca5a5,stroke:#dc2626
    style G fill:#fca5a5,stroke:#dc2626
â„šī¸ Info

Prerequisites: Apollo Server configuration. Understanding of security concepts.

Query Depth Limiting

const depthLimit = require('graphql-depth-limit');

const server = new ApolloServer({
  typeDefs,
  resolvers,
  validationRules: [
    depthLimit(7, { ignore: [ /^__/, /^internal/ ] }),
  ],
});

// Query depth 3 (safe)
query { threats { device { name } } }

// Query depth 15 (blocked)
query { threats { device { threats { device { threats { device { threats { ... } } } } } } } }
// → Error: Query depth limit of 7 exceeded

Query Cost Analysis

const { costAnalysis } = require('graphql-cost-analysis');

const server = new ApolloServer({
  typeDefs,
  resolvers,
  validationRules: [
    costAnalysis({
      maximumCost: 1000,
      defaultCost: 1,
      costMap: {
        // Expensive fields cost more
        "Device.threats": 10,
        "User.devices": 5,
        "Threat.rawAnalysis": 20,
        "Scan.fullResults": 15,
      },
      onComplete(cost) {
        console.log(`Query cost: ${cost}`);
      },
    }),
  ],
});

// Example cost calculation
// query { devices { threats { analysis } } }
// devices (1) + threats per device (10 per × N) + analysis (20 per)
// = 1 + (10 × N) + (20 × N × M)

Rate Limiting for GraphQL

const rateLimit = require('express-rate-limit');
const { ApolloServer } = require('apollo-server-express');

// IP-based rate limiting
const limiter = rateLimit({
  windowMs: 60 * 1000,  // 1 minute
  max: 100,              // 100 requests per minute
  message: { errors: [{ message: 'Too many requests', extensions: { code: 'RATE_LIMITED' } }] },
});

// More granular: cost-based rate limiting
const costTracker = new Map();

function costBasedRateLimit(context, queryCost) {
  const key = context.user?.id || context.ip;
  const current = costTracker.get(key) || 0;
  const newTotal = current + queryCost;
  
  if (newTotal > 5000) { // Max 5000 cost per minute
    throw new Error('Query budget exceeded');
  }
  
  costTracker.set(key, newTotal);
  setTimeout(() => costTracker.set(key, (costTracker.get(key) || 0) - queryCost), 60000);
}

Introspection Control

const server = new ApolloServer({
  typeDefs,
  resolvers,
  // Disable introspection in production
  introspection: process.env.NODE_ENV !== 'production',
});

// Or: allow introspection only for authenticated admins
const server = new ApolloServer({
  typeDefs,
  resolvers,
  introspection: true, // Still enabled
  plugins: [{
    requestDidStart({ request, context }) {
      // Block introspection queries from non-admins
      if (isIntrospectionQuery(request.query) && context.user?.role !== 'ADMIN') {
        throw new Error('Introspection requires admin role');
      }
    },
  }],
});

Persisted Query Allowlist

// Only allow registered queries (strict mode)
const allowlist = new Set([
  'hash1abc...', // GetThreats
  'hash2def...', // GetDevices
  'hash3ghi...', // CreateDevice
]);

const server = new ApolloServer({
  typeDefs,
  resolvers,
  persistedQueries: {
    ttl: 900,
  },
  plugins: [{
    requestDidStart({ request }) {
      // Reject queries not in the allowlist
      if (process.env.NODE_ENV === 'production') {
        const hash = request.extensions?.persistedQuery?.sha256Hash;
        if (!hash || !allowlist.has(hash)) {
          throw new Error('Query not in allowlist');
        }
      }
    },
  }],
});

CSRF Protection

const server = new ApolloServer({
  typeDefs,
  resolvers,
  csrfPrevention: true, // Requires apollo-require-preflight header
});

// Client must include this header on mutations:
// fetch('/graphql', {
//   method: 'POST',
//   headers: {
//     'Content-Type': 'application/json',
//     'apollo-require-preflight': 'true',  // Required by csrfPrevention
//   },
//   body: JSON.stringify({ query: 'mutation { ... }' }),
// });

Security Headers

const helmet = require('helmet');
const { ApolloServer } = require('apollo-server-express');

app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
    },
  },
  hsts: {
    maxAge: 31536000,
    includeSubDomains: true,
  },
}));

Common Mistakes

1. Leaving Introspection Enabled in Production

Without introspection: false, anyone can query __schema to see all your types, fields, arguments, and deprecated fields — a roadmap for attackers.

2. No Depth Limits

Without depth limiting, a client creates an infinite loop: threats { device { threats { device { ... } } } }. Depth limit of 7-10 prevents this.

3. Ignoring Cost Analysis

Depth limits alone don't prevent expensive queries. A query at depth 3 that requests 1000 devices with expensive fields can still overload the server.

4. Not Implementing Rate Limiting

Without rate limiting, an attacker floods your API with requests. Implement both IP-based and cost-based rate limiting.

5. Using Default CORS Settings

cors({ origin: '*' }) allows any website to make requests to your GraphQL endpoint. Restrict to specific origins.

Practice Questions

  1. What does query depth limiting prevent?
  2. How does cost analysis differ from depth limiting?
  3. Why disable introspection in production?
  4. What is CSRF protection in Apollo Server?
  5. What are persisted query allowlists?

Answers:

  1. Depth limiting prevents deeply nested queries from consuming excessive server resources. A depth of 7-10 catches malicious or accidental infinite nesting.
  2. Depth limiting restricts nesting levels. Cost analysis assigns weights to fields and limits total query cost. Expensive but shallow queries pass depth limits but fail cost analysis.
  3. Introspection exposes every type, field, argument, and directive in your schema. Attackers use this information to find weak points and craft malicious queries.
  4. CSRF prevention requires a custom header (apollo-require-preflight) on mutations. Browsers enforce CORS on custom headers, preventing cross-site request forgery attacks.
  5. Persisted query allowlists restrict the API to a predefined set of queries. Only queries whose hashes are in the allowlist can execute. This is the most restrictive and secure mode.

Challenge: Implement a comprehensive security configuration for DodaTech's GraphQL API. Include: depth limit of 7 with graphql-depth-limit, cost analysis with graphql-cost-analysis (max 1000, with appropriate costs for expensive fields), rate limiting (100 req/min per IP, 5000 cost/min per user), CSRF prevention, disabled introspection in production, helmet security headers, CORS restricted to dashboard domains, and a persisted query allowlist for critical operations.

FAQ

Can depth limiting block legitimate queries?

A depth of 7-10 accommodates almost all legitimate queries while blocking attacks. If you have legitimate deep queries (e.g., nested comments), increase the limit selectively using the ignore option.

How should I set the maximum query cost?

Audit your most expensive legitimate query and set the max to 2-3x that value. Monitor rejected queries and adjust. Start with 1000 and adjust based on metrics.

Is introspection a security risk?

Yes — introspection reveals your entire schema including deprecated fields, internal types, and argument details. Disable in production or restrict to authorized users.

How do I handle rate limiting in GraphQL?

Use two layers: IP-based rate limiting (express-rate-limit) and query cost-based rate limiting (track cost per user per time window). Cost-based limiting catches abusive queries regardless of IP.

What is the safest GraphQL deployment configuration?

Persisted queries + allowlist + depth limit + cost analysis + rate limiting + disabled introspection + CSRF protection + restricted CORS + HTTPS only. This minimizes attack surface.

Mini Project

Implement a security-hardened Apollo Server for DodaTech. Configure all security measures: depth limiting (7), cost analysis (max 1000), rate limiting (100 req/min, 5000 cost/min), CSRF prevention, disabled introspection in production, helmet security headers, CORS with origin whitelist, persisted query allowlist for 10 critical queries, and write security tests that verify each protection blocks attack queries.

What's Next

Topic Description
Performance Query optimization and Caching
Project Full-stack GraphQL application
Testing Testing GraphQL APIs
âŦ… Testing
➡ Performance

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro