Here is an example of Node.js code that is vulnerable to GraphQL introspection and query depth abuse:
🥺 Vulnerable Code
// Vulnerable: full schema exposed, unlimited query depth and cost
const server = new ApolloServer({
typeDefs,
resolvers,
introspection: true,
});
// An attacker can then run:
// { users { posts { author { posts { author { posts { title } } } } } } }Introspection hands over the complete schema, including the internal mutations and fields that never appear in the public client. With that map, a deeply nested or aliased query multiplies into millions of resolver calls and database round trips, so one HTTP request takes the API down. Aliases and batched operations also turn a single request into thousands of login or lookup attempts, which walks straight past a per-request rate limit.
😎 Secure Code
Here is a version of the same code that is secured against GraphQL introspection and query depth abuse:
const depthLimit = require("graphql-depth-limit");
const { createComplexityLimitRule } = require("graphql-validation-complexity");
const isProd = process.env.NODE_ENV === "production";
const server = new ApolloServer({
typeDefs,
resolvers,
introspection: !isProd,
validationRules: [
depthLimit(7),
createComplexityLimitRule(1000),
],
allowBatchedHttpRequests: false,
formatError: (error) => ({
message: isProd ? "Request failed" : error.message,
}),
});Introspection and the playground are development conveniences, so they are off in production, and the depth and complexity rules reject an abusive document before a single resolver runs. Turning off HTTP batching removes the alias-and-batch brute force trick. None of this replaces authorization: check permissions inside each resolver against the authenticated user, because hiding a field from the schema is not the same as protecting the data behind it.