From 5c8881c9a70059a36bfac8158011d00636894a29 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 8 Aug 2026 02:14:46 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20Fix=20Serve?= =?UTF-8?q?r-Side=20Request=20Forgery=20(SSRF)=20risk=20in=20plugin=20down?= =?UTF-8?q?loads?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaced usage of `http.Get` for fetching user-provided plugin URLs with a custom `http.Client`. The new client (`newSafeHTTPClient`) includes an explicitly configured `net.Dialer` with a `Control` hook that intercepts socket connections *after* DNS resolution to evaluate the actual IP address being connected to. This blocks access to specific targeted cloud metadata endpoints (like 169.254.169.254), protecting against DNS rebinding and IP obfuscation SSRF attacks. Also introduces explicit timeouts for these external requests. Co-authored-by: himattm <6266621+himattm@users.noreply.github.com> --- .jules/sentinel.md | 4 ++++ internal/plugin/manager.go | 29 +++++++++++++++++++++++++++-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 80620ce..cb2eac7 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -16,3 +16,7 @@ **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-05-24 - SSRF vulnerability in plugin downloads +**Vulnerability:** The application used `http.Get` directly for downloading plugins from potentially user-controlled URLs without IP validation, exposing it to Server-Side Request Forgery (SSRF) attacks, specifically against cloud metadata endpoints like `169.254.169.254`. +**Learning:** `http.Get` does not protect against SSRF. Merely parsing URLs is insufficient since DNS rebinding can still target internal IPs. +**Prevention:** Use a custom `http.Client` with a `net.Dialer` and a `Control` hook to intercept socket connections *after* DNS resolution but *before* connecting, blocking prohibited IPs (e.g., `169.254.169.254`). Also, ensure proper timeouts are configured. diff --git a/internal/plugin/manager.go b/internal/plugin/manager.go index 7aad027..7a6cc11 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" @@ -24,6 +26,27 @@ import ( var metadataRegex = regexp.MustCompile(`^#\s*@(\w+[-\w]*)\s+(.+)$`) var versionRegex = regexp.MustCompile(`(?m)^#\s*@version\s+(.+)$`) +func newSafeHTTPClient() *http.Client { + 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 { + host = strings.SplitN(host, "%", 2)[0] + if ip := net.ParseIP(host); ip != nil && ip.String() == "169.254.169.254" { + return fmt.Errorf("blocked cloud metadata IP") + } + } + return nil + }, + } + t := http.DefaultTransport.(*http.Transport).Clone() + t.DialContext = dialer.DialContext + t.DisableKeepAlives = true + return &http.Client{Timeout: 10 * time.Second, Transport: t} +} + // sanitizeFilename ensures a filename cannot be used for path traversal func sanitizeFilename(name string) string { return filepath.Base(filepath.Clean("/" + name)) @@ -410,7 +433,8 @@ func (m *Manager) addScriptPlugin(owner, repo, pluginName string) error { fmt.Printf("Fetching script from: %s\n", rawURL) - resp, err := http.Get(rawURL) + client := newSafeHTTPClient() + resp, err := client.Get(rawURL) if err != nil { return fmt.Errorf("failed to fetch plugin: %w", err) } @@ -468,7 +492,8 @@ func (m *Manager) addFromDirectURL(rawURL string) error { return fmt.Errorf("unsupported URL scheme: %s", parsedURL.Scheme) } - resp, err := http.Get(parsedURL.String()) + client := newSafeHTTPClient() + resp, err := client.Get(parsedURL.String()) if err != nil { return fmt.Errorf("failed to fetch plugin: %w", err) }