GraphQL se ha convertido en el estándar de facto para APIs modernas. El 70% de las organizaciones utilizan GraphQL según Wallarm API ThreatStats 2025, y Akamai documentó aproximadamente 150 mil millones de ataques a APIs entre enero de 2023 y diciembre de 2024. Pero la flexibilidad de GraphQL crea una superficie de ataque fundamentalmente diferente a REST.
Escape's State of GraphQL Security 2024 escaneó 160 endpoints públicos de GraphQL y 3,835 operaciones, encontrando 13,720 issues incluyendo 4,527 críticos. El 69% de los servicios eran susceptibles a DoS por consumo unrestricted de recursos.
1. La Diferencia GraphQL vs REST
| Aspecto | REST | GraphQL |
|---|---|---|
| Estructura | Múltiples endpoints fijos | Un solo endpoint /graphql |
| Consulta | El servidor decide qué devolver | El cliente especifica exactamente qué campos quiere |
| Exposición | Endpoints limitados | Todo el esquema potencialmente expuesto |
| Autorización | Por endpoint (fácil de controlar) | Por campo y tipo (hay que controlar cada resolver) |
| Batch | No nativo | Consultas múltiples en una sola petición |
El problema de seguridad fundamental de GraphQL es que un solo endpoint expone todo el grafo de datos. Si hay un fallo de autorización en un campo, el atacante puede consultarlo directamente sin necesidad de descubrir URLs ocultas.
2. Las 10 Vulnerabilidades Críticas de GraphQL
1. Introspection Habilitada en Producción
El riesgo: cualquier atacante puede obtener el esquema completo de la API enviando una consulta __schema, obteniendo todos los tipos, campos, argumentos y relaciones.
# Consulta de introspection — revela el esquema completo
query {
__schema {
types {
name
fields {
name
type { name }
}
}
}
}
Mitigación: deshabilitar introspection en producción:
// Apollo Server v4
const server = new ApolloServer({
typeDefs,
resolvers,
introspection: process.env.NODE_ENV !== 'production',
})
# Strawberry (Python)
@strawberry.type
class Query:
...
schema = strawberry.Schema(
query=Query,
# La introspection se deshabilita automáticamente en producción
# con la configuración adecuada del servidor ASGI
)
Advertencia: deshabilitar introspection no es suficiente si las sugerencias de campo (field suggestions) están activas. Herramientas como Clairvoyance pueden reconstruir el esquema a partir de los mensajes de error "Did you mean...?".
2. Batching y Alias Abuse
El riesgo: GraphQL permite consultar múltiples operaciones en una sola petición HTTP. Un atacante puede usar alias y batching para:
- Bypass de rate limiting: 1 petición HTTP = N consultas. El rate limit por HTTP request no cuenta las operaciones individuales.
- Credential stuffing: enviar 100 combinaciones de usuario/contraseña en una sola petición.
- OTP brute force: probar 50 códigos OTP en una sola petición.
- Amplificación DoS: 1 petición que ejecuta cientos de resolvers.
# Alias abuse — 10 consultas en una sola petición
query {
a1: login(password: "pass1") { success }
a2: login(password: "pass2") { success }
a3: login(password: "pass3") { success }
a4: login(password: "pass4") { success }
a5: login(password: "pass5") { success }
}
Mitigaciones:
- Rate limiting por operación (no por petición HTTP)
- Límite de alias por campo (ej: max 5 alias)
- Deshabilitar array batching si no es necesario
- Contar las operaciones en el motor de ejecución GraphQL, no solo en el middleware HTTP
// Apollo Router — demand control
demand_control:
max_cost: 1000
min_cost: 0
enable_experimental_rate_limiting: true
3. Autorización BOLA/BFLA a Nivel de Resolver
El riesgo: en REST, la autorización se aplica por endpoint. En GraphQL, cada campo puede tener su propio resolver, y cada resolver debe verificar autorización. Si un resolver de campo anidado no verifica permisos, los datos quedan expuestos.
Caso real: Shopify pagó $5,000 en HackerOne por un IDOR en GraphQL (HackerOne #2207248) donde un atacante podía descargar facturas de cualquier comercio cambiando el ID en el resolver BillingDocumentDownload.
Mitigación: aplicar autorización en cada resolver, no asumir que el resolver padre protege los campos anidados.
// Incorrecto: asume que el resolver padre ya verificó autorización
const resolvers = {
User: {
invoices: (parent, args, context) => {
// Falta verificación — cualquier usuario autenticado puede
// consultar facturas de cualquier otro usuario
return db.invoices.findByUser(parent.id)
}
}
}
// Correcto: verificar autorización en cada resolver
const resolvers = {
User: {
invoices: async (parent, args, context) => {
if (context.user.id !== parent.id && context.user.role !== 'admin') {
throw new GraphQLError('Acceso denegado', {
extensions: { code: 'FORBIDDEN' }
})
}
return db.invoices.findByUser(parent.id)
}
}
}
4. Consultas de Profundidad Excesiva (Deeply Nested Queries)
El riesgo: GraphQL permite consultas con N niveles de anidamiento. Una consulta maliciosa puede agotar los recursos del servidor.
# Depth DoS — 10 niveles de anidamiento
query {
user { posts { comments { user { posts { comments { user { ... } } } } } } }
}
Caso real: graphql-java CVE-2023-28867 carecía de límites de profundidad de recursión, permitiendo que una consulta profundamente anidada causara un stack overflow en la JVM.
Mitigación: establecer un límite de profundidad (recomendado: 7-10 niveles):
// graphql-depth-limit (Node.js)
const depthLimit = require('graphql-depth-limit')
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [depthLimit(10)],
})
5. Consultas de Complejidad Excesiva (Query Cost Analysis)
El riesgo: no todas las consultas tienen el mismo coste. Una consulta que solicita 1000 registros con 50 campos cada uno es mucho más cara que una que solicita 1 registro con 2 campos. Sin cost analysis, un atacante puede agotar la base de datos.
Mitigación: asignar costes a los campos y rechazar consultas que superen un umbral:
// GraphQL Armor — cost analysis
const { ApolloArmor } = require('@escape.tech/graphql-armor')
const armor = new ApolloArmor({
costLimit: {
maxCost: 1000,
objectCost: 2,
scalarCost: 1,
costFieldConfig: {
'Query.search': { multiplier: 10 },
},
},
})
6. Field Suggestions (Reconstrucción de Esquema Sin Introspection)
El riesgo: incluso con introspection deshabilitada, GraphQL sugiere correcciones cuando se escribe mal un campo. La herramienta Clairvoyance automatiza la reconstrucción del esquema a partir de estos mensajes de error.
Mitigación: deshabilitar field suggestions con una regla de validación personalizada:
// Apollo Server — deshabilitar field suggestions
const { NoSuggestionsRule } = require('./NoSuggestionsRule')
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [NoSuggestionsRule],
})
7. Exposición de GraphiQL/Playground en Producción
El riesgo: las interfaces gráficas de desarrollo (GraphiQL, Apollo Sandbox, Playground) permiten a cualquiera explorar y probar la API interactivamente.
Mitigación: deshabilitar en producción:
// Apollo Server
const server = new ApolloServer({
typeDefs,
resolvers,
introspection: false,
// No incluir ApolloServerPluginLandingPageGraphQLPlayground en producción
plugins: process.env.NODE_ENV === 'production'
? []
: [ApolloServerPluginLandingPageLocalDefault()],
})
8. CSRF en Endpoints GraphQL
El riesgo: si el endpoint GraphQL acepta Content-Type: application/x-www-form-urlencoded o multipart/form-data, un atacante puede enviar consultas maliciosas desde un formulario HTML en otro sitio aprovechando la cookie de sesión del usuario.
Caso real: Apollo Server 2 era vulnerable a CSRF a través del bundle graphql-upload con multipart/form-data (GHSA-2p3c-p3qw-69r4). Apollo Server 4 activa csrfPrevention: true por defecto.
Mitigaciones:
- Exigir cabecera personalizada (ej:
X-APOLLO-CSRF-PREVENTION) - Rechazar
application/x-www-form-urlencoded - Validar cabecera Origin
9. Persisted Query Bypass
El riesgo: las Persisted Queries (o Persisted Query Lists, PQL) no son un mecanismo de seguridad por sí mismas — son una optimización de rendimiento. Muchas implementaciones solo validan el hash de la consulta, no la operación completa.
Mitigación: usar PQL como safelist real: solo las consultas registradas en la lista blanca pueden ejecutarse. No confundir Automatic Persisted Queries (APQ, una cache) con safelisting real.
10. Subscriptions sin Autorización por Evento
El riesgo: las subscriptions GraphQL a menudo verifican la autorización solo al conectar, no al enviar cada evento. Un atacante puede suscribirse a un ID de recurso y luego cambiar el contexto para recibir datos de otro recurso.
Mitigación: verificar autorización en cada evento, no solo al establecer la conexión WebSocket:
// Apollo Server — autorización por evento en subscription
const resolvers = {
Subscription: {
invoiceUpdated: {
subscribe: withFilter(
() => pubsub.asyncIterator(['INVOICE_UPDATED']),
(payload, variables, context) => {
// Verificar autorización en cada evento
return context.user.id === payload.invoice.userId
},
),
},
},
}
3. Checklist de Seguridad GraphQL
- Introspection deshabilitado en producción
- Field suggestions deshabilitadas
- GraphiQL/Playground/deshabilitado en producción
- Errores sin stack traces (debug mode desactivado)
- Depth limit configurado (recomendado: 7-10 niveles)
- Cost analysis / query complexity configurado
- Alias limit configurado (max 5-10)
- Rate limiting por operación (no solo por HTTP request)
- Array batching deshabilitado si no es necesario
- CSRF prevention activado (cabecera personalizada requerida)
- CORS con lista blanca (nunca
Access-Control-Allow-Origin: *con credenciales) - Autorización verificada en cada resolver (no solo en el padre)
- Timeout configurado por resolver
- Paginación obligatoria en listas
- Validation rules de GraphQL Armor o similar implementadas
- Persisted Query List si se requiere safelisting real
- Subscriptions con autorización por evento
Conclusión
GraphQL es un objetivo de alto valor porque un solo endpoint expone todo el modelo de datos. Las vulnerabilidades más críticas — introspection abuse, batching/alias bypass, y autorización BOLA a nivel de resolver — son específicas de GraphQL y no existen en REST. Las defensas que funcionan no son exóticas: capas de límites en edge, transporte, parser, validador, resolver y observabilidad. La combinación de límites de profundidad, cost analysis, alias control, y autorización en cada resolver cubre el 90% de los vectores de ataque documentados en los CVEs de GraphQL de los últimos años.
Artículos Relacionados
- OWASP API Security Top 10 — Vulnerabilidades comunes a todas las APIs REST y GraphQL
- Pentesting Web con OWASP Top 10 — Técnicas de testing web que complementan el pentesting de GraphQL
- SQL Injection: Guía Completa — Inyección de consultas similar a los ataques de introspection y batching en GraphQL
- DevSecOps y Pipelines CI/CD Seguros — Automatizar pruebas de seguridad GraphQL en tu pipeline de desarrollo