High CWE-200 CWE-799 OWASP API4:2023

GraphQL Security in Vibe Coded Apps

How to find and fix introspection, DoS, batching, and injection attacks in GraphQL APIs

Quick Answer

GraphQL APIs have unique attack vectors that REST doesn't. Introspection exposes your schema, nested queries cause DoS, and batching bypasses rate limits. Research found 46,000+ security issues in 1,500 GraphQL endpoints, with 10% critical.

46K+
Issues Found
1,500
APIs Scanned
10%
Critical Severity
5
Attack Vectors

Source: Escape Tech State of GraphQL Security (2024) | OWASP API Security Top 10

What is GraphQL security?

GraphQL security addresses the unique vulnerabilities in GraphQL APIs that don't exist in REST. While REST exposes fixed endpoints, GraphQL lets clients request exactly what they want - which creates new attack surfaces.

Think of REST like a restaurant with a fixed menu - you order item #5 and get what's listed. GraphQL is like a chef who'll cook anything you ask for. More flexible, but also more ways to abuse it if you don't set boundaries.

The OWASP API Security Top 10 (2023) identifies several categories relevant to GraphQL: CWE-799 (Improper Control of Interaction Frequency) for DoS attacks, CWE-200 (Information Disclosure) for introspection leaks, and CWE-89 (SQL Injection) for resolver vulnerabilities.

What are the 5 critical GraphQL attack vectors?

GraphQL has five unique attack vectors that vibe coders need to understand. Each exploits GraphQL's flexibility in a different way.

1. Introspection attacks (CWE-200)

Introspection attacks exploit GraphQL's built-in schema discovery feature. The __schema query returns your entire API structure - every type, field, mutation, and relationship.

Introspection Query
# Attacker sends this to discover your entire schema
{
  __schema {
    types {
      name
      fields {
        name
        type { name }
      }
    }
  }
}

# Response reveals: User, AdminUser, InternalConfig, etc.
# Attackers now know exactly what to target

Even if introspection is disabled, attackers can use field suggestion - sending invalid field names and analyzing error messages that suggest valid alternatives.

2. Deep/nested query DoS (CWE-799)

Nested query attacks send deeply recursive queries that exhaust server resources. Without depth limits, a single malicious query can crash your entire API.

DoS Attack Query
# Single query that crashes your server
query {
  user(id: "1") {
    friends {
      friends {
        friends {
          friends {
            # ... 100 levels deep
            # Each level multiplies database queries
          }
        }
      }
    }
  }
}

# Result: Database overwhelmed, API crashes

Circular fragments are equally dangerous - they reference themselves, creating infinite recursion.

3. Batching attacks

Batching attacks bypass rate limiting by sending thousands of operations in a single HTTP request. Your rate limiter sees one request; the server processes 10,000 operations.

Batching Attack
// Single HTTP request with 10,000 login attempts
[
  {"query": "mutation { login(email:\"[email protected]\", pass:\"pass1\") { token } }"},
  {"query": "mutation { login(email:\"[email protected]\", pass:\"pass2\") { token } }"},
  {"query": "mutation { login(email:\"[email protected]\", pass:\"pass3\") { token } }"},
  // ... 9,997 more attempts
]

// Rate limiter: "1 request, looks fine!"
// Server: processes all 10,000 login attempts

This enables brute force attacks on passwords, OTPs, and any mutation with predictable inputs.

4. Injection via resolvers (CWE-89)

SQL injection and NoSQL injection happen in GraphQL resolvers just like REST endpoints. The difference is that GraphQL's flexibility makes it easier to inject malicious input.

Resolver Injection
// Vulnerable resolver
const resolvers = {
  Query: {
    user: async (_, { id }) => {
      // VULNERABLE: Template literal in SQL
      return db.query(`SELECT * FROM users WHERE id = '${id}'`)
    }
  }
}

