Merge commit from fork

* fix: sanitize host headers before use

* add additional test
This commit is contained in:
Livio Spring
2025-10-29 10:05:37 +01:00
committed by GitHub
parent f4503e07cd
commit 72a5c33e6a
10 changed files with 157 additions and 32 deletions
@@ -88,10 +88,10 @@ func setInstance(ctx context.Context, verifier authz.InstanceVerifier) (_ contex
defer func() { span.EndWithError(err) }()
requestContext := zitadel_http.DomainContext(ctx)
if requestContext.InstanceHost == "" {
if requestContext.InstanceDomain() == "" {
return nil, zerrors.ThrowNotFound(err, "INST-zWq7X", "Errors.IAM.NotFound")
}
instance, err := verifier.InstanceByHost(authCtx, requestContext.InstanceHost, requestContext.PublicHost)
instance, err := verifier.InstanceByHost(authCtx, requestContext.InstanceDomain(), requestContext.RequestedDomain())
if err != nil {
return nil, err
}
@@ -1,11 +1,15 @@
package middleware
import (
"errors"
"net"
"net/http"
"slices"
"strconv"
"github.com/gorilla/mux"
"github.com/muhlemmer/httpforwarded"
"github.com/zitadel/logging"
http_util "github.com/zitadel/zitadel/internal/api/http"
)
@@ -16,7 +20,7 @@ func WithOrigin(enforceHttps bool, http1Header, http2Header string, instanceHost
origin := composeDomainContext(
r,
enforceHttps,
// to make sure we don't break existing configurations we append the existing checked headers as well
// to make sure we don't break existing configurations, we append the existing checked headers as well
slices.Compact(append(instanceHostHeaders, http1Header, http2Header, http_util.Forwarded, http_util.ZitadelForwarded, http_util.ForwardedFor, http_util.ForwardedHost, http_util.ForwardedProto)),
publicDomainHeaders,
)
@@ -25,17 +29,13 @@ func WithOrigin(enforceHttps bool, http1Header, http2Header string, instanceHost
}
}
func composeDomainContext(r *http.Request, enforceHttps bool, instanceDomainHeaders, publicDomainHeaders []string) *http_util.DomainCtx {
func composeDomainContext(r *http.Request, enforceHttps bool, instanceDomainHeaders, publicDomainHeaders []string) (_ *http_util.DomainCtx) {
instanceHost, instanceProto := hostFromRequest(r, instanceDomainHeaders)
publicHost, publicProto := hostFromRequest(r, publicDomainHeaders)
if instanceHost == "" {
instanceHost = r.Host
}
return &http_util.DomainCtx{
InstanceHost: instanceHost,
Protocol: protocolFromRequest(instanceProto, publicProto, enforceHttps),
PublicHost: publicHost,
}
return http_util.NewDomainCtx(instanceHost, publicHost, protocolFromRequest(instanceProto, publicProto, enforceHttps))
}
func protocolFromRequest(instanceProto, publicProto string, enforceHttps bool) string {
@@ -67,7 +67,7 @@ func hostFromRequest(r *http.Request, headers []string) (host, proto string) {
hostFromHeader = r.Header.Get(header)
}
if host == "" {
host = hostFromHeader
host = sanitizeHost(hostFromHeader)
}
if proto == "" && (protoFromHeader == "http" || protoFromHeader == "https") {
proto = protoFromHeader
@@ -76,6 +76,35 @@ func hostFromRequest(r *http.Request, headers []string) (host, proto string) {
return host, proto
}
func sanitizeHost(rawHost string) (host string) {
if rawHost == "" {
return ""
}
host, port, err := net.SplitHostPort(rawHost)
if err != nil {
// if the error is about a missing port, the host is probably just "example.com", so we can return it
if isMissingPortError(err) {
return rawHost
}
// if the error is about something else, the host is probably invalid, so we log it and return an empty string
logging.WithFields("host", rawHost).Warning("invalid host header, ignoring header")
return ""
}
// if the port is not numeric, the host was probably something like "localhost:@attacker.com"
portNumber, err := strconv.Atoi(port)
if err != nil || portNumber < 1 || portNumber > 65535 {
logging.WithFields("host", rawHost).Warning("invalid port in host header, ignoring header")
return ""
}
// if we reach this point, the host contains a valid port, so we return the complete host
return rawHost
}
func isMissingPortError(err error) bool {
var addrErr *net.AddrError
return errors.As(err, &addrErr) && (addrErr.Err == "missing port in address")
}
func hostFromForwarded(values []string) (string, string) {
fwd, fwdErr := httpforwarded.Parse(values)
if fwdErr == nil {
@@ -205,3 +205,75 @@ func Test_composeOrigin(t *testing.T) {
})
}
}
func Test_sanitizeHost(t *testing.T) {
type args struct {
rawHost string
}
tests := []struct {
name string
args args
want string
}{
{
name: "normal host",
args: args{rawHost: "example.com"},
want: "example.com",
},
{
name: "host with port",
args: args{rawHost: "example.com:8080"},
want: "example.com:8080",
},
{
name: "ipv4",
args: args{rawHost: "192.168.1.1"},
want: "192.168.1.1",
},
{
name: "ipv4 with port",
args: args{rawHost: "192.168.1.1:8080"},
want: "192.168.1.1:8080",
},
{
name: "ipv6",
args: args{rawHost: "[2001:0db8:85a3:0000:0000:8a2e:0370:7334]"},
want: "[2001:0db8:85a3:0000:0000:8a2e:0370:7334]",
},
{
name: "ipv6 with port",
args: args{rawHost: "[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:8080"},
want: "[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:8080",
},
{
name: "host with trailing colon",
args: args{rawHost: "example.com:"},
want: "",
},
{
name: "host with invalid port",
args: args{rawHost: "example.com:port"},
want: "",
},
{
name: "invalid host",
args: args{rawHost: "localhost:@attacker.com"},
want: "",
},
{
name: "invalid host",
args: args{rawHost: "localhost:@attacker.com:8080"},
want: "",
},
{
name: "empty host",
args: args{rawHost: ""},
want: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equalf(t, tt.want, sanitizeHost(tt.args.rawHost), "sanitizeHost(%v)", tt.args.rawHost)
})
}
}