From 592b46f7207c3f30d6b1a7d14f91239ec76c966e Mon Sep 17 00:00:00 2001 From: Yanhu007 Date: Thu, 16 Apr 2026 08:12:16 +0800 Subject: [PATCH] fix: block SSRF by rejecting requests to private/internal IPs The HTTP plugin validates URL scheme but does not block requests to private IP ranges. A malicious plugin can probe the local network, access cloud metadata endpoints (169.254.169.254), or hit internal services on localhost. Add a custom DialContext that resolves the target hostname and rejects connections to loopback, private, link-local, and unspecified IP addresses before establishing the connection. Fixes #505 --- plugin/http.go | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/plugin/http.go b/plugin/http.go index a608d16..dc59ddd 100644 --- a/plugin/http.go +++ b/plugin/http.go @@ -1,7 +1,10 @@ package plugin import ( + "context" + "fmt" "io" + "net" "net/http" "strings" "time" @@ -16,6 +19,43 @@ const ( var httpClient = &http.Client{ Timeout: httpTimeout, + Transport: &http.Transport{ + DialContext: safeDialContext, + }, +} + +// safeDialContext prevents SSRF by blocking connections to private/internal +// IP ranges including loopback, link-local, and cloud metadata endpoints. +func safeDialContext(ctx context.Context, network, addr string) (net.Conn, error) { + host, port, err := net.SplitHostPort(addr) + if err != nil { + return nil, fmt.Errorf("invalid address: %w", err) + } + + ips, err := net.DefaultResolver.LookupIPAddr(ctx, host) + if err != nil { + return nil, fmt.Errorf("DNS lookup failed: %w", err) + } + + for _, ip := range ips { + if isPrivateIP(ip.IP) { + return nil, fmt.Errorf("requests to private/internal addresses are not allowed: %s", ip.IP) + } + } + + // All IPs are safe; connect to the first one. + var d net.Dialer + return d.DialContext(ctx, network, net.JoinHostPort(ips[0].IP.String(), port)) +} + +// isPrivateIP returns true if the IP is in a private, loopback, link-local, +// or otherwise internal range that should not be reachable from plugins. +func isPrivateIP(ip net.IP) bool { + return ip.IsLoopback() || + ip.IsPrivate() || + ip.IsLinkLocalUnicast() || + ip.IsLinkLocalMulticast() || + ip.IsUnspecified() } // luaHTTP implements matcha.http(options) — make an HTTP request.