diff --git a/host_canon_test.go b/host_canon_test.go new file mode 100644 index 0000000..2400157 --- /dev/null +++ b/host_canon_test.go @@ -0,0 +1,25 @@ +package traefikoidc + +import "testing" + +func TestValidateHostCanonicalForms(t *testing.T) { + o := &TraefikOidc{} + blocked := []string{ + "localhost.", "LOCALHOST.", "metadata.google.internal.", "metadata.google.internal", + "169.254.169.254.", "fe80::1%eth0", "[fe80::1]", "::1", + } + for _, h := range blocked { + if err := o.validateHost(h); err == nil { + t.Errorf("validateHost(%q) = nil, want blocked", h) + } + } + allowed := []string{"idp.example.com", "idp.example.com.", "2606:4700::1111"} + for _, h := range allowed { + if err := o.validateHost(h); err != nil { + t.Errorf("validateHost(%q) = %v, want allowed", h, err) + } + } + if !isLoopbackHost("localhost.") { + t.Error("isLoopbackHost(localhost.) = false") + } +} diff --git a/settings.go b/settings.go index 4df11a6..309b797 100644 --- a/settings.go +++ b/settings.go @@ -954,6 +954,7 @@ func isValidSecureURL(s string) bool { // isLoopbackHost reports whether host is "localhost" or a loopback IP literal // (127.0.0.0/8 or ::1). func isLoopbackHost(host string) bool { + host = canonicalHost(host) if strings.EqualFold(host, "localhost") { return true } diff --git a/url_helpers.go b/url_helpers.go index f3714e9..0c5ddbf 100644 --- a/url_helpers.go +++ b/url_helpers.go @@ -453,6 +453,15 @@ func sameHost(a, b string) bool { return strings.EqualFold(ua.Host, ub.Host) } +// canonicalHost drops the root dot ("localhost." is localhost) and an IPv6 +// zone id so the blocklists see one spelling per host. +func canonicalHost(host string) string { + if i := strings.IndexByte(host, '%'); i > 0 && net.ParseIP(host[:i]) != nil { + host = host[:i] + } + return strings.TrimRight(host, ".") +} + // validateHost validates a hostname or IP address for security. // It prevents access to localhost, private networks, and known metadata endpoints. // When allowPrivateIPAddresses is enabled, private IP checks are skipped. @@ -462,13 +471,14 @@ func sameHost(a, b string) bool { // Returns: // - An error if the host is dangerous or not allowed, nil if safe. func (t *TraefikOidc) validateHost(host string) error { - hostname := host - if strings.Contains(host, ":") { + hostname := canonicalHost(host) + if net.ParseIP(hostname) == nil && strings.Contains(hostname, ":") { var err error - hostname, _, err = net.SplitHostPort(host) + hostname, _, err = net.SplitHostPort(hostname) if err != nil { return fmt.Errorf("invalid host format: %w", err) } + hostname = canonicalHost(hostname) } ip := net.ParseIP(hostname)