@@ -302,6 +302,19 @@ func isRestrictedIP(ip net.IP) (bool, string) {
302302 return true , fmt .Sprintf ("IPv4-mapped %s" , reason )
303303 }
304304 }
305+ // Teredo tunneling addresses: 2001:0000::/32
306+ // Embed arbitrary IPv4 in the payload; can reach internal hosts via relay.
307+ if ip [0 ] == 0x20 && ip [1 ] == 0x01 && ip [2 ] == 0x00 && ip [3 ] == 0x00 {
308+ return true , "Teredo tunneling address"
309+ }
310+ // 6to4 addresses: 2002::/16
311+ // Bits 16-47 carry an IPv4 address; block when embedded IPv4 is restricted.
312+ if ip [0 ] == 0x20 && ip [1 ] == 0x02 {
313+ embeddedIP := net .IP (ip [2 :6 ])
314+ if restricted , reason := isRestrictedIP (embeddedIP ); restricted {
315+ return true , fmt .Sprintf ("6to4 embedded %s" , reason )
316+ }
317+ }
305318 }
306319
307320 return false , ""
@@ -357,15 +370,15 @@ func isIPLikeHostname(hostname string) bool {
357370 return false
358371}
359372
360- // IsSSRFSafeURL validates a URL to prevent SSRF attacks
373+ // isSSRFSafeURL validates a URL to prevent SSRF attacks
361374// It checks for:
362375// - Valid http/https protocol
363376// - Private IP addresses (10.x.x.x, 172.16-31.x.x, 192.168.x.x)
364377// - Loopback addresses (127.x.x.x, ::1)
365378// - Link-local addresses (169.254.x.x, fe80::)
366379// - Cloud metadata endpoints
367380// - Reserved hostnames (localhost, *.local, etc.)
368- func IsSSRFSafeURL (rawURL string ) (bool , string ) {
381+ func isSSRFSafeURL (rawURL string ) (bool , string ) {
369382 if rawURL == "" {
370383 return false , "URL is empty"
371384 }
@@ -408,11 +421,14 @@ func IsSSRFSafeURL(rawURL string) (bool, string) {
408421 }
409422 }
410423
411- // STRICT MODE: Completely block IP addresses in URLs
412- // This prevents all IP-based SSRF attacks including edge cases and bypasses
424+ // STRICT MODE: Block all direct IP addresses in URLs (both IPv4 and IPv6).
425+ // This prevents IP-based SSRF attacks including obfuscation, tunneling, and
426+ // transition mechanism bypasses. Legitimate IPs should be whitelisted via
427+ // SSRF_WHITELIST env var; the whitelist is checked by ValidateURLForSSRF
428+ // before this function is called.
413429 ip := net .ParseIP (hostname )
414430 if ip != nil {
415- return false , "direct IP address access is not allowed, use domain name instead "
431+ return false , "direct IP address access is not allowed, use domain name or add to SSRF_WHITELIST "
416432 }
417433
418434 // Also check for IP addresses in various formats that ParseIP might not catch
@@ -425,11 +441,6 @@ func IsSSRFSafeURL(rawURL string) (bool, string) {
425441 // This prevents DNS rebinding attacks where a domain resolves to internal IPs
426442 ips , err := net .LookupIP (hostname )
427443 if err != nil {
428- // DNS resolution failed - reject the URL for security
429- // This prevents attacks where:
430- // 1. The domain is only resolvable within internal network (intranet domains)
431- // 2. Different DNS servers between validation and actual request
432- // 3. Attacker-controlled DNS that selectively responds
433444 return false , fmt .Sprintf ("DNS resolution failed for hostname %s: cannot verify if it resolves to safe IP" , hostname )
434445 }
435446
@@ -760,9 +771,18 @@ func NewSSRFSafeHTTPClient(config SSRFSafeHTTPClientConfig) *http.Client {
760771 return fmt .Errorf ("stopped after %d redirects" , config .MaxRedirects )
761772 }
762773
763- // Validate the redirect target URL for SSRF
774+ // Validate the redirect target URL for SSRF (whitelist-aware).
775+ // Even whitelisted hosts must use http/https to prevent scheme-based attacks.
776+ redirectScheme := strings .ToLower (req .URL .Scheme )
777+ if redirectScheme != "http" && redirectScheme != "https" {
778+ return fmt .Errorf ("%w: invalid scheme %s" , ErrSSRFRedirectBlocked , redirectScheme )
779+ }
780+ redirectHost := req .URL .Hostname ()
781+ if redirectHost != "" && IsSSRFWhitelisted (redirectHost ) {
782+ return nil
783+ }
764784 redirectURL := req .URL .String ()
765- if safe , reason := IsSSRFSafeURL (redirectURL ); ! safe {
785+ if safe , reason := isSSRFSafeURL (redirectURL ); ! safe {
766786 return fmt .Errorf ("%w: %s" , ErrSSRFRedirectBlocked , reason )
767787 }
768788
@@ -782,7 +802,9 @@ func SSRFSafeDialContext(ctx context.Context, network, addr string) (net.Conn, e
782802 }
783803
784804 // Whitelisted hosts bypass all dial-time SSRF checks, consistent with
785- // ValidateURLForSSRF which skips IsSSRFSafeURL for whitelisted hosts.
805+ // ValidateURLForSSRF which skips isSSRFSafeURL for whitelisted hosts.
806+ // NOTE: This intentionally relaxes DNS-rebinding protection for whitelisted
807+ // hosts. Admins must ensure whitelisted domains are under their control.
786808 if IsSystemProxy (addr ) || IsSSRFWhitelisted (host ) {
787809 dialer := & net.Dialer {
788810 Timeout : 30 * time .Second ,
@@ -834,10 +856,11 @@ func SSRFSafeDialContext(ctx context.Context, network, addr string) (net.Conn, e
834856// allowed host patterns. Each entry can be:
835857// - An exact domain: "example.com"
836858// - A wildcard domain: "*.example.com" (matches all subdomains)
837- // - An IP address: "203.0.113.5"
838- // - A CIDR range: "10.0.0.0/8"
859+ // - An IPv4 address: "203.0.113.5"
860+ // - An IPv6 address: "2001:db8::1"
861+ // - A CIDR range (v4 or v6): "10.0.0.0/8", "2001:db8::/32"
839862//
840- // Whitelisted entries bypass the normal SSRF checks performed by IsSSRFSafeURL .
863+ // Whitelisted entries bypass the normal SSRF checks performed by isSSRFSafeURL .
841864
842865var (
843866 ssrfWhitelistOnce sync.Once
@@ -932,16 +955,16 @@ func IsSSRFWhitelisted(hostname string) bool {
932955 return false
933956}
934957
935- // ResetSSRFWhitelistForTest resets the whitelist singleton so tests can
958+ // resetSSRFWhitelistForTest resets the whitelist singleton so tests can
936959// re-read the environment variable. NOT for production use.
937- func ResetSSRFWhitelistForTest () {
960+ func resetSSRFWhitelistForTest () {
938961 ssrfWhitelistOnce = sync.Once {}
939962 ssrfWhitelist = nil
940963}
941964
942965// ValidateURLForSSRF is the centralised entry-point that all handlers should
943966// call to validate a user-supplied URL. It first checks the SSRF_WHITELIST;
944- // whitelisted hosts skip the full IsSSRFSafeURL check.
967+ // whitelisted hosts skip the full isSSRFSafeURL check.
945968//
946969// rawURL may be a full URL ("https://example.com/v1") or a bare host/host:port
947970// (for cases like ReconnectDocReader). If a scheme is missing the function
@@ -975,7 +998,7 @@ func ValidateURLForSSRF(rawURL string) error {
975998 }
976999
9771000 // Delegate to the full SSRF validation (uses the normalised URL).
978- if safe , reason := IsSSRFSafeURL (normalized ); ! safe {
1001+ if safe , reason := isSSRFSafeURL (normalized ); ! safe {
9791002 return fmt .Errorf ("SSRF validation failed: %s" , reason )
9801003 }
9811004 return nil
0 commit comments