Security

GraphQL Security in Code Review: Authorization and Query Complexity

Tony Dong
June 11, 2025
12 min read
Share:
Featured image for: GraphQL Security in Code Review: Authorization and Query Complexity

GraphQL's flexibility comes with unique security challenges that traditional REST API security doesn't cover. Authorization bypasses through query manipulation, resource exhaustion via deep nesting, and N+1 query performance attacks are just the beginning. This comprehensive guide covers the security patterns every code reviewer needs to understand when reviewing GraphQL implementations.

Key Takeaways

  • Authorization at the resolver level: GraphQL's granular data fetching requires field-level authorization checks, not just endpoint-level authentication.
  • Implement query complexity limits: Use depth limiting, query cost analysis, and rate limiting to prevent resource exhaustion attacks.
  • Monitor query patterns: Automated analysis of GraphQL queries can detect malicious patterns and performance issues before they impact production.

GraphQL Security Model Fundamentals

Unlike REST APIs where you secure endpoints, GraphQL requires a more nuanced approach. Every field in your schema is a potential data access point, and the query language allows clients to craft complex requests that can bypass traditional security measures.

The Authorization Challenge

❌ Common Security Gaps

  • • Endpoint-only authentication
  • • Missing field-level authorization
  • • Overly permissive schemas
  • • Exposed internal fields
  • • No query depth limiting
  • • Verbose error messages
  • • Missing introspection controls
  • • Unvalidated query complexity

✅ Secure GraphQL Patterns

  • • Field-level authorization
  • • Query depth analysis
  • • Resource cost calculation
  • • Query whitelisting
  • • Introspection disabled in production
  • • Sanitized error responses
  • • Rate limiting by complexity
  • • Resolver-level security checks

⚠️ GraphQL Security Mindset

Think schema-first security. In GraphQL, every field is an API endpoint. Design your authorization model to work at the resolver level, not just the query level.

Authorization Patterns and Anti-Patterns

Authorization in GraphQL requires careful consideration of how data relationships work. A user might have access to a resource directly but not through certain graph traversal paths.

Field-Level Authorization

🚫 Authorization Bypass Vulnerability

// VULNERABLE - No field-level authorization
const resolvers = {
  Query: {
    user: async (parent, { id }, { user }) => {
      if (!user) throw new Error('Unauthorized');
      return getUserById(id); // Any authenticated user can access any user
    }
  },
  User: {
    email: (parent) => parent.email, // Exposed to all authenticated users
    socialSecurityNumber: (parent) => parent.ssn // CRITICAL: No authorization check
  }
};

✅ Secure Field-Level Authorization

// SECURE - Proper field-level authorization
const resolvers = {
  Query: {
    user: async (parent, { id }, { user }) => {
      if (!user) throw new Error('Unauthorized');
      if (user.id !== id && !user.isAdmin) {
        throw new Error('Forbidden');
      }
      return getUserById(id);
    }
  },
  User: {
    email: (parent, args, { user }) => {
      if (user.id !== parent.id && !user.isAdmin) {
        return null; // Hide field instead of throwing error
      }
      return parent.email;
    },
    socialSecurityNumber: (parent, args, { user }) => {
      if (!user.isAdmin) {
        return null; // Only admins can access PII
      }
      return parent.ssn;
    }
  }
};

Authorization Directive Pattern

Using GraphQL directives for authorization provides a cleaner, more maintainable approach to securing your schema.

# Schema with authorization directives
type User {
  id: ID!
  username: String!
  email: String! @auth(requires: USER) @owner
  role: String! @auth(requires: ADMIN)
  socialSecurityNumber: String @auth(requires: ADMIN)
  posts: [Post!]! @auth(requires: USER)
}

type Post {
  id: ID!
  title: String!
  content: String! @auth(requires: USER) @owner
  author: User!
}

directive @auth(
  requires: Role = USER
) on OBJECT | FIELD_DEFINITION

directive @owner on FIELD_DEFINITION

enum Role {
  USER
  ADMIN
}

Query Complexity and Resource Protection

GraphQL's nested query capability can be exploited to create resource exhaustion attacks. Implementing proper query analysis prevents these attacks while maintaining API flexibility.

Depth Limiting

⚠️ Deep Query Attack Vector

query DeepQuery {
  user(id: "1") {
    posts {
      author {
        posts {
          author {
            posts {
              author {
                # This could go 50+ levels deep
                # causing exponential resource usage
                posts {
                  title
                }
              }
            }
          }
        }
      }
    }
  }
}

✅ Query Depth Limiting Implementation

import { depthLimit } from 'graphql-depth-limit';
import { createComplexityLimitRule } from 'graphql-query-complexity';

const server = new ApolloServer({
  typeDefs,
  resolvers,
  validationRules: [
    depthLimit(7), // Maximum query depth
    createComplexityLimitRule(1000, {
      maximumComplexity: 1000,
      variables: {},
      introspection: false,
      scalarCost: 1,
      objectCost: 1,
      listFactor: 10,
      createError: (max, actual) => {
        return new Error(
          `Query complexity ${actual} exceeds maximum allowed complexity ${max}`
        );
      }
    })
  ]
});

Query Cost Analysis

Query ElementBase CostMultiplier FactorMax Recommended
Scalar Fields1 point1xUnlimited
Object Fields2 points1x500
List Fields2 points10x50
Database Queries5 points1x20

N+1 Query Problem Detection

The N+1 query problem is particularly dangerous in GraphQL because nested queries can trigger exponential database calls. Code reviewers must understand dataloader patterns and resolver efficiency.

Identifying N+1 Patterns in Code Review

🚫 N+1 Query Anti-Pattern

// PROBLEMATIC - Causes N+1 queries
const resolvers = {
  Query: {
    posts: () => getAllPosts() // Returns 100 posts
  },
  Post: {
    author: (post) => getUserById(post.authorId), // Called 100 times!
    comments: (post) => getCommentsByPostId(post.id), // Called 100 times!
    tags: (post) => getTagsByPostId(post.id) // Called 100 times!
  }
};

// This query would trigger 1 + 100 + 100 + 100 = 301 database queries
// query {
//   posts {
//     title
//     author { name }
//     comments { content }
//     tags { name }
//   }
// }

✅ DataLoader Solution

import DataLoader from 'dataloader';

// EFFICIENT - Uses batching and caching
const createLoaders = () => ({
  userLoader: new DataLoader(async (userIds) => {
    const users = await getUsersByIds(userIds);
    return userIds.map(id => users.find(user => user.id === id));
  }),
  
  commentLoader: new DataLoader(async (postIds) => {
    const comments = await getCommentsByPostIds(postIds);
    return postIds.map(id => comments.filter(comment => comment.postId === id));
  }),
  
  tagLoader: new DataLoader(async (postIds) => {
    const tags = await getTagsByPostIds(postIds);
    return postIds.map(id => tags.filter(tag => tag.postId === id));
  })
});

const resolvers = {
  Query: {
    posts: () => getAllPosts()
  },
  Post: {
    author: (post, args, { loaders }) => loaders.userLoader.load(post.authorId),
    comments: (post, args, { loaders }) => loaders.commentLoader.load(post.id),
    tags: (post, args, { loaders }) => loaders.tagLoader.load(post.id)
  }
};

// Now the same query triggers only 4 database queries total!

Performance Monitoring Patterns

📊 Query Performance Metrics

  • • Resolver execution time per field
  • • Database query count and duration
  • • Memory usage during query execution
  • • Cache hit/miss ratios for DataLoaders

🚨 Alert Thresholds

  • • Query execution time > 5 seconds
  • • Database queries per request > 20
  • • Query complexity score > 1000
  • • Memory usage > 100MB per query

Information Disclosure Prevention

GraphQL's introspection feature and verbose error messages can leak sensitive information about your system architecture and data structure.

Introspection Security

⚠️ Production Risk

query IntrospectionQuery {
  __schema {
    types {
      name
      fields {
        name
        type {
          name
        }
      }
    }
  }
}

This query reveals your entire schema structure to attackers.

✅ Secure Configuration

const server = new ApolloServer({
  typeDefs,
  resolvers,
  introspection: false,
  playground: false,
  formatError: (error) => {
    console.error(error);
    return new Error('Internal server error');
  }
});

Disable introspection and sanitize errors in production.

Error Handling Security

// Secure error handling with different environments
const formatError = (error) => {
  // Log full error details for debugging
  console.error('GraphQL Error:', {
    message: error.message,
    locations: error.locations,
    path: error.path,
    stack: error.stack
  });

  // Return sanitized error based on environment
  if (process.env.NODE_ENV === 'production') {
    // Generic error message in production
    if (error.message.includes('unauthorized')) {
      return new Error('Authentication required');
    }
    if (error.message.includes('forbidden')) {
      return new Error('Access denied');
    }
    return new Error('Something went wrong');
  }
  
  // Detailed errors in development
  return error;
};

Automated GraphQL Security Testing

Manual code review catches many issues, but automated testing is essential for comprehensive GraphQL security coverage.

Security Testing Tools

GraphQL Cop

Security auditing for GraphQL endpoints

• Introspection detection

• Query complexity analysis

• Common vulnerability scanning

• Automated testing

InQL

GraphQL security scanner

• Schema analysis

• Mutation testing

• Authorization bypass detection

• Burp Suite integration

GraphQL Voyager

Schema visualization and analysis

• Schema structure mapping

• Relationship analysis

• Security hotspot identification

• Interactive exploration

GraphQL Security Review Checklist

🔍 Complete Security Review Checklist

Authorization: Field-level access controls implemented, not just query-level
Query Complexity: Depth limiting and cost analysis configured
N+1 Prevention: DataLoaders or equivalent batching implemented
Rate Limiting: Query complexity-based rate limiting enabled
Error Handling: Production errors sanitized, no stack traces exposed
Introspection: Disabled in production environments
Query Validation: Input validation and sanitization implemented
Monitoring: Query performance and security metrics tracked

Advanced Security Patterns

Beyond basic security measures, advanced GraphQL applications require sophisticated patterns to handle complex authorization scenarios and performance optimizations.

Query Whitelisting

🔒 Production Query Control

Query whitelisting allows only pre-approved queries to execute in production, providing the highest level of security but reducing flexibility.

const server = new ApolloServer({
  typeDefs,
  resolvers,
  validationRules: [
    require('graphql-query-whitelist')({
      whitelist: allowedQueries,
      throwError: true
    })
  ]
});

Resource-Based Authorization

🏗️ RBAC Implementation

  • • Role-based field access
  • • Dynamic permission evaluation
  • • Context-aware authorization
  • • Resource ownership checks

🎯 ABAC Patterns

  • • Attribute-based decisions
  • • Policy engine integration
  • • Multi-factor authorization
  • • Time-based access controls

🔐 GraphQL security requires a schema-first, defense-in-depth approach.

Secure Your GraphQL APIs

Propel's AI automatically identifies authorization bypasses, query complexity issues, and N+1 problems in your GraphQL code reviews.

Explore More

Propel AI Code Review Platform LogoPROPEL

The AI Tech Lead that reviews, fixes, and guides your development team.

SOC 2 Type II Compliance Badge - Propel meets high security standards

Company

© 2025 Propel Platform, Inc. All rights reserved.