Files
descrybe/apps/api/internal/woocommerce/url.go
T
greeneclipse 8580c996c3 Initial commit of Descrybe v2 without local scratch artifacts.
Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
2026-08-09 22:47:43 +02:00

120 lines
2.6 KiB
Go

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
}