GHSA-mrxx-39g5-ph77: Dgraph: Pre-Auth Full Database Exfiltration via DQL Injection in Upsert Condition Field
1. Executive SummaryA 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 is a single HTTP POST to `/mutate?commitNow=true` containing a crafted `cond` field in an upsert mutation. The `cond` value is concatenated directly into a DQL query string via `strings.Builder.WriteString` after only a cosmetic `strings.Replace` transformation. No escaping, parameterization, or structural validation is applied. An attacker injects an additional DQL query block into the `cond` string, which the DQL parser accepts as a syntactically valid named query block. The injected query executes server-side and its results are returned in the HTTP response.There are no credentials involved. When ACL is disabled (the default), the `/mutate` endpoint requires no authentication. The `authorizeQuery` and `authorizeMutation` functions both return `nil` immediately when `AclSecretKey` is not configured. Even when ACL is enabled, a user with mutation-only permission can inject read queries that bypass per-predicate ACL authorization, because the injected query block is not subject to the normal authorization flow.POC clip:https://github.com/user-attachments/assets/edf43615-b0d5-46cd-abd9-2cb9423790d22. CVSS Score**CVSS 3.1: 9.1 (Critical)**| Metric              | Value     | Rationale                                                                          |
| ------------------- | --------- | ---------------------------------------------------------------------------------- |
| Attack Vector       | Network   | HTTP POST to port 8080                                                             |
| Attack Complexity   | Low       | Single request, no special conditions beyond default config                        |
| 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 injection can also be used to manipulate upsert conditions, bypassing uniqueness constraints and conditional mutation logic |
| Availability        | None      | No denial of service                                                               |3. Vulnerability Summary| Field     | Value                                                                                      |
| --------- | ------------------------------------------------------------------------------------------ |
| Title     | Pre-Auth DQL Injection via Unsanitized Cond Field in Upsert Mutations                      |
| 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                                                           |
| Tested version       | v25.3.0                                                                                        |
| HTTP handler         | `dgraph/cmd/alpha/http.go` line 345 (`mutationHandler`)                                       |
| Cond extraction      | `dgraph/cmd/alpha/http.go` line 413 (`strconv.Unquote`)                                       |
| Cond passthrough     | `edgraph/server.go` line 2011 (`ParseMutationObject`, copies `mu.Cond` verbatim)              |
| Injection sink       | `edgraph/server.go` line 750 (`upsertQB.WriteString(cond)`)                                   |
| Only transformation  | `edgraph/server.go` line 730 (`strings.Replace(gmu.Cond, "@if", "@filter", 1)`)               |
| 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         | None. Default configuration. ACL disabled is the default.                                      |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, `--security 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 714-757 (`buildUpsertQuery`)
**CWE:** CWE-943 (Improper Neutralization of Special Elements in Data Query Logic)The `/mutate` endpoint accepts JSON bodies containing a `mutations` array. Each mutation can include a `cond` field, intended for conditional upserts with syntax like `@if(eq(name, "Alice"))`. This condition is supposed to be spliced into the DQL query as a `@filter` clause on a dummy `var(func: uid(0))` block.The handler at `http.go:413` extracts the `cond` value via `strconv.Unquote`, which interprets `\n` as actual newlines but performs no sanitization:`ParseMutationObject` at `server.go:2011` copies it verbatim:`buildUpsertQuery` at `server.go:730` applies one cosmetic replacement then concatenates the raw string directly into the DQL query:There is no escaping, no parameterization, no structural validation, and no character allowlist between the HTTP input and the query string concatenation.An attacker crafts a `cond` value that closes the `@filter(...)` clause and opens an entirely new named query block:After `buildUpsertQuery` processes this, the resulting DQL is:The DQL parser (`dql.ParseWithNeedVars`) accepts multiple query blocks within a single `{}` container. It parses `leak(...)` as a legitimate named query. The `validateResult` function at `parser.go:740` only checks for duplicate aliases and explicitly skips `var` queries. The injected query uses a unique alias, so validation passes.All three queries execute. The results of the injected `leak` block are serialized to JSON and returned to the attacker at `http.go:498`:The `@if` condition evaluates to false (`"nonexistent"` matches nothing), so the `set` mutation never actually writes data. The attack is a pure read disguised as a mutation. No data is modified.7. Full Chain ExplanationThe attacker has no Dgraph credentials and no prior access to the server.**Step 1.** The attacker sends one HTTP request:No `X-Dgraph-AccessToken` header. No `X-Dgraph-AuthToken` header. The `/mutate` endpoint has no authentication wrapper in default configuration.**Step 2.** `mutationHandler` at `http.go:345` calls `readRequest` to get the body, then `extractMutation` which calls `strconv.Unquote` on the `cond` field. The `\n` becomes a real newline. The result is stored in `api.Mutation.Cond`.**Step 3.** The request enters `edgraph.Server.QueryNoGrpc` at `http.go:471`, which calls `doQuery` -> `parseRequest` -> `ParseMutationObject`. The `Cond` is copied verbatim to `dql.Mutation.Cond` at `server.go:2011`.**Step 4.** `buildUpsertQuery` at `server.go:714` processes the condition. The only transformation is `strings.Replace(gmu.Cond, "@if", "@filter", 1)` at line 730. The full string, including the injected `leak(...)` block, is written into the query builder at line 750.**Step 5.** `dql.ParseWithNeedVars` parses the constructed DQL string. It encounters three query blocks: `q`, the upsert check `var`, and the injected `leak`. All three are accepted as valid DQL.**Step 6.** `authorizeQuery` at `access.go:958` returns `nil` immediately because `AclSecretKey == nil` (ACL not configured). No predicate-level authorization is performed.**Step 7.** `processQuery` executes all three query blocks. The `leak` block traverses every node with a `dgraph.type` predicate and returns all requested fields.**Step 8.** The response is returned to the attacker at `http.go:498`. The `data.queries.leak` array contains every matching node with all their predicates, including secrets, credentials, and PII.8. Proof of ConceptFiles| File                    | Purpose                                                    |
| ----------------------- | ---------------------------------------------------------- |
| report.md               | This vulnerability report                                  |
| poc.py                  | Exploit: sends the injection and prints leaked data        |
| docker-compose.yml      | Spins up a Dgraph cluster (1 Zero + 1 Alpha, default config) |
| DGraphPreAuthDQL.mp4    | Screen recording of the full attack from start to exfiltration |POC files zip:
[LEAD_001_DQL.zip](https://github.com/user-attachments/files/25996009/LEAD_001_DQL.zip)poc.pyThe exploit sends a single POST to `/mutate?commitNow=true` with the crafted `cond` field. It parses the response and prints all exfiltrated records, highlighting secrets, AWS credentials, and GCP service account keys.Tested Output9. Steps to ReproducePrerequisitesPython 3 with `requests` (`pip install requests`)Docker and Docker ComposeStep 1: Start DgraphWait for health:Step 2: Seed test dataStep 3: Run the exploitWhat to verifyHTTP POST returns 200 (endpoint is reachable without auth)Response contains `data.queries.leak` with an array of nodesThe nodes include fields the attacker never queried through legitimate means (secrets, AWS keys, GCP keys)No data was modified in the database (the `@if` condition prevents the `set` from executing)10. Mitigations and Patch**Location:** `edgraph/server.go`, `buildUpsertQuery` (line 714)Instead of concatenating the raw `cond` string into the DQL query, `buildUpsertQuery` should parse the `cond` value with the DQL lexer and construct the `@filter` as a parsed AST subtree. This eliminates the injection surface entirely because the filter is built programmatically rather than spliced in as a raw string. The existing `strings.Replace(gmu.Cond, "@if", "@filter", 1)` at line 730 is a semantic transformation, not a security control, and should not be relied upon for sanitization.
CVSS v3.1: 9.1