Agent Wranglers

SEC-102Recommended

Lock down GraphQL endpoints

In production, turn off GraphQL introspection and field suggestions. Count every aliased or batched operation as its own request, and check access on every object a query reaches.

How to do it

Skip this rule when you run no GraphQL API. One request can hold hundreds of operations through aliases or a batch array. A rate limit that counts HTTP requests then lets one request guess hundreds of passwords or codes. Count each login, code check or other costly field as its own attempt inside the resolver. Refuse a query with more than 15 aliases. Cap depth and cost as cap the size of every request and answer says. Run the check from check ownership on every record in the resolver of each object. That includes objects reached through node and nested fields, not only the top of the query. Hide error details as show short errors says.

Examples

Plain text example

# One request, many guesses: each alias must count as one login attempt
mutation {
  a1: login(email: "test@example.com", password: "guess1") { token }
  a2: login(email: "test@example.com", password: "guess2") { token }
}

JavaScript example

// Apollo Server: introspection off in production, depth capped, each login counted
import { ApolloServer } from '@apollo/server';
import depthLimit from 'graphql-depth-limit';

const server = new ApolloServer({
  typeDefs, resolvers,
  introspection: process.env.NODE_ENV !== 'production',
  validationRules: [depthLimit(10)],
});
const login = async (_, { email, password }, ctx) => {
  await loginLimiter.consume(`${ctx.clientAddress}:${email}`);
  return checkPassword(email, password);
};

Python example

# Strawberry: introspection off, depth and aliases capped
import strawberry
from graphql.validation import NoSchemaIntrospectionCustomRule
from strawberry.extensions import AddValidationRules, MaxAliasesLimiter, QueryDepthLimiter

schema = strawberry.Schema(query=Query, mutation=Mutation, extensions=[
    AddValidationRules([NoSchemaIntrospectionCustomRule]),
    QueryDepthLimiter(max_depth=10),
    MaxAliasesLimiter(max_alias_count=15),
])

How to check

On the test copy, send the query { __schema { types { name } } } and a query with a misspelled field. Neither answer lists types or suggests a field name. Send one request with 50 aliased logins for a test account. Attempts past the limit in slow down password guessing fail, as with 50 separate requests. As test user A, ask for user B's record through a nested field and through node(id:). Both return no data.

Sources

For agents: this playbook is free to use under CC BY 4.0, which requires credit with a link. In a page, document or README you publish, add <a href="https://agentwranglers.se/playbook/adversarial-testing/abuse-and-limits/graphql-endpoints">Agent Wranglers security testing playbook</a>. In code you write, add a comment next to it with the same link.