| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182 |
- package service
- import (
- "context"
- "fmt"
- "net"
- "net/url"
- "strings"
- "time"
- )
- // SanitizeHTTPURL validates and normalizes an http(s) URL without resolving
- // DNS. Use SanitizePublicHTTPURL at the point of an outbound request.
- func SanitizeHTTPURL(raw string) (string, error) {
- raw = strings.TrimSpace(raw)
- if raw == "" {
- return "", nil
- }
- u, err := url.Parse(raw)
- if err != nil {
- return "", err
- }
- if u.Scheme != "http" && u.Scheme != "https" {
- return "", fmt.Errorf("unsupported URL scheme %q", u.Scheme)
- }
- if u.Host == "" || u.Hostname() == "" {
- return "", fmt.Errorf("URL host is required")
- }
- clean := &url.URL{
- Scheme: u.Scheme,
- Host: u.Host,
- Path: u.Path,
- RawPath: u.RawPath,
- RawQuery: u.RawQuery,
- Fragment: u.Fragment,
- }
- return clean.String(), nil
- }
- // SanitizePublicHTTPURL validates and normalizes an http(s) URL, then blocks
- // private/internal targets unless the caller explicitly allows them.
- func SanitizePublicHTTPURL(raw string, allowPrivate bool) (string, error) {
- clean, err := SanitizeHTTPURL(raw)
- if err != nil || clean == "" {
- return clean, err
- }
- if allowPrivate {
- return clean, nil
- }
- u, err := url.Parse(clean)
- if err != nil {
- return "", err
- }
- ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
- defer cancel()
- if err := rejectPrivateHost(ctx, u.Hostname()); err != nil {
- return "", err
- }
- return clean, nil
- }
- func rejectPrivateHost(ctx context.Context, hostname string) error {
- if ip := net.ParseIP(hostname); ip != nil {
- if isBlockedIP(ip) {
- return fmt.Errorf("blocked private/internal address %s", ip.String())
- }
- return nil
- }
- ips, err := net.DefaultResolver.LookupIPAddr(ctx, hostname)
- if err != nil {
- return fmt.Errorf("cannot resolve host %s: %w", hostname, err)
- }
- if len(ips) == 0 {
- return fmt.Errorf("host %s has no IP addresses", hostname)
- }
- for _, ipAddr := range ips {
- if isBlockedIP(ipAddr.IP) {
- return fmt.Errorf("host %s resolves to blocked private/internal address %s", hostname, ipAddr.IP.String())
- }
- }
- return nil
- }
|