mirror of
https://github.com/GMWalletApp/epusdt.git
synced 2026-07-07 18:26:16 +00:00
fix: reject private callback and rate API URLs
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
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()
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func withLookupIPAddr(t *testing.T, fn func(context.Context, string) ([]net.IPAddr, error)) {
|
||||
t.Helper()
|
||||
old := lookupIPAddr
|
||||
lookupIPAddr = fn
|
||||
t.Cleanup(func() {
|
||||
lookupIPAddr = old
|
||||
})
|
||||
}
|
||||
|
||||
func TestValidatePublicHTTPURLAllowsPublicHTTPURL(t *testing.T) {
|
||||
withLookupIPAddr(t, func(ctx context.Context, host string) ([]net.IPAddr, error) {
|
||||
if host != "example.com" {
|
||||
t.Fatalf("lookup host = %q, want example.com", host)
|
||||
}
|
||||
return []net.IPAddr{{IP: net.ParseIP("93.184.216.34")}}, nil
|
||||
})
|
||||
|
||||
if err := ValidatePublicHTTPURL("https://example.com/api/"); err != nil {
|
||||
t.Fatalf("ValidatePublicHTTPURL returned error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatePublicHTTPURLRejectsUnsafeURLs(t *testing.T) {
|
||||
tests := []string{
|
||||
"",
|
||||
"://bad",
|
||||
"ftp://example.com/file",
|
||||
"https:///missing-host",
|
||||
"http://localhost/notify",
|
||||
"http://localhost./notify",
|
||||
"http://127.0.0.1/notify",
|
||||
"http://[::1]/notify",
|
||||
"http://10.0.0.1/notify",
|
||||
"http://172.16.0.1/notify",
|
||||
"http://172.31.255.255/notify",
|
||||
"http://192.168.1.1/notify",
|
||||
"http://169.254.1.1/notify",
|
||||
"http://169.254.169.254/latest/meta-data/",
|
||||
}
|
||||
|
||||
for _, raw := range tests {
|
||||
t.Run(raw, func(t *testing.T) {
|
||||
if err := ValidatePublicHTTPURL(raw); err == nil {
|
||||
t.Fatalf("ValidatePublicHTTPURL(%q) returned nil, want error", raw)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatePublicHTTPURLRejectsDomainResolvingToPrivateIP(t *testing.T) {
|
||||
withLookupIPAddr(t, func(ctx context.Context, host string) ([]net.IPAddr, error) {
|
||||
return []net.IPAddr{{IP: net.ParseIP("93.184.216.34")}, {IP: net.ParseIP("10.0.0.1")}}, nil
|
||||
})
|
||||
|
||||
if err := ValidatePublicHTTPURL("https://example.com/api/"); err == nil {
|
||||
t.Fatal("ValidatePublicHTTPURL returned nil, want error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatePublicHTTPURLRejectsUnresolvableDomain(t *testing.T) {
|
||||
withLookupIPAddr(t, func(ctx context.Context, host string) ([]net.IPAddr, error) {
|
||||
return nil, errors.New("lookup failed")
|
||||
})
|
||||
|
||||
if err := ValidatePublicHTTPURL("https://example.com/api/"); err == nil {
|
||||
t.Fatal("ValidatePublicHTTPURL returned nil, want error")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user