SafeHTTPClient returns an HTTP client with SSRF protection It validates URLs and blocks requests to private networks
(timeout time.Duration)
| 156 | // SafeHTTPClient returns an HTTP client with SSRF protection |
| 157 | // It validates URLs and blocks requests to private networks |
| 158 | func SafeHTTPClient(timeout time.Duration) *http.Client { |
| 159 | dialer := &net.Dialer{ |
| 160 | Timeout: timeout, |
| 161 | KeepAlive: 30 * time.Second, |
| 162 | } |
| 163 | |
| 164 | transport := &http.Transport{ |
| 165 | DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { |
| 166 | // Extract host from address |
| 167 | host, _, err := net.SplitHostPort(addr) |
| 168 | if err != nil { |
| 169 | host = addr |
| 170 | } |
| 171 | |
| 172 | // Resolve and check the IP |
| 173 | ips, err := net.LookupIP(host) |
| 174 | if err != nil { |
| 175 | return nil, fmt.Errorf("SSRF protection: failed to resolve host %s: %w", host, err) |
| 176 | } |
| 177 | |
| 178 | for _, ip := range ips { |
| 179 | if isPrivateIP(ip) { |
| 180 | return nil, fmt.Errorf("SSRF protection: blocked connection to private IP %s", ip) |
| 181 | } |
| 182 | } |
| 183 | |
| 184 | return dialer.DialContext(ctx, network, addr) |
| 185 | }, |
| 186 | } |
| 187 | |
| 188 | return &http.Client{ |
| 189 | Timeout: timeout, |
| 190 | Transport: transport, |
| 191 | CheckRedirect: func(req *http.Request, via []*http.Request) error { |
| 192 | if len(via) >= 10 { |
| 193 | return fmt.Errorf("too many redirects") |
| 194 | } |
| 195 | |
| 196 | // Validate the redirect URL |
| 197 | if err := ValidateURL(req.URL.String()); err != nil { |
| 198 | return fmt.Errorf("SSRF protection: redirect blocked - %w", err) |
| 199 | } |
| 200 | |
| 201 | return nil |
| 202 | }, |
| 203 | } |
| 204 | } |
| 205 | |
| 206 | // SafeGet performs a GET request with SSRF protection |
| 207 | // It validates the URL before making the request and uses a safe HTTP client |
no test coverage detected