@@ -39,7 +39,20 @@ type ipLookupFunc func(ctx context.Context, host string) ([]net.IPAddr, error)
3939// surfaced them to the policy gate as SystemWrite, so honouring that decision
4040// here preserves explicitly-allowed localhost access (and keeps httptest-backed
4141// tests working). Every other host is resolved via lookup and validated.
42- func ssrfGuardedDial (base dialFunc , lookup ipLookupFunc ) dialFunc {
42+ //
43+ // allowedHosts lists hostnames (without ports) that the operator explicitly
44+ // trusts, e.g. the configured web_search base_url. These hosts bypass the
45+ // internal-IP block so that container-internal services such as SearXNG can be
46+ // reached by name, while still being pinned to the resolved IP so the kernel
47+ // cannot re-resolve to a rebound address.
48+ func ssrfGuardedDial (base dialFunc , lookup ipLookupFunc , allowedHosts ... string ) dialFunc {
49+ allowed := make (map [string ]struct {}, len (allowedHosts ))
50+ for _ , h := range allowedHosts {
51+ if h != "" {
52+ allowed [h ] = struct {}{}
53+ }
54+ }
55+
4356 return func (ctx context.Context , network , addr string ) (net.Conn , error ) {
4457 host , port , err := net .SplitHostPort (addr )
4558 if err != nil {
@@ -50,6 +63,8 @@ func ssrfGuardedDial(base dialFunc, lookup ipLookupFunc) dialFunc {
5063 return base (ctx , network , addr )
5164 }
5265
66+ _ , hostAllowed := allowed [host ]
67+
5368 ips , err := lookup (ctx , host )
5469 if err != nil {
5570 return nil , err
@@ -60,9 +75,13 @@ func ssrfGuardedDial(base dialFunc, lookup ipLookupFunc) dialFunc {
6075 // Validate every answer before dialing any of them: an attacker can
6176 // return a safe IP alongside an internal one hoping the dialer picks
6277 // the internal address. Refuse the whole set if any is internal.
63- for _ , ipa := range ips {
64- if danger .IsBlockedIP (ipa .IP ) {
65- return nil , fmt .Errorf ("blocked connection to %q: resolves to internal address %s (possible SSRF / DNS rebinding)" , host , ipa .IP )
78+ // Operator-allowed hosts bypass this check so configured internal
79+ // backends (e.g. SearXNG under Docker) remain reachable.
80+ if ! hostAllowed {
81+ for _ , ipa := range ips {
82+ if danger .IsBlockedIP (ipa .IP ) {
83+ return nil , fmt .Errorf ("blocked connection to %q: resolves to internal address %s (possible SSRF / DNS rebinding)" , host , ipa .IP )
84+ }
6685 }
6786 }
6887 // Pin to validated IPs — never hand the hostname back to the kernel,
@@ -81,20 +100,22 @@ func ssrfGuardedDial(base dialFunc, lookup ipLookupFunc) dialFunc {
81100}
82101
83102// ssrfGuardedTransport returns an *http.Transport whose DialContext is the SSRF
84- // guard above, backed by the real dialer and resolver. It clones the default
85- // transport when possible so it inherits sane defaults (env proxy handling,
86- // idle-conn limits, HTTP/2, TLS handshake timeout); if a third-party package
87- // has swapped http.DefaultTransport for a non-*http.Transport RoundTripper, it
88- // falls back to a fresh transport with explicit proxy handling rather than
89- // panicking on the type assertion — this runs at startup, so it must fail safe.
90- func ssrfGuardedTransport () * http.Transport {
103+ // guard above, backed by the real dialer and resolver. allowedHosts are
104+ // operator-trusted hostnames (e.g. the web_search base_url host) that may
105+ // resolve to internal addresses. It clones the default transport when possible
106+ // so it inherits sane defaults (env proxy handling, idle-conn limits, HTTP/2,
107+ // TLS handshake timeout); if a third-party package has swapped
108+ // http.DefaultTransport for a non-*http.Transport RoundTripper, it falls back
109+ // to a fresh transport with explicit proxy handling rather than panicking on
110+ // the type assertion — this runs at startup, so it must fail safe.
111+ func ssrfGuardedTransport (allowedHosts ... string ) * http.Transport {
91112 base := & net.Dialer {Timeout : 30 * time .Second , KeepAlive : 30 * time .Second }
92113 tr , ok := http .DefaultTransport .(* http.Transport )
93114 if ok {
94115 tr = tr .Clone ()
95116 } else {
96117 tr = & http.Transport {Proxy : http .ProxyFromEnvironment }
97118 }
98- tr .DialContext = ssrfGuardedDial (base .DialContext , net .DefaultResolver .LookupIPAddr )
119+ tr .DialContext = ssrfGuardedDial (base .DialContext , net .DefaultResolver .LookupIPAddr , allowedHosts ... )
99120 return tr
100121}
0 commit comments