mirror of
https://github.com/GMWalletApp/epusdt.git
synced 2026-07-07 10:16:15 +00:00
89 lines
2.0 KiB
Go
89 lines
2.0 KiB
Go
package security
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const dnsLookupTimeout = 3 * time.Second
|
|
|
|
var lookupIPAddr = func(ctx context.Context, host string) ([]net.IPAddr, error) {
|
|
return net.DefaultResolver.LookupIPAddr(ctx, host)
|
|
}
|
|
|
|
// ValidatePublicHTTPURL verifies that raw is an HTTP(S) URL whose host does not
|
|
// point to localhost, private networks, link-local addresses, or metadata IPs.
|
|
func ValidatePublicHTTPURL(raw string) error {
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" {
|
|
return errors.New("url is required")
|
|
}
|
|
|
|
parsed, err := url.Parse(raw)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid url: %w", err)
|
|
}
|
|
|
|
scheme := strings.ToLower(parsed.Scheme)
|
|
if scheme != "http" && scheme != "https" {
|
|
return errors.New("url scheme must be http or https")
|
|
}
|
|
|
|
host := strings.TrimSpace(parsed.Hostname())
|
|
if host == "" {
|
|
return errors.New("url host is required")
|
|
}
|
|
if strings.Contains(host, "%") {
|
|
return fmt.Errorf("url host %q is not allowed", host)
|
|
}
|
|
if strings.TrimSuffix(strings.ToLower(host), ".") == "localhost" {
|
|
return fmt.Errorf("url host %q is not allowed", host)
|
|
}
|
|
|
|
if ip := net.ParseIP(host); ip != nil {
|
|
return validatePublicIP(host, ip)
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), dnsLookupTimeout)
|
|
defer cancel()
|
|
|
|
addrs, err := lookupIPAddr(ctx, host)
|
|
if err != nil {
|
|
return fmt.Errorf("resolve url host %q: %w", host, err)
|
|
}
|
|
if len(addrs) == 0 {
|
|
return fmt.Errorf("resolve url host %q: no addresses", host)
|
|
}
|
|
for _, addr := range addrs {
|
|
if err := validatePublicIP(host, addr.IP); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validatePublicIP(host string, ip net.IP) error {
|
|
if isUnsafeIP(ip) {
|
|
return fmt.Errorf("url host %q resolves to disallowed address %s", host, ip.String())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func isUnsafeIP(ip net.IP) bool {
|
|
if ip == nil {
|
|
return true
|
|
}
|
|
return ip.IsLoopback() ||
|
|
ip.IsPrivate() ||
|
|
ip.IsUnspecified() ||
|
|
ip.IsLinkLocalUnicast() ||
|
|
ip.IsLinkLocalMulticast() ||
|
|
ip.IsMulticast() ||
|
|
!ip.IsGlobalUnicast()
|
|
}
|