Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
120 lines
2.6 KiB
Go
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
|
|
}
|