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.
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user