Skip to content

VNX-GQL-002: GraphQL query batching enabled without a limit (DoS)

Reports unbounded GraphQL operation batching switched on with a literal true: Apollo allowBatchedHttpRequests, graphql-yoga batching, Mercurius allowBatchedQueries, or batch=True on a Python GraphQLView.

Updated 02 Nov 2026Checked against cli@de9b52a
Field Value
Rule ID VNX-GQL-002
Severity Medium
SARIF level warning
Kind sast
Languages JavaScript / TypeScript, Python
CWE CWE-770
CAPEC CAPEC-469
ATT&CK T1499
CWSS vector CWSS:1.0/TI:M/AP:RU/AL:S/IC:N/FC:LT/RP:RU/RL:S/AV:L/AS:N/IN:A/SC:A/BI:M/DI:H/EX:M/EC:N/P:C
Tags graphql dos resource-exhaustion
Source internal/sast/rules/vnx-gql-002.rego

Overview

This rule detects Apollo Server configured with allowBatchedHttpRequests: true. Query batching allows a client to send an array of multiple GraphQL operations in a single HTTP request. Without this being paired with strict per-operation rate limiting and query depth/complexity controls, an attacker can send hundreds of complex operations in a single request, completely bypassing any HTTP-layer rate limiting and causing denial of service through CPU or database connection exhaustion.

GraphQL’s flexible query language creates DoS attack surfaces that HTTP-layer controls cannot address alone. Both HTTP-level batching and deeply nested single queries can exhaust server resources, this rule targets the former; depth/complexity limits address the latter.

OWASP ASVS v4: V13.4.1, Verify that a query allowlist or a combination of depth limiting and amount limiting is used to prevent GraphQL DoS as a result of expensive, nested queries.

Why This Matters

GraphQL’s flexible query structure creates unique denial-of-service opportunities that HTTP-layer rate limiting cannot fully address:

Batching amplification: With allowBatchedHttpRequests: true, an attacker sends a JSON array of 50 operations in a single HTTP request. Your load balancer counts one request; your server executes 50 expensive resolver chains. Standard rate limiting (requests per minute per IP) is bypassed entirely.

Nested query exhaustion: A maliciously nested query can cause exponential resolver execution even without batching:

# This single query can lock up your event loop or exhaust your DB connection pool
{
  user {
    friends {
      friends {
        friends {
          friends {
            posts {
              comments {
                author {
                  friends { id }
                }
              }
            }
          }
        }
      }
    }
  }
}

Real-world GraphQL DoS attacks have taken production services offline within seconds because the default GraphQL execution engine has no built-in query complexity limits. The OWASP GraphQL Cheat Sheet explicitly calls out both batching and depth limiting as required controls.

What Gets Flagged

// FLAGGED: batching enabled without compensating controls
const server = new ApolloServer({
  typeDefs,
  resolvers,
  allowBatchedHttpRequests: true,  // <-- flagged
});

// FLAGGED: graphql-yoga and Mercurius
const yoga = createYoga({ schema, batching: true });
app.register(mercurius, { schema, resolvers, allowBatchedQueries: true });
# FLAGGED: batch=True on a GraphQLView (or a subclass's) as_view()
app.add_url_rule('/graphql', view_func=GraphQLView.as_view('graphql', schema=schema, batch=True))

# FLAGGED: batch = True as a class attribute of a GraphQLView subclass
class BatchedGraphQLView(GraphQLView):
    batch = True

Not flagged: false or an environment-derived value, bounded batching (batching: { limit: 5 }), batching: true on a non-GraphQL object or nested in another key (transport: { batching: true }), DataLoader(fn, batch=True) beside a view, and test files. A missing depth or complexity limit is not reported: it cannot be judged from one file.

Remediation

Most applications do not need HTTP-level query batching. The default in Apollo Server is false:

// SAFE: batching disabled (default behaviour)
const server = new ApolloServer({
  typeDefs,
  resolvers,
  // allowBatchedHttpRequests omitted, defaults to false
});

Add Query Depth and Complexity Limits (Apollo Server / graphql-js)

Install graphql-depth-limit and a complexity analysis library, then apply them as validation rules:

// Install: npm install graphql-depth-limit graphql-cost-analysis
import depthLimit from 'graphql-depth-limit';
import costAnalysis from 'graphql-cost-analysis';

const server = new ApolloServer({
  typeDefs,
  resolvers,
  allowBatchedHttpRequests: false,
  validationRules: [
    depthLimit(7),
    costAnalysis({ maximumCost: 1000, defaultCost: 1 }),
  ],
});

Graphene (Python)

Use the built-in depth_limit_validator from graphene.validation:

from graphene_django.views import GraphQLView
from graphene.validation import depth_limit_validator

class DepthLimitedGraphQLView(GraphQLView):
    def get_validation_rules(self):
        return [depth_limit_validator(max_depth=10)]

# In urls.py
urlpatterns = [
    path('graphql/', DepthLimitedGraphQLView.as_view(graphiql=False)),
]

graphql-java

Use MaxQueryDepthInstrumentation and MaxQueryComplexityInstrumentation:

GraphQL graphQL = GraphQL.newGraphQL(schema)
    .instrumentation(new ChainedInstrumentation(Arrays.asList(
        new MaxQueryDepthInstrumentation(10),
        new MaxQueryComplexityInstrumentation(200)
    )))
    .build();

graphql-java ships with a default maximum depth of 20. Set this explicitly to a value appropriate for your schema rather than relying on the default.

gqlgen (Go)

// In graph/schema.resolvers.go, add complexity limits via handler config
import "github.com/99designs/gqlgen/graphql/handler/extension"

srv := handler.NewDefaultServer(generated.NewExecutableSchema(cfg))
srv.Use(extension.FixedComplexityLimit(300))

If You Must Enable Batching

If batching is required for a trusted internal client (e.g., a BFF layer), apply all of the following compensating controls:

  • Count each operation in a batch separately against rate limits
  • Set a maximum number of operations per batch (e.g., no more than 10)
  • Apply query depth and complexity limits to every operation in the batch
  • Restrict batching to authenticated sessions only
const server = new ApolloServer({
  typeDefs,
  resolvers,
  allowBatchedHttpRequests: true,
  validationRules: [depthLimit(7), costAnalysis({ maximumCost: 500 })],
  plugins: [
    {
      async requestDidStart({ request }) {
        // Reject batches larger than 10 operations
        if (Array.isArray(request.body) && request.body.length > 10) {
          throw new Error('Batch size limit exceeded');
        }
      },
    },
  ],
});

References

Suppressing this rule

Put a nosec comment on the line, or on line 1 to cover the whole file. Use the comment marker of the file’s language (#, //, -- or ;):

# nosec vnx-gql-002
// nosec vnx-gql-002

Or record an ignore rule: vulnetix ignore add --rule vnx-gql-002 --reason "...". See Suppressing findings.

Edit this pageSources: cli/internal/sast/rules/vnx-gql-002.rego, cli/internal/sast/docs/vnx-gql-002.md
esc
↑ ↓ move↵ openSearches in your browser. A key like autonomy.max or a code like threshold_required jumps straight to it.