Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
36 changes: 34 additions & 2 deletions internal/plugin/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/url"
"os"
Expand All @@ -16,6 +17,7 @@ import (
"runtime"
"sort"
"strings"
"syscall"
"time"

"github.com/himattm/prism/internal/fsutil"
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
Loading