package woocommerce import ( "errors" "fmt" "net" "net/url" "strings" ) var ( ErrInvalidStoreURL = errors.New("invalid store url") ErrBlockedStoreURL = errors.New("store url host is not allowed") ) // NormalizeStoreURL validates and normalizes a WooCommerce store base URL. // Requires https (http only for localhost). Blocks metadata and private ranges (SSRF). func NormalizeStoreURL(raw string) (string, error) { raw = strings.TrimSpace(raw) if raw == "" { return "", ErrInvalidStoreURL } if !strings.Contains(raw, "://") { raw = "https://" + raw } u, err := url.Parse(raw) if err != nil || u.Host == "" { return "", ErrInvalidStoreURL } scheme := strings.ToLower(u.Scheme) if scheme != "https" && scheme != "http" { return "", ErrInvalidStoreURL } host := strings.ToLower(u.Hostname()) if host == "" { return "", ErrInvalidStoreURL } if host == "metadata.google.internal" || host == "metadata" { return "", ErrBlockedStoreURL } ips, err := resolveHostIPs(host) if err != nil { if isLiteralIP(host) { return "", err } } else { for _, ip := range ips { if !allowedIP(ip, host) { return "", ErrBlockedStoreURL } } } if scheme == "http" && !isLoopbackHost(host) { return "", fmt.Errorf("%w: https required (http only allowed for localhost)", ErrInvalidStoreURL) } u.Scheme = scheme if u.Port() != "" { u.Host = net.JoinHostPort(u.Hostname(), u.Port()) } else { u.Host = u.Hostname() } u.Path = strings.TrimRight(u.Path, "/") u.RawQuery = "" u.Fragment = "" u.User = nil return u.String(), nil } func resolveHostIPs(host string) ([]net.IP, error) { if ip := net.ParseIP(host); ip != nil { return []net.IP{ip}, nil } addrs, err := net.LookupIP(host) if err != nil { return nil, ErrInvalidStoreURL } return addrs, nil } func isLiteralIP(host string) bool { return net.ParseIP(host) != nil } func isLoopbackHost(host string) bool { if host == "localhost" { return true } ip := net.ParseIP(host) return ip != nil && ip.IsLoopback() } func allowedIP(ip net.IP, host string) bool { if ip.IsLoopback() { return isLoopbackHost(host) } if ip.IsUnspecified() || ip.IsMulticast() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() { return false } if ip4 := ip.To4(); ip4 != nil { if ip4[0] == 10 { return false } if ip4[0] == 172 && ip4[1] >= 16 && ip4[1] <= 31 { return false } if ip4[0] == 192 && ip4[1] == 168 { return false } if ip4[0] == 169 && ip4[1] == 254 { return false } if ip4[0] == 100 && ip4[1] >= 64 && ip4[1] <= 127 { return false } } else if ip.IsPrivate() { return false } return true }