// Attack query
query {
  user(id: "' OR '1'='1") {
    email
    password_hash
  }
}

// Result: Returns ALL users in database

5. Authorization bypass (BOLA/IDOR)

Broken access control in GraphQL often happens at the field level. Resolvers check authentication but skip authorization - letting users access other users' data.

Authorization Bypass
// Vulnerable resolver - checks auth but not authorization
const resolvers = {
  Query: {
    user: async (_, { id }, context) => {
      if (!context.user) throw new Error('Not authenticated')
      // MISSING: Check if context.user.id === id
      return getUserById(id)  // Returns ANY user
    }
  }
}

// Logged in as user 123, request user 456's data
query {
  user(id: "456") {
    email
    ssn
    bankAccount
  }
}

Why do AI tools generate vulnerable GraphQL code?

AI tools generate insecure GraphQL APIs because they optimize for "working code" not "secure code." When you ask for a GraphQL API, the AI focuses on functionality - schemas, resolvers, mutations that work.

Common AI-generated vulnerable patterns

// What Cursor, Bolt, Claude Code typically generate:

const server = new ApolloServer({
  typeDefs,
  resolvers,
  // introspection: true (default - VULNERABLE)
  // No depth limits (VULNERABLE)
  // No complexity limits (VULNERABLE)
  // No batch limits (VULNERABLE)
})

// Resolvers without authorization:
user: async (_, { id }) => getUserById(id)  // No auth check

// Resolvers with injection:
users: async (_, { search }) => {
  return db.query(`SELECT * FROM users WHERE name LIKE '%${search}%'`)
}

Why this happens:

  • Developer experience priority: Introspection enabled by default for GraphQL tooling
  • No security in training data: Most GraphQL tutorials skip security configuration
  • Complexity of proper auth: Field-level authorization requires significant boilerplate
  • Focus on happy path: AI generates code that works, not code that's hardened

What could happen if my GraphQL API is vulnerable?

Vulnerable GraphQL APIs expose you to multiple attack scenarios, each with serious consequences.

  • Complete schema exposure: Attackers map your entire data model, finding hidden admin fields and internal types
  • API denial of service: A single malicious query crashes your entire API, affecting all users
  • Brute force attacks: Batching bypasses rate limits, enabling password and OTP cracking
  • Database compromise: Injection through resolvers leads to full database access
  • Data theft: Authorization bypass lets attackers access any user's sensitive data
  • User enumeration: Error message differences reveal which users exist

How do I detect GraphQL vulnerabilities?

Detect GraphQL security issues by testing each of the five attack vectors against your API. Start with introspection, then check for missing limits.

Detection Tests
# 1. Test introspection (should return error in production)
curl -X POST your-api.com/graphql \
  -H "Content-Type: application/json" \
  -d '{"query": "{ __schema { types { name } } }"}'

# 2. Test depth limits (should be rejected)
# Send a 50-level nested query

# 3. Test batching (should be limited)
# Send array of 100+ queries in single request

# 4. Test authorization
# As user A, request user B's data

# 5. Check resolver code for:
# - Template literals in database queries
# - Missing context.user checks

Don't want to test manually?

Scan your GraphQL API

How do I secure my GraphQL API?

Secure your GraphQL API by implementing protections for all five attack vectors. Use the AI fix prompt below or apply manual fixes per framework.

AI Fix Prompt

Copy this prompt into Cursor, Claude Code, or Bolt to automatically secure your GraphQL API:

Copy-paste this prompt
Secure my GraphQL API against all 5 attack vectors. ## What to look for Search for these security issues in my GraphQL code: 1. Introspection enabled in production: - ApolloServer without introspection: false - graphql-yoga without disabled introspection - Any GraphQL server in production mode 2. Missing depth limits: - No graphql-depth-limit or similar - No validationRules array - Schemas with recursive types (User -> friends -> User) 3. Missing complexity limits: - No graphql-query-complexity or graphql-validation-complexity - No cost calculation on fields - List fields without pagination limits 4. Unrestricted batching: - allowBatchedHttpRequests: true without limits - No per-operation rate limiting - Express/Apollo accepting array queries 5. Resolver vulnerabilities: - Template literals in database queries - Missing context.user checks - No field-level authorization ## How to fix ### Disable introspection in production (Apollo Server): ```typescript import { ApolloServer } from '@apollo/server'; const server = new ApolloServer({ typeDefs, resolvers, introspection: process.env.NODE_ENV !== 'production', }); ``` ### Add depth and complexity limits: ```typescript import depthLimit from 'graphql-depth-limit'; import { createComplexityLimitRule } from 'graphql-validation-complexity'; const server = new ApolloServer({ typeDefs, resolvers, validationRules: [ depthLimit(10), createComplexityLimitRule(1000, { scalarCost: 1, objectCost: 5, listFactor: 10, }), ], }); ``` ### Limit batching: ```typescript const server = new ApolloServer({ typeDefs, resolvers, allowBatchedHttpRequests: true, }); // Add middleware to limit batch size app.use('/graphql', (req, res, next) => { if (Array.isArray(req.body) && req.body.length > 5) { return res.status(400).json({ error: 'Batch size limit exceeded' }); } next(); }); ``` ### Add field-level authorization: ```typescript import { GraphQLError } from 'graphql'; const resolvers = { Query: { user: async (_, { id }, context) => { if (!context.user) { throw new GraphQLError('Unauthenticated', { extensions: { code: 'UNAUTHENTICATED' }, }); } if (context.user.id !== id && !context.user.isAdmin) { throw new GraphQLError('Forbidden', { extensions: { code: 'FORBIDDEN' }, }); } return getUserById(id); }, }, }; ``` ### Fix resolver injection: ```typescript // Before (vulnerable) users: async (_, { search }) => { return db.query(`SELECT * FROM users WHERE name LIKE '%${search}%'`) } // After (secure) - Use parameterized queries users: async (_, { search }) => { return db.query('SELECT * FROM users WHERE name LIKE $1', [`%${search}%`]) } ``` ## Install required packages ```bash npm install graphql-depth-limit graphql-validation-complexity ``` ## After fixing 1. Test introspection is disabled in production 2. Verify depth limit rejects nested queries 3. Confirm batch requests are limited 4. Check all resolvers have authorization 5. List all files you modified with before/after snippets Please proceed systematically through my codebase.

Manual fixes by framework

Apollo Server

VULNERABLE
// Default Apollo Server - all vulnerabilities
import { ApolloServer } from '@apollo/server';

const server = new ApolloServer({
  typeDefs,
  resolvers,
  // introspection: true (default)
  // No depth limits
  // No complexity limits
});
SECURE
// Hardened Apollo Server
import { ApolloServer } from '@apollo/server';
import depthLimit from 'graphql-depth-limit';
import { createComplexityLimitRule } from 'graphql-validation-complexity';

const server = new ApolloServer({
  typeDefs,
  resolvers,
  introspection: process.env.NODE_ENV !== 'production',
  validationRules: [
    depthLimit(10),
    createComplexityLimitRule(1000),
  ],
});

Hasura

Hasura has built-in security features. Configure in your console or metadata:

# Disable introspection for non-admin roles
# In hasura metadata or console:
{
  "introspection_options": {
    "disabled_for_roles": ["user", "anonymous"]
  }
}

# Use allowlists in production
# Only pre-approved queries can execute
{
  "allow_list": {
    "enabled": true
  }
}

GraphQL Yoga

import { createYoga, useDisableIntrospection } from 'graphql-yoga';
import { useDepthLimit } from '@envelop/depth-limit';

const yoga = createYoga({
  schema,
  plugins: [
    process.env.NODE_ENV === 'production' && useDisableIntrospection(),
    useDepthLimit({ maxDepth: 10 }),
  ].filter(Boolean),
});

Prevention methods by priority

1

Disable Introspection

Turn off schema discovery in production. Keep enabled for development tooling only.

introspection: process.env.NODE_ENV !== 'production'
2

Add Depth Limits

Prevent nested query DoS by limiting query depth to 10-15 levels maximum.

validationRules: [depthLimit(10)]
3

Add Complexity Limits

Calculate query cost based on fields. Reject queries exceeding your threshold.

createComplexityLimitRule(1000)
4

Limit Batching

Restrict batch size to 5-10 operations. Implement per-operation rate limiting.

if (req.body.length > 5) reject()
5

Field Authorization

Check authorization in every resolver, not just authentication at the gateway.

if (context.user.id !== id) throw Forbidden
6

Parameterized Queries

Never use template literals in database queries. Use parameterized queries.

db.query('...WHERE id = $1', [id])

Frequently asked questions

What are the main security risks with GraphQL?

The five main GraphQL security risks are: introspection attacks (exposing your entire schema), nested query DoS (crashing your API with deep queries), batching attacks (bypassing rate limits), injection via resolvers (SQL/NoSQL injection in database calls), and authorization bypass (accessing data without proper permissions). Research by Escape Tech found 46,000+ security issues across 1,500 GraphQL endpoints, with 10% classified as critical.

Should I disable GraphQL introspection in production?

Yes. Disable introspection in production environments. Introspection exposes your entire API schema including all types, fields, mutations, and their relationships. Attackers use this to understand your data model and find attack vectors. In Apollo Server, set introspection: false for production. Keep it enabled in development for tooling like GraphQL Playground.

How do I prevent GraphQL DoS attacks?

Prevent GraphQL DoS by implementing three controls: query depth limits (max 10-15 levels), query complexity limits (calculate cost based on fields and lists), and rate limiting per operation. Use libraries like graphql-depth-limit and graphql-query-complexity. Also limit batch sizes to prevent batching attacks. A single malicious query without these limits can crash your entire API.

What is a GraphQL batching attack?

A GraphQL batching attack sends multiple operations in a single HTTP request to bypass rate limiting. Attackers bundle thousands of login attempts or OTP guesses into one request, evading per-request rate limits. For example, 10,000 password attempts arrive as one HTTP request. Prevent this by limiting batch sizes to 5-10 operations and implementing per-operation rate limiting, not just per-request.

Is GraphQL more secure than REST?

GraphQL is not inherently more or less secure than REST - it has different security challenges. GraphQL exposes more attack surface through introspection and flexible queries. REST has simpler access control because endpoints map to resources. GraphQL requires field-level authorization, while REST uses endpoint-level. Both need proper authentication, authorization, and input validation.

What is GraphQL introspection and why is it dangerous?

GraphQL introspection is a built-in feature that lets clients query the schema structure using the __schema query. It returns all types, fields, mutations, and their relationships. This is dangerous in production because attackers can map your entire API without documentation. They discover hidden fields, internal types, and potential injection points. Disable it using your GraphQL server settings.

How do I implement rate limiting in GraphQL?

Implement GraphQL rate limiting at the operation level, not just HTTP request level. Use query complexity scoring where each field has a cost. Set maximum complexity per request (e.g., 1000). Limit batch sizes to 5-10 operations. Apply different limits for mutations versus queries. Consider using persisted queries in production to whitelist allowed operations entirely.

How do I secure a GraphQL API?

Secure your GraphQL API with these steps: disable introspection in production, add query depth limits (10-15 max), implement complexity limits, restrict batch sizes, add field-level authorization checks in resolvers, validate all inputs before database queries, use HTTPS, implement proper authentication, and consider persisted queries for public APIs.

Related content

Scan your GraphQL API for vulnerabilities

Check for introspection leaks, missing limits, and resolver injection in your vibe coded GraphQL API.

Try Vibeship Scanner