validateWebTarget validates rawURL against the SSRF policy and returns a canonical, re-encoded URL string safe to hand to the HTTP client. Hostname targets are only checked for scheme/metadata here; their resolved IP is enforced at dial time by ssrfDialControl (rebinding-proof).
(rawURL string)
| 69 | // targets are only checked for scheme/metadata here; their resolved IP is |
| 70 | // enforced at dial time by ssrfDialControl (rebinding-proof). |
| 71 | func validateWebTarget(rawURL string) (string, error) { |
| 72 | parsed, err := url.Parse(strings.TrimSpace(rawURL)) |
| 73 | if err != nil { |
| 74 | return "", fmt.Errorf("invalid URL: %w", err) |
| 75 | } |
| 76 | |
| 77 | switch parsed.Scheme { |
| 78 | case "http", "https": |
| 79 | // allowed |
| 80 | default: |
| 81 | return "", fmt.Errorf("unsupported URL scheme %q (only http/https are allowed)", parsed.Scheme) |
| 82 | } |
| 83 | |
| 84 | host := parsed.Hostname() |
| 85 | if host == "" { |
| 86 | return "", fmt.Errorf("missing host in URL") |
| 87 | } |
| 88 | |
| 89 | if isMetadataHostname(host) { |
| 90 | return "", fmt.Errorf("blocked cloud metadata hostname %q", host) |
| 91 | } |
| 92 | |
| 93 | // A literal IP is checked immediately (no DNS needed). Hostnames are |
| 94 | // enforced at dial time. |
| 95 | if ip := net.ParseIP(host); ip != nil { |
| 96 | if err := checkWebIP(ip, webBlockPrivate(), host); err != nil { |
| 97 | return "", err |
| 98 | } |
| 99 | } |
| 100 | |
| 101 | return parsed.String(), nil |
| 102 | } |
| 103 | |
| 104 | // ssrfDialControl is installed as net.Dialer.Control on the web transport. It |
| 105 | // runs after DNS resolution with the concrete address about to be dialed, |