← Back to Attack Research

Injecting a query language you have never heard of: DQL injection in Dgraph

Everyone knows to parameterize SQL. Almost nobody thinks to parameterize Dgraph Query Language. Three 2026 flaws show what happens when a graph database formats user input straight into DQL text: a login-path oracle, and two pre-auth paths that read the whole graph from one unauthenticated request.

Injection is not a SQL problem. It is a string-building problem, and it follows you into every query language your stack speaks. Dgraph is a graph database, and it has its own language, Dgraph Query Language, usually just called DQL. In 2026 three separate flaws showed the same root cause: user-controlled values were formatted directly into DQL text instead of being passed as parameters. Two of them, CVE-2026-41327 and CVE-2026-41328, are pre-authentication and let a single crafted request read the entire graph. Both are rated CVSS 9.1 Critical. The third, CVE-2026-44840, is a login-path oracle rated 7.5 High. This is CWE-943, improper neutralization of special elements in a data query, and it is the injection class teams forget exists because the query language is not SQL.

Here is the uncomfortable part. Every engineer who touches a relational database has been trained to parameterize, a discipline that exists because SQL injection taught the industry a hard lesson. But that discipline is attached in most people's heads to the word SQL, not to the concept of an interpreter that mixes code and data. Move to a graph database with its own query language, and the muscle memory does not fire. The value gets pasted into the query text with a format string, nobody flags it in review, and the same 1998-vintage bug ships in 2026. Dgraph's three flaws show that the injection family does not care what the interpreter is called. Verifiable security.

Three flaws, one root cause

All three bugs live in dgraph-io/dgraph, the open-source graph database. They differ in which request field the attacker controls and in what the query is trying to do, but the mechanism is identical: an untrusted value reaches DQL as text, and a well-placed character lets it stop being a value and start being query syntax.

The login oracle (CVE-2026-44840)

The login path builds a checkpwd() DQL query with fmt.Sprintf, dropping the user-supplied password into the query string without escaping. Because the password lands inside a DQL string literal, a password value that contains a double-quote character closes that literal early and lets the rest of the value append attacker-controlled DQL. This one is confidentiality-only, an oracle: the injected DQL changes the shape of the response, so an attacker can ask yes-or-no questions of the database through the login endpoint. That is why it is scored 7.5 High rather than Critical, and why the fix landed in 25.3.4 (and the 24.1.9 maintenance line). Its advisory is GHSA-q2m9-6jp9-c6mc.

The upsert condition (CVE-2026-41327)

Dgraph upserts let a mutation carry a cond field, a conditional expression evaluated as part of the operation. That value is concatenated into the DQL body through a string builder, so a crafted cond becomes query syntax rather than a condition. Because Dgraph ships with access control (ACL) disabled by default, the mutate endpoint answers without credentials, which means a single unauthenticated POST /mutate?commitNow=true can smuggle a read of the whole graph into what looks like a write. Pre-auth, full-database exfiltration, CVSS 9.1 Critical. Fixed in 25.3.3.

The language tag (CVE-2026-41328)

The third takes an even more obscure field. When Dgraph ingests RDF-style triples, an NQuad can carry a Lang language tag on a predicate. The internal addQueryIfUnique helper builds a DQL query with fmt.Sprintf using that Lang value unvalidated, so a crafted key of the form <predicate>@<payload> injects a named query block of the attacker's choosing. Same outcome as the upsert path: unauthenticated, whole-graph read, CVSS 9.1 Critical, fixed in 25.3.3. Its advisory is GHSA-x92x-px7w-4gx4.

DQL BUILT BY STRING FORMATTING developer intends: ... checkpwd(pw, "USER_VALUE") ... attacker sends: USER_VALUE = " ^ closes the string literal early engine now runs: ... checkpwd(pw, "") ... the query executed is no longer the query the developer wrote common cause: values pasted into query text, never parameterized common enabler: ACL is OFF by default, so mutate answers pre-auth CWE-943 | same shape in login, upsert cond, and NQuad Lang

The double-quote breakout is illustrative of the class, not a working payload against any host. The point is structural: a value becomes syntax.

Why a non-SQL query language is the blind spot

The reason this class keeps landing is not that the fix is hard. Parameterized queries are a solved problem that every mature database driver offers. The reason is attention: static analysis rules, code-review checklists, and developer instinct are all tuned to a small set of well-known interpreters, the shell, SQL, HTML, LDAP. A vendor-specific query language sits outside that set, so the same fmt.Sprintf that would trigger alarms around a SQL statement passes review without comment when the target is DQL.

Be precise about the severity, because that precision is the brand. The pre-auth cluster is near the worst case: no credentials, and a single request reading the entire dataset, on a database that by default does not ask who is calling. That default matters: ACL being off is what turns two parsing bugs into unauthenticated exfiltration rather than a privilege-escalation footnote. The login oracle is a real but narrower problem: it leaks through a side channel rather than dumping data outright, which is why it is High and not Critical. Getting that distinction right, 9.1 for the cluster and 7.5 for the oracle, is the difference between an accurate advisory and a scary one.

The injection family does not care what the interpreter is called. Move to a query language nobody parameterizes, and a 1998 bug ships in 2026.

How to recognize your exposure

You can assess this without sending a single hostile payload, because the exposure is decided by two readable facts and one config flag.

Close DQL injection on a Dgraph deployment

How Celvex catches this

Find. Prove. Fix. Verify.

Find

A read-only sweep fingerprints the Dgraph Alpha version through its build and health surface and determines whether ACL is enabled, flagging exposed pre-auth query and mutate endpoints without sending a malicious payload.

Prove

Authorized injection of a benign marker predicate through the login or upsert path shows the response shape change, proving reachability without exfiltrating real data. The version and ACL-state evidence becomes an Ed25519-signed Proof Capsule.

Fix

The capsule names the steps: upgrade to 25.3.4 or later (or 24.1.9), enable ACL, and put the mutate and GraphQL endpoints behind authentication.

Verify

A fresh sweep re-tests the marker injection, confirms it is neutralized on the patched build, and confirms ACL now blocks unauthenticated mutate. The finding closes with a recorded verified-fix event.

The lesson generalizes past this one database. Every query language your stack speaks is an interpreter, and every interpreter that mixes attacker-influenced data with instructions is an injection surface, whether it is SQL, a graph language, a search DSL, or a template. The discipline that beat SQL injection was never about SQL. It was about never letting a value become syntax.

Verifiable security. Find it. Prove it. Fix it. Verify the fix held. That is what we ship.

Sources

Is your graph database answering unauthenticated queries?

Free Exposure Check, no signup required. We fingerprint your Dgraph nodes, determine ACL state, flag pre-auth query and mutate surfaces, and ship a signed Proof Capsule for the highest-confidence finding.

Run a Free Scan →