From 2307247c383fb5abbb5f516c5106783ca614b558 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 3 Aug 2026 02:26:17 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[HIGH]=20Fi?= =?UTF-8?q?x=20SSRF=20vulnerability=20in=20plugin=20downloader?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: himattm <6266621+himattm@users.noreply.github.com> --- .jules/sentinel.md | 5 +++++ internal/plugin/manager.go | 36 ++++++++++++++++++++++++++++++++++-- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 80620ce..3f2d630 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -16,3 +16,8 @@ **Vulnerability:** Writing fully buffered in-memory data to temporary disk files solely for parsing. **Learning:** This increases attack surface, risks disk exhaustion, and violates the principle of least privilege. **Prevention:** Refactor parsing functions to accept byte slices or `io.Reader` directly to process data in memory. + +## 2024-08-03 - Prevent SSRF with net.Dialer Control Hook +**Vulnerability:** Relying on `net/url.Parse` to validate URL schemes doesn't fully protect against SSRF (e.g. against cloud metadata services) and custom transports can break 'Happy Eyeballs'. +**Learning:** Using a `net.Dialer` with a `Control` hook allows inspecting the resolved IP *before* the socket connects, providing a robust way to block targeted IPs (like `169.254.169.254`) without breaking legitimate local use cases. +**Prevention:** Clone `http.DefaultTransport` and configure its `DialContext` to use a dialer with a Control hook that validates the resolved IP. Block specific cloud metadata endpoints instead of all private IPs. diff --git a/internal/plugin/manager.go b/internal/plugin/manager.go index 7aad027..f2935aa 100644 --- a/internal/plugin/manager.go +++ b/internal/plugin/manager.go @@ -7,6 +7,7 @@ import ( "encoding/json" "fmt" "io" + "net" "net/http" "net/url" "os" @@ -16,6 +17,7 @@ import ( "runtime" "sort" "strings" + "syscall" "time" "github.com/himattm/prism/internal/fsutil" @@ -410,7 +412,7 @@ func (m *Manager) addScriptPlugin(owner, repo, pluginName string) error { fmt.Printf("Fetching script from: %s\n", rawURL) - resp, err := http.Get(rawURL) + resp, err := secureGet(rawURL) if err != nil { return fmt.Errorf("failed to fetch plugin: %w", err) } @@ -468,7 +470,7 @@ func (m *Manager) addFromDirectURL(rawURL string) error { return fmt.Errorf("unsupported URL scheme: %s", parsedURL.Scheme) } - resp, err := http.Get(parsedURL.String()) + resp, err := secureGet(parsedURL.String()) if err != nil { return fmt.Errorf("failed to fetch plugin: %w", err) } @@ -860,3 +862,33 @@ func CompareVersions(a, b string) int { return 0 } + +func secureGet(urlStr string) (*http.Response, error) { + dialer := &net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + Control: func(network, address string, c syscall.RawConn) error { + host, _, err := net.SplitHostPort(address) + if err != nil { + return err + } + if idx := strings.IndexByte(host, '%'); idx != -1 { + host = host[:idx] + } + ip := net.ParseIP(host) + if ip != nil && ip.Equal(net.ParseIP("169.254.169.254")) { + return fmt.Errorf("blocked request to cloud metadata IP") + } + return nil + }, + } + transport := http.DefaultTransport.(*http.Transport).Clone() + transport.DialContext = dialer.DialContext + transport.DisableKeepAlives = true + + client := &http.Client{ + Timeout: 10 * time.Second, + Transport: transport, + } + return client.Get(urlStr) +}