Security & Depth Limits
A GraphQL endpoint is one URL that can return any shape of data — which makes the usual REST-style controls insufficient. Layer the defences: depth and complexity limits, persisted queries in production, field-level authorisation, rate limits with query-cost weighting, and disabling introspection on public schemas you control.
Defence-in-depth for a public GraphQL endpoint
EXAMPLE
// ===== 1) Query depth + complexity =====
// Each field costs something; abusive nesting balloons cost.
import depthLimit from 'graphql-depth-limit';
import { createComplexityLimitRule } from 'graphql-validation-complexity';
const validationRules = [
depthLimit(10), // reject queries deeper than 10 levels
createComplexityLimitRule(1000, {
onCost: (cost) => console.log('query cost:', cost),
formatErrorMessage: (cost) => \`query too complex (${cost})\`,
}),
];
// Apollo Server / Yoga:
const server = new ApolloServer({
schema,
validationRules,
});
// ===== 2) Per-field directives — auth, cost weighting =====
type Query {
me: User @auth
users(limit: Int = 20): [User!]! @auth(roles: ["admin"]) @cost(complexity: 5)
search(query: String!): [Result!]! @cost(complexity: 10, multipliers: ["limit"])
}
// ===== 3) Persisted queries — the strongest control =====
// Clients send a hash; server only executes queries it has seen before.
// Apollo Persisted Queries:
// plugin: ApolloServerPluginPersistedQueries()
// Yoga:
// usePersistedOperations({ getPersistedOperation: (hash) => store.get(hash) })
//
// Effect: ad-hoc abusive queries are rejected because they have no hash.
// Public APIs with self-serve clients can EITHER offer persisted queries OR
// require an auth token + tighter limits.
// ===== 4) Disable introspection in production (where it matters) =====
const server = new ApolloServer({
schema,
introspection: process.env.NODE_ENV !== 'production', // off in prod
});
// Introspection itself is not a vulnerability, but on a CLOSED system it
// reduces the discoverability of internal types. On an OPEN system (you
// publish the schema anyway) leave it on.
// ===== 5) Per-user rate limit, cost-weighted =====
// Pseudo-code:
// const cost = computeCost(parsedQuery);
// if (await bucket.consume(user.id, cost) > budget) return 429;
// You charge cheap queries differently from expensive ones, which is the
// whole point of having complexity weights.
// ===== 6) Pagination boundaries enforced server-side =====
type Query {
orders(first: Int = 20, after: String): OrderConnection!
}
const resolvers = {
Query: {
orders: (_, { first, after }) => {
const safeFirst = Math.min(Math.max(first, 1), 100); // CAP at 100
return paginate(db.orders, safeFirst, after);
},
},
};
// ===== 7) Field-level authorisation =====
// Resolvers return different shapes per requester.
const resolvers = {
User: {
email: (parent, _, { user }) => {
if (!user) return null;
if (user.id === parent.id || user.role === 'admin') return parent.email;
throw new ForbiddenError('email not visible');
},
},
};
// ===== 8) DataLoader to defeat N+1 amplification =====
// Without it: a single query with [users.orders.customer] explodes into
// thousands of round trips. WITH it: a few batched queries.
// ===== 9) Disable batching on public endpoints =====
// Apollo's batched HTTP transport lets one POST send N operations.
// On public endpoints prefer per-op rate limits; either disable batching
// or count each batched op against the limit.
// ===== 10) Error hygiene =====
// Map internal exceptions to safe public messages. Stack traces are for logs,
// not for the response.
const server = new ApolloServer({
schema,
formatError: (formatted, err) => {
console.error(err);
return { message: formatted.message, extensions: { code: formatted.extensions?.code } };
},
});
Why it matters
Persisted queries are the single biggest GraphQL security win you can deploy. They flip the model from "the client can send anything" to "the client can send these N approved queries" — making depth attacks, introspection-then-exploit chains, and accidental N+1 explosions all impossible by construction.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
// Limit query depth, complexity, batching. // Use persisted queries; disable introspection in prod.Try it Yourself »
Discussion
Loading…