DEV Community

Cover image for GraphQL Security: Protecting Queries and Mutations

GraphQL Security: Protecting Queries and Mutations

GraphQL's flexibility is powerful—until attackers exploit it. Without proper defenses, queries can expose the entire schema, mutations can bypass validation, and resolvers can leak sensitive data. Securing GraphQL means defending at every layer: schema, query, and resolver.


1. Validate and Limit Query Complexity

Unrestricted queries invite query bombs—attackers craft deeply nested queries that exhaust server resources.

import { buildSchema, graphql } from 'graphql';
import { createComplexityLimitMiddleware } from 'graphql-query-complexity';

const middleware = createComplexityLimitMiddleware({
  maximumComplexity: 1000,
  variables: {},
  onComplete: (complexity) => console.log(`Query complexity: ${complexity}`),
  createError: (max, actual) => new Error(`Query too complex: ${actual} > ${max}`)
});

app.use('/graphql', middleware);
Enter fullscreen mode Exit fullscreen mode

2. Authenticate and Authorize Mutations

Every mutation is a write operation. Enforce role-based access and validate permissions before executing.

const resolvers = {
  Mutation: {
    updateUser: (parent, args, context) => {
      if (!context.user) throw new Error('Unauthorized');
      if (context.user.id !== args.userId && context.user.role !== 'ADMIN') {
        throw new Error('Forbidden: Cannot modify other users');
      }
      return db.users.update(args.userId, args.data);
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

3. Hide Sensitive Fields from Schema

Introspection leaks your entire schema. Disable it in production and control field visibility per role.

const apollo = new ApolloServer({
  typeDefs,
  resolvers,
  introspection: process.env.NODE_ENV !== 'production',
  context: ({ req }) => ({
    user: verifyToken(req.headers.authorization),
    isIntrospectionAllowed: process.env.NODE_ENV === 'development'
  })
});
Enter fullscreen mode Exit fullscreen mode

4. Sanitize Resolver Arguments

User input in mutations should be treated as hostile until proven otherwise.

import validator from 'validator';

const resolvers = {
  Mutation: {
    createPost: (parent, args) => {
      const sanitized = {
        title: validator.escape(args.title),
        content: validator.trim(args.content),
        tags: args.tags.map(t => validator.escape(t))
      };
      return db.posts.create(sanitized);
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

5. Rate Limit Per User

Prevent mutations from being used as attack vectors by enforcing per-user rate limits.

import rateLimit from 'graphql-rate-limit';

const rateLimitDirective = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 10
});

const typeDefs = `
  type Mutation {
    createPost(title: String!): Post @rateLimit(window: "15m", limit: 10)
  }
`;
Enter fullscreen mode Exit fullscreen mode

GraphQL's power comes from its precision. That same precision must apply to security.

With proper validation, complexity limits, and role-based access, GraphQL becomes a secure foundation for modern APIs.


Thanks for reading! If this post helped you understand GraphQL security patterns, please share it with your team or leave a comment with your own security wins.

I help teams implement GraphQL architectures that balance flexibility with bulletproof security.

Explore more: kodex.studio

Top comments (0)