Referencia Detallada de Seguridad GraphQL
Asegurar APIs GraphQL contra leaks de introspection, ataques de profundidad, DoS por costo, abuso de batching e inyeccion. Cubre patrones de auth, rate limiting y hardening de produccion.
Nota para desarrolladores hispanohablantes: Esta guía incluye ejemplos y convenciones de nomenclatura adaptadas a equipos que trabajan en español. Cuando existen diferencias significativas en terminología técnica entre el inglés y el español, se indican explícitamente para facilitar la comunicación en equipos multiculturales.
Introducción
Las APIs GraphQL son poderosas para los clientes y peligrosas para los atacantes. Un solo endpoint acepta queries arbitrarias, lo que significa que el servidor debe defenderse contra queries profundamente anidadas, excesivamente complejas, o disenadas para extraer datos sensibles. Las APIs REST limitan la exposicion a traves de endpoints fijos; GraphQL expone el esquema completo y deja a los clientes construir cualquier query. Lo siguiente es una guia practica para la superficie de ataque y las defensas que necesitas en produccion.
Superficie de Ataque
Las APIs GraphQL enfrentan estas amenazas principales:
- Leaks de introspection: El esquema revela todos los tipos, campos, y relaciones
- Ataques de profundidad de query: Queries profundamente anidadas causan llamadas recursivas a resolvers
- DoS basado en costo: Queries costosas (muchos campos, listas grandes) consumen recursos del servidor
- Abuso de batching: Los clientes envian muchas queries en una request para bypassar rate limits
- Inyeccion: Input del usuario pasado a resolvers llega a bases de datos o servicios externos
- Divulgacion de informacion: Mensajes de error revelan estructura interna o datos
Control de Introspection
Deshabilitar en Producción
Introspection deja a cualquiera descubrir tu esquema entero. En produccion, deshabilitalo.
import { ApolloServer } from "@apollo/server";
const server = new ApolloServer({
typeDefs,
resolvers,
introspection: false, // Deshabilitar en produccion
});
Habilitar Solo para Usuarios Autorizados
Si necesitas introspection para herramientas internas, protejela detras de autenticacion.
const server = new ApolloServer({
typeDefs,
resolvers,
introspection: process.env.NODE_ENV !== "production",
plugins: [
{
async requestDidForOperation(requestContext) {
if (requestContext.operation?.operation === "query") {
const query = requestContext.request.query || "";
if (query.includes("__schema") || query.includes("__type") ) {
const user = requestContext.contextValue.user;
if (!user || user.role !== "ADMIN") {
throw new Error("Introspection is disabled");
}
}
}
},
},
],
});
Persisted Queries como Alternativa
En lugar de exponer introspection, usa persisted queries. Los clientes envian un hash de query en lugar de la query completa. El servidor mapea el hash a una query pre-registrada. Los clientes nunca ven el esquema completo.
import { ApolloServer } from "@apollo/server";
import { ApolloServerPluginCacheControl } from "@apollo/server/plugin/cacheControl";
const server = new ApolloServer({
typeDefs,
resolvers,
introspection: false,
allowBatchedHttpRequests: false,
});
Limitación de Profundidad de Query
Una query como user { posts { author { posts { author { ... } } } } } puede recursar indefinidamente. Cada nivel dispara llamadas a resolvers y queries de base de datos. Limita la profundidad maxima.
import depthLimit from "graphql-depth-limit";
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [depthLimit(10)], // Max 10 niveles de profundidad
});
Elegir un Limite de Profundidad
Analiza tu esquema para determinar la profundidad legitima maxima. Un esquema tipico de e-commerce podria necesitar:
query { users { orders { items { product { category { name } } } } } }= profundidad 6
Setea el limite a la profundidad legitima maxima mas un margen pequeno (ej., 10 para una profundidad legitima maxima de 7).
Análisis de Costo de Query
Limitar la profundidad no es suficiente. Una query superficial con 100 campos o una lista con 1000 items puede ser costosa. El analisis de costo asigna un costo a cada campo y rechaza queries que exceden un presupuesto.
Cálculo de Costo Estático
import costAnalysis from "graphql-cost-analysis";
const costAnalyzer = costAnalysis({
maximumCost: 1000,
variables: {},
createCost: 1, // Costo de una mutacion create
readCost: 1, // Costo de leer un campo
updateCost: 1, // Costo de una mutacion update
deleteCost: 1, // Costo de una mutacion delete
complexityCost: 2, // Costo de un campo con argumentos
listMultiplier: 10, // Multiplicador para campos de lista
onSuccess: (cost) => console.log(`Query cost: ${cost}`),
onError: (cost, maximumCost) => {
throw new Error(`Query cost ${cost} exceeds maximum ${maximumCost}`);
},
});
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [costAnalyzer],
});
Directivas de Costo por Campo
Anota campos costosos con directivas de costo para que el analizador calcule con precision.
type Query {
users(first: Int = 10): [User!]! @cost(complexity: 2, multipliers: ["first"])
search(term: String!, limit: Int = 20): [SearchResult!]! @cost(complexity: 5, multipliers: ["limit"])
report(startDate: DateTime!, endDate: DateTime!): Report @cost(complexity: 50)
}
Costo Dinámico con @listSize
Para conexiones relay, usa @listSize para estimar el numero de items retornados.
type UserConnection {
edges: [UserEdge!]! @cost(complexity: 1, multipliers: ["first"], useMultipliers: true)
}
Rate Limiting
Rate Limiting por Operación
Limita por nombre de operacion o hash de query, no solo por IP. Esto previene que un cliente inunde el servidor con diferentes queries.
import rateLimit from "express-rate-limit";
const limiter = rateLimit({
windowMs: 60 * 1000, // 1 minuto
max: 100, // 100 requests por minuto por IP
message: "Too many requests",
});
app.use("/graphql", limiter);
Rate Limiting Consciente de Costo
Combina analisis de costo con rate limiting. Rastrea el costo total de queries por cliente, no solo el conteo. Un cliente que envia 10 queries baratas no deberia alcanzar el mismo limite que uno que envia 10 queries costosas.
const clientCosts = new Map(); // clientId -> { cost, resetAt }
function checkCostBudget(clientId, queryCost) {
const now = Date.now();
let entry = clientCosts.get(clientId);
if (!entry || entry.resetAt < now) {
entry = { cost: 0, resetAt: now + 60000 }; // Ventana de 1 minuto
clientCosts.set(clientId, entry);
}
if (entry.cost + queryCost > 5000) {
throw new Error(`Cost budget exceeded: ${entry.cost + queryCost} > 5000`);
}
entry.cost += queryCost;
}
Autenticación y Autorización
Autenticación en Context
Autentica en la capa HTTP, no en el esquema. Extrae el usuario del request y ponlo en el context.
const server = new ApolloServer({
typeDefs,
resolvers,
context: async ({ req }) => {
const token = req.headers.authorization?.replace("Bearer ", "");
if (!token) return { user: null };
const user = await verifyToken(token);
return { user };
},
});
Autorización a Nivel de Campo
Verifica permisos en resolvers. No confies en que el cliente evite consultar campos sensibles.
const resolvers = {
User: {
email: (user, _args, ctx) => {
if (!ctx.user) throw new ForbiddenError("Not authenticated");
if (ctx.user.id !== user.id && ctx.user.role !== "ADMIN") {
throw new ForbiddenError("Not authorized to view email");
}
return user.email;
},
ssn: (user, _args, ctx) => {
if (ctx.user?.role !== "ADMIN") {
throw new ForbiddenError("SSN requires admin access");
}
return user.ssn;
},
},
};
Directivas de Esquema para Autorización
Usa directivas custom para declarar roles requeridos en campos. Esto hace los permisos visibles en el esquema.
type Query {
adminDashboard: AdminDashboard @auth(requires: ADMIN)
userSettings: UserSettings @auth(requires: USER)
}
directive @auth(requires: Role!) on FIELD_DEFINITION
enum Role {
ADMIN
USER
GUEST
}
const { SchemaDirectiveVisitor } = require("@graphql-tools/utils");
const { defaultFieldResolver } = require("graphql");
class AuthDirective extends SchemaDirectiveVisitor {
visitFieldDefinition(field) {
const requiredRole = this.args.requires;
const originalResolve = field.resolve || defaultFieldResolver;
field.resolve = async (root, args, ctx, info) => {
if (!ctx.user) throw new ForbiddenError("Not authenticated");
if (ctx.user.role !== requiredRole && ctx.user.role !== "ADMIN") {
throw new ForbiddenError(`Requires ${requiredRole} role`);
}
return originalResolve(root, args, ctx, info);
};
}
}
Prevención de Abuso de Batching
Clientes GraphQL como Apollo Client batchean multiples queries en una request. Atacantes pueden abusar esto para enviar cientos de queries en una sola HTTP request, bypassando rate limits que cuentan HTTP requests.
Limitar Tamaño de Batch
const server = new ApolloServer({
typeDefs,
resolvers,
allowBatchedHttpRequests: true,
plugins: [{
async requestDidForOperation() {
// Check batch size in context
},
}],
});
// Middleware Express para limitar batch size
app.use("/graphql", (req, res, next) => {
if (Array.isArray(req.body) && req.body.length > 10) {
return res.status(400).json({ error: "Batch size limit: 10 operations" });
}
next();
});
Prevención de Inyección
Inyección SQL via Resolvers
GraphQL no previene inyeccion SQL. Si un resolver pasa input del usuario directamente a una query de base de datos, es vulnerable.
// VULNERABLE: concatenacion de strings
const resolvers = {
Query: {
userByName: (_root, { name }, ctx) => {
return ctx.db.query(`SELECT * FROM users WHERE name = '${name}'`);
},
},
};
// SEGURO: query parametrizada
const resolvers = {
Query: {
userByName: (_root, { name }, ctx) => {
return ctx.db.query("SELECT * FROM users WHERE name = $1", [name]);
},
},
};
Inyección NoSQL
Ten cuidado con objetos pasados a bases de datos NoSQL. Un input de query como { "email": { "$ne": null } } puede matchear todos los documentos.
// VULNERABLE: pasar input crudo a MongoDB
const resolvers = {
Query: {
search: (_root, { filter }, ctx) => {
return ctx.db.users.find(filter).toArray(); // filter podria ser { $where: "true" }
},
},
};
// SEGURO: whitelist de campos y sanitizacion
const resolvers = {
Query: {
search: (_root, { filter }, ctx) => {
const safeFilter = {
name: filter.name,
email: filter.email,
};
Object.keys(safeFilter).forEach(k => safeFilter[k] === undefined && delete safeFilter[k]);
return ctx.db.users.find(safeFilter).toArray();
},
},
};
Manejo de Errores para Seguridad
Ocultar Errores Internos
No expongas stack traces, mensajes de error de base de datos, o paths internos a los clientes. Retorna errores genericos con codigos estructurados.
const server = new ApolloServer({
typeDefs,
resolvers,
formatError: (formattedError, error) => {
// Loggear error completo server-side
console.error(error);
// Retornar error sanitizado al cliente
if (formattedError.extensions?.code === "INTERNAL_SERVER_ERROR") {
return {
message: "Internal server error",
extensions: { code: "INTERNAL_SERVER_ERROR" },
};
}
return formattedError;
},
});
Evitar Leakage de Datos en Errores
No incluyas datos sensibles en mensajes de error. En lugar de “User with email alice@example.com not found”, retorna “User not found”.
Checklist de Hardening de Producción
- Introspection deshabilitada en produccion
- Limite de profundidad de query seteado (ej., 10)
- Analisis de costo de query con presupuesto maximo
- Rate limiting por IP y por operacion
- Autenticacion requerida para todas las queries no publicas
- Autorizacion a nivel de campo en campos sensibles
- Tamaño de batch limitado (ej., 10 operaciones)
- Prevencion de inyeccion SQL/NoSQL (queries parametrizadas, sanitizacion de input)
- Mensajes de error sanitizados (sin stack traces, sin datos internos)
- HTTPS enforceado
- CORS configurado para permitir solo origenes confiables
- Logging de queries sospechosas (alto costo, profundidad alta, intentos de introspection)
- Persisted queries para APIs publicas
- Query allowlist para operaciones de cliente conocidas
Preguntas Frecuentes
¿Debería deshabilitar introspection en producción?
Si. Introspection revela tu esquema entero a cualquiera que pueda enviar una query. Atacantes usan esto para encontrar campos sensibles y construir queries dirigidas. Deshabilita en produccion y usa persisted queries o un schema registry para herramientas internas.
¿Es GraphQL menos seguro que REST?
GraphQL tiene una superficie de ataque mas amplia porque acepta queries arbitrarias. REST limita la exposicion a traves de endpoints fijos. Sin embargo, GraphQL con depth limiting, cost analysis, y auth adecuados es tan seguro como REST. La clave es aplicar los mismos principios de seguridad (auth, rate limiting, validacion de input) a GraphQL.
¿Cómo prevengo que N+1 queries sean explotadas?
Usa DataLoader para batch loading. El analisis de costo captura queries que piden muchos campos de lista. El depth limiting previene anidamiento recursivo. Combinadas, estas defensas previenen la mayoria de los ataques DoS basados en N+1.
¿Qué es un persisted query?
Un persisted query es una query pre-registrada con el servidor. El cliente envia un hash en lugar del texto completo de la query. El servidor busca la query por hash. Esto previene que los clientes envien queries arbitrarias, reduciendo la superficie de ataque a solo operaciones pre-aprobadas.
¿Cómo manejo CORS para GraphQL?
Setea CORS para permitir solo tus origenes de cliente conocidos. No uses Access-Control-Allow-Origin: * con credenciales. Configura origenes especificos en tu middleware del servidor.
import cors from "cors";
app.use("/graphql", cors({
origin: ["https://app.example.com", "https://admin.example.com"],
credentials: true,
}));
¿Debería usar un WAF para GraphQL?
Si. Un Web Application Firewall con soporte GraphQL (como Cloudflare GraphQL Protection o AWS WAF) puede bloquear queries maliciosas antes de que lleguen a tu servidor. Busca WAFs que entiendan tipos de operacion GraphQL, profundidad, y complejidad.
See Also
Recursos Relacionados
Complete Guide to GraphQL Schema Design
Design GraphQL schemas for evolution, performance, and maintainability. Covers type design, connections, mutations, error handling, deprecation, and schema-first vs code-first workflows.
GuideGraphQL vs REST — When to Choose and How to Migrate
A decision guide comparing GraphQL and REST APIs: use cases, performance, caching, tooling, and migration strategies for engineering teams.
PatternGraphQL Interface Polymorphism Pattern
Model polymorphic types with GraphQL interfaces to share field contracts across different object types while keeping resolvers type-specific.
RecipeField-Level Auth with Custom GraphQL Schema Directives
Implement field-level authorization in GraphQL using custom schema directives that check user roles and permissions per field
GuideComplete Guide to GraphQL Caching
Cache GraphQL responses at every layer: CDN, gateway, DataLoader, persisted queries, and client-side. Covers cache keys, invalidation, HTTP caching directives, and Apollo Client cache.
GuideComplete Guide to GraphQL Testing
Test GraphQL APIs at every layer: unit tests for resolvers, integration tests for schema, E2E tests for operations. Covers mocking, fixtures, snapshot testing, and performance testing.