Go · d7y.io/dragonfly/v2
Dragonfly scheduler v1 and v2 gRPC unauthenticated SSRF via attacker-controlled PeerHost in DownloadTinyFile
The Dragonfly scheduler's v1 gRPC service contains an unauthenticated Server-Side Request Forgery (SSRF). When a peer reports a successful download of a TINY task, the scheduler calls Peer.DownloadTinyFile() and issues an HTTP GET to a host and port taken verbatim from the attacker-controlled PeerHost.Ip / PeerHost.DownPort fields of the gRPC request body. The HTTP client uses a bare http.Transport with no address validation, so a remote, unauthenticated client can force the scheduler to connect to arbitrary internal addresses, including 127.0.0.1 (loopback), 169.254.0.0/16 (link-local, e.g. cloud metadata), and RFC1918 ranges. The fetched response is stored in Task.DirectPiece and can subsequently be served to other peers, making this a read-SSRF with a data-exfiltration path.
The manager's preheat code path already wraps its HTTP client with nethttp.NewSafeDialer() (which rejects non-global-unicast destinations); the scheduler's DownloadTinyFile path is missing this guard (sibling gap).
Medium. The attack requires no authentication (the scheduler gRPC server runs with insecure transport credentials by default and has no auth interceptor), and the destination is fully attacker-controlled. Impact is limited to read-SSRF: blind reachability probing of internal hosts/ports plus exfiltration of up to TinyFileSize (128) bytes per task from internal HTTP services into Task.DirectPiece. It is not remote code execution, and PeerHost.DownPort is constrained by proto validation to >= 1024, which excludes destination port 80.
dragonflyoss/dragonflyd7y.io/dragonfly/v2scheduler/resource/standard/peer.go (DownloadTinyFile), reached via scheduler/service/service_v1.go (storeHost, RegisterPeerTask, ReportPeerResult, handlePeerSuccess)v2.4.4-rc.2 (commit 0822e3aecc3369017d6b25c9441ff6f318129b31)The scheduler exposes the v1 gRPC service without authentication by default:
scheduler/scheduler.go lines 235-246, mTLS is only configured when cfg.Server.TLS != nil; otherwise the server is created with rpc.NewInsecureCredentials().scheduler/config/config.go New() (line 336) does not set Server.TLS, so the default deployment uses insecure credentials.pkg/rpc/scheduler/server/server.go lines 71-86 contains ratelimit, error-conversion, prometheus, zap-logging, validator, and recovery interceptors, but no authentication interceptor.A remote client can therefore invoke RegisterPeerTask and ReportPeerResult without credentials. The PeerHost message carried in the request is consumed by storeHost in scheduler/service/service_v1.go lines 816-845, which copies peerHost.Ip and peerHost.DownPort directly into resource.Host.IP and resource.Host.DownloadPort with no destination-address restriction. The proto validator only requires PeerHost.Ip to be a syntactically valid IP (net.ParseIP != nil) and DownPort in [1024, 65535); it does not restrict the address to global-unicast, so 127.0.0.1, 169.254.169.254, and RFC1918 addresses all pass validation.
When the reported peer is a TINY task, handlePeerSuccess lines 1176-1202 calls peer.DownloadTinyFile() and stores the result in peer.Task.DirectPiece. The sink, DownloadTinyFile lines 435-478, builds the URL from net.JoinHostPort(p.Host.IP, p.Host.DownloadPort) and dispatches it through a bare http.Transport (TLSClientConfig: InsecureSkipVerify: true) with no DialContext/socket control:
targetURL := url.URL{
Scheme: "http",
Host: net.JoinHostPort(p.Host.IP, strconv.Itoa(int(p.Host.DownloadPort))),
Path: fmt.Sprintf("download/%s/%s", p.Task.ID[:3], p.Task.ID),
RawQuery: fmt.Sprintf("peerId=%s", p.ID),
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, targetURL.String(), nil)
if err != nil {
return []byte{}, err
}
req.Header.Set(headers.Range, fmt.Sprintf("bytes=%d-%d", 0, p.Task.ContentLength.Load()-1))
p.Log.Infof("download tiny file %s, header is : %#v", targetURL.String(), req.Header)
client := &http.Client{
Timeout: 30 * time.Second,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
resp, err := client.Do(req)
By contrast, the manager's preheat path in internal/job/image.go line 211 sets DialContext: nethttp.NewSafeDialer().DialContext, and safeSocketControl in pkg/net/http/http.go lines 60-80 rejects any destination where !ip.IsGlobalUnicast(). The scheduler DownloadTinyFile path is missing this protection.
All three drivers below were built inside the d7y.io/dragonfly/v2 module at commit 0822e3aecc3369017d6b25c9441ff6f318129b31 (release v2.4.4-rc.2), so they invoke the genuine, unmodified production sink standard.Peer.DownloadTinyFile() and the genuine nethttp.NewSafeDialer() / safeSocketControl. No reimplementation of the sink is used in the positive control.
git clone --depth 1 --branch v2.4.4-rc.2 \
https://github.com/dragonflyoss/dragonfly.git
cd dragonfly
git rev-parse HEAD # 0822e3aecc3369017d6b25c9441ff6f318129b31
go version # go1.26.1
The scheduler builds and serves the v1 gRPC service with insecure transport credentials when Server.TLS is unset (the shipped default). The relevant decision is at scheduler/scheduler.go:235-246:
if cfg.Server.TLS != nil {
// Initialize grpc server with tls.
transportCredentials, err := rpc.NewServerCredentials(cfg.Server.TLS.CACert, cfg.Server.TLS.Cert, cfg.Server.TLS.Key)
if err != nil {
logger.Errorf("failed to create server credentials: %v", err)
return nil, err
}
schedulerServerOptions = append(schedulerServerOptions, grpc.Creds(transportCredentials))
} else {
// Initialize grpc server without tls.
schedulerServerOptions = append(schedulerServerOptions, grpc.Creds(rpc.NewInsecureCredentials()))
}
and the v1 handlers RegisterPeerTask / ReportPeerResult are registered with no auth interceptor (pkg/rpc/scheduler/server/server.go:71-90). Any network client can therefore drive the chain RegisterPeerTask -> (TINY task) -> ReportPeerResult -> handlePeerSuccess -> DownloadTinyFile using a PeerHost whose Ip/DownPort point at an internal target.
storeHost (service_v1.go:816) constructs the resource host straight from the gRPC PeerHost:
host := resource.NewHost(
peerHost.Id, peerHost.Ip, peerHost.Hostname, peerHost
Is your project exposed to this? Stateward checks every dependency on every pull request and flags it only if your code actually reaches it.
Check my repoSources: CISA KEV (public domain), OSV.dev & GitHub Advisory Database (CC-BY-4.0), FIRST EPSS, NVD/CWE (public domain). Served live from the Stateward advisory database.