@@ -119,22 +119,30 @@ func NewSSRFSafeTransport() *http.Transport {
119119 t = & http.Transport {Proxy : http .ProxyFromEnvironment }
120120 }
121121 proxies := proxyDialAllowlist ()
122- t . DialContext = ( & net.Dialer {
122+ guarded := & net.Dialer {
123123 Timeout : 30 * time .Second ,
124124 KeepAlive : 30 * time .Second ,
125- Control : func (network , address string , c syscall.RawConn ) error {
126- if _ , ok := proxies [canonicalHostPort (address )]; ok {
127- return nil
128- }
129- return SSRFDialControl (network , address , c )
130- },
131- }).DialContext
125+ Control : SSRFDialControl ,
126+ }
127+ trusted := & net.Dialer {
128+ Timeout : 30 * time .Second ,
129+ KeepAlive : 30 * time .Second ,
130+ }
131+ // The address here is pre-resolution: with a proxy configured the
132+ // transport dials the proxy's literal host:port, so the allowlist can
133+ // be matched by string without ever resolving the proxy hostname.
134+ t .DialContext = func (ctx context.Context , network , address string ) (net.Conn , error ) {
135+ if _ , ok := proxies [canonicalHostPort (address )]; ok {
136+ return trusted .DialContext (ctx , network , address )
137+ }
138+ return guarded .DialContext (ctx , network , address )
139+ }
132140 return t
133141}
134142
135143// canonicalHostPort normalises a "host:port" dial address so equivalent
136144// IPv4 forms compare equal. An IPv4-mapped IPv6 address (::ffff:a.b.c.d)
137- // is folded back to its dotted-quad form, matching what proxyHostPorts
145+ // is folded back to its dotted-quad form, matching what proxyHostPort
138146// stores in the allowlist for an IPv4-literal proxy. Non-IP hosts and
139147// malformed inputs are returned unchanged so the allowlist lookup
140148// simply misses and the SSRF fall-through applies.
@@ -153,12 +161,13 @@ func canonicalHostPort(address string) string {
153161 return net .JoinHostPort (ip .String (), port )
154162}
155163
156- // proxyDialAllowlist returns the set of "host:port" strings that the
157- // HTTP_PROXY / HTTPS_PROXY / ALL_PROXY environment variables resolve to.
158- // Hostname-based proxies are resolved to all of their A/AAAA records so
159- // the comparison can be done against the post-resolution dial address.
164+ // proxyDialAllowlist returns the set of "host:port" strings the
165+ // HTTP_PROXY / HTTPS_PROXY / ALL_PROXY environment variables name.
166+ // Hostnames are kept verbatim — never resolved — because the transport
167+ // dials a configured proxy by its literal host:port, so the comparison
168+ // happens pre-resolution. Constructors must not block on DNS.
160169// The result is a snapshot taken at call time — safe to capture in a
161- // dialer's Control closure.
170+ // dial closure.
162171func proxyDialAllowlist () map [string ]struct {} {
163172 out := map [string ]struct {}{}
164173 raw := []string {
@@ -167,26 +176,26 @@ func proxyDialAllowlist() map[string]struct{} {
167176 os .Getenv ("ALL_PROXY" ), os .Getenv ("all_proxy" ),
168177 }
169178 for _ , s := range raw {
170- for addr := range proxyHostPorts (s ) {
179+ if addr := proxyHostPort (s ); addr != "" {
171180 out [addr ] = struct {}{}
172181 }
173182 }
174183 return out
175184}
176185
177- // proxyHostPorts parses a single proxy specification (matching the syntax
186+ // proxyHostPort parses a single proxy specification (matching the syntax
178187// accepted by net/http.ProxyFromEnvironment: a full URL or a bare host[:port]
179- // in which case http:// is implied) and returns each "ip :port" it can be
180- // reached at. A nil/empty input yields an empty set .
181- func proxyHostPorts (spec string ) map [ string ] struct {} {
188+ // in which case http:// is implied) and returns its canonical "host :port".
189+ // An empty or unparseable input yields "" .
190+ func proxyHostPort (spec string ) string {
182191 if spec == "" {
183- return nil
192+ return ""
184193 }
185194 u , err := url .Parse (spec )
186195 if err != nil || u .Host == "" {
187196 u , err = url .Parse ("http://" + spec )
188197 if err != nil || u .Host == "" {
189- return nil
198+ return ""
190199 }
191200 }
192201 port := u .Port ()
@@ -200,24 +209,7 @@ func proxyHostPorts(spec string) map[string]struct{} {
200209 port = "80"
201210 }
202211 }
203- out := map [string ]struct {}{}
204- host := u .Hostname ()
205- if ip := net .ParseIP (host ); ip != nil {
206- out [net .JoinHostPort (ip .String (), port )] = struct {}{}
207- return out
208- }
209- // The allowlist is resolved once, at transport-construction time, so its
210- // (potentially blocking) DNS lookups are not repeated on every dial.
211- // There is no caller context at construction; a bounded independent root
212- // is the right base for the resolver timeout.
213- //rubocop:disable Lint/ContextConnectivity
214- ctx , cancel := context .WithTimeout (context .Background (), 5 * time .Second )
215- defer cancel ()
216- ips , _ := net .DefaultResolver .LookupIPAddr (ctx , host )
217- for _ , ip := range ips {
218- out [net .JoinHostPort (ip .IP .String (), port )] = struct {}{}
219- }
220- return out
212+ return canonicalHostPort (net .JoinHostPort (u .Hostname (), port ))
221213}
222214
223215// BoundedRedirects returns an http.Client.CheckRedirect that limits a
0 commit comments