fix: validate proto header and provide https enforcement (#9975)

# Which Problems Are Solved

ZITADEL uses the notification triggering requests Forwarded or
X-Forwarded-Proto header to build the button link sent in emails for
confirming a password reset with the emailed code. If this header is
overwritten and a user clicks the link to a malicious site in the email,
the secret code can be retrieved and used to reset the users password
and take over his account.

Accounts with MFA or Passwordless enabled can not be taken over by this
attack.

# How the Problems Are Solved

- The `X-Forwarded-Proto` and `proto` of the Forwarded headers are
validated (http / https).
- Additionally, when exposing ZITADEL through https. An overwrite to
http is no longer possible.

# Additional Changes

None

# Additional Context

None

(cherry picked from commit c097887bc5)
This commit is contained in:
Livio Spring
2025-05-28 10:12:27 +02:00
parent 7fed732d3c
commit 373f9fd418
2 changed files with 39 additions and 35 deletions

View File

@@ -10,12 +10,12 @@ import (
http_util "github.com/zitadel/zitadel/internal/api/http" http_util "github.com/zitadel/zitadel/internal/api/http"
) )
func WithOrigin(fallBackToHttps bool, http1Header, http2Header string, instanceHostHeaders, publicDomainHeaders []string) mux.MiddlewareFunc { func WithOrigin(enforceHttps bool, http1Header, http2Header string, instanceHostHeaders, publicDomainHeaders []string) mux.MiddlewareFunc {
return func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := composeDomainContext( origin := composeDomainContext(
r, r,
fallBackToHttps, 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)), slices.Compact(append(instanceHostHeaders, http1Header, http2Header, http_util.Forwarded, http_util.ZitadelForwarded, http_util.ForwardedFor, http_util.ForwardedHost, http_util.ForwardedProto)),
publicDomainHeaders, publicDomainHeaders,
@@ -25,28 +25,32 @@ func WithOrigin(fallBackToHttps bool, http1Header, http2Header string, instanceH
} }
} }
func composeDomainContext(r *http.Request, fallBackToHttps 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) instanceHost, instanceProto := hostFromRequest(r, instanceDomainHeaders)
publicHost, publicProto := hostFromRequest(r, publicDomainHeaders) publicHost, publicProto := hostFromRequest(r, publicDomainHeaders)
if publicProto == "" {
publicProto = instanceProto
}
if publicProto == "" {
publicProto = "http"
if fallBackToHttps {
publicProto = "https"
}
}
if instanceHost == "" { if instanceHost == "" {
instanceHost = r.Host instanceHost = r.Host
} }
return &http_util.DomainCtx{ return &http_util.DomainCtx{
InstanceHost: instanceHost, InstanceHost: instanceHost,
Protocol: publicProto, Protocol: protocolFromRequest(instanceProto, publicProto, enforceHttps),
PublicHost: publicHost, PublicHost: publicHost,
} }
} }
func protocolFromRequest(instanceProto, publicProto string, enforceHttps bool) string {
if enforceHttps {
return "https"
}
if publicProto != "" {
return publicProto
}
if instanceProto != "" {
return instanceProto
}
return "http"
}
func hostFromRequest(r *http.Request, headers []string) (host, proto string) { func hostFromRequest(r *http.Request, headers []string) (host, proto string) {
var hostFromHeader, protoFromHeader string var hostFromHeader, protoFromHeader string
for _, header := range headers { for _, header := range headers {
@@ -65,7 +69,7 @@ func hostFromRequest(r *http.Request, headers []string) (host, proto string) {
if host == "" { if host == "" {
host = hostFromHeader host = hostFromHeader
} }
if proto == "" { if proto == "" && (protoFromHeader == "http" || protoFromHeader == "https") {
proto = protoFromHeader proto = protoFromHeader
} }
} }

View File

@@ -11,8 +11,8 @@ import (
func Test_composeOrigin(t *testing.T) { func Test_composeOrigin(t *testing.T) {
type args struct { type args struct {
h http.Header h http.Header
fallBackToHttps bool enforceHttps bool
} }
tests := []struct { tests := []struct {
name string name string
@@ -30,7 +30,7 @@ func Test_composeOrigin(t *testing.T) {
h: http.Header{ h: http.Header{
"Forwarded": []string{"proto=https"}, "Forwarded": []string{"proto=https"},
}, },
fallBackToHttps: false, enforceHttps: false,
}, },
want: &http_util.DomainCtx{ want: &http_util.DomainCtx{
InstanceHost: "host.header", InstanceHost: "host.header",
@@ -42,7 +42,7 @@ func Test_composeOrigin(t *testing.T) {
h: http.Header{ h: http.Header{
"Forwarded": []string{"host=forwarded.host"}, "Forwarded": []string{"host=forwarded.host"},
}, },
fallBackToHttps: false, enforceHttps: false,
}, },
want: &http_util.DomainCtx{ want: &http_util.DomainCtx{
InstanceHost: "forwarded.host", InstanceHost: "forwarded.host",
@@ -54,7 +54,7 @@ func Test_composeOrigin(t *testing.T) {
h: http.Header{ h: http.Header{
"Forwarded": []string{"proto=https;host=forwarded.host"}, "Forwarded": []string{"proto=https;host=forwarded.host"},
}, },
fallBackToHttps: false, enforceHttps: false,
}, },
want: &http_util.DomainCtx{ want: &http_util.DomainCtx{
InstanceHost: "forwarded.host", InstanceHost: "forwarded.host",
@@ -66,7 +66,7 @@ func Test_composeOrigin(t *testing.T) {
h: http.Header{ h: http.Header{
"Forwarded": []string{"proto=https;host=forwarded.host, proto=http;host=forwarded.host2"}, "Forwarded": []string{"proto=https;host=forwarded.host, proto=http;host=forwarded.host2"},
}, },
fallBackToHttps: false, enforceHttps: false,
}, },
want: &http_util.DomainCtx{ want: &http_util.DomainCtx{
InstanceHost: "forwarded.host", InstanceHost: "forwarded.host",
@@ -78,7 +78,7 @@ func Test_composeOrigin(t *testing.T) {
h: http.Header{ h: http.Header{
"Forwarded": []string{"proto=https;host=forwarded.host, proto=http"}, "Forwarded": []string{"proto=https;host=forwarded.host, proto=http"},
}, },
fallBackToHttps: false, enforceHttps: false,
}, },
want: &http_util.DomainCtx{ want: &http_util.DomainCtx{
InstanceHost: "forwarded.host", InstanceHost: "forwarded.host",
@@ -90,11 +90,11 @@ func Test_composeOrigin(t *testing.T) {
h: http.Header{ h: http.Header{
"Forwarded": []string{"proto=http", "proto=https;host=forwarded.host", "proto=http"}, "Forwarded": []string{"proto=http", "proto=https;host=forwarded.host", "proto=http"},
}, },
fallBackToHttps: true, enforceHttps: true,
}, },
want: &http_util.DomainCtx{ want: &http_util.DomainCtx{
InstanceHost: "forwarded.host", InstanceHost: "forwarded.host",
Protocol: "http", Protocol: "https",
}, },
}, { }, {
name: "x-forwarded-proto https", name: "x-forwarded-proto https",
@@ -102,7 +102,7 @@ func Test_composeOrigin(t *testing.T) {
h: http.Header{ h: http.Header{
"X-Forwarded-Proto": []string{"https"}, "X-Forwarded-Proto": []string{"https"},
}, },
fallBackToHttps: false, enforceHttps: false,
}, },
want: &http_util.DomainCtx{ want: &http_util.DomainCtx{
InstanceHost: "host.header", InstanceHost: "host.header",
@@ -114,25 +114,25 @@ func Test_composeOrigin(t *testing.T) {
h: http.Header{ h: http.Header{
"X-Forwarded-Proto": []string{"http"}, "X-Forwarded-Proto": []string{"http"},
}, },
fallBackToHttps: true, enforceHttps: true,
}, },
want: &http_util.DomainCtx{ want: &http_util.DomainCtx{
InstanceHost: "host.header", InstanceHost: "host.header",
Protocol: "http", Protocol: "https",
}, },
}, { }, {
name: "fallback to http", name: "fallback to http",
args: args{ args: args{
fallBackToHttps: false, enforceHttps: false,
}, },
want: &http_util.DomainCtx{ want: &http_util.DomainCtx{
InstanceHost: "host.header", InstanceHost: "host.header",
Protocol: "http", Protocol: "http",
}, },
}, { }, {
name: "fallback to https", name: "enforce https",
args: args{ args: args{
fallBackToHttps: true, enforceHttps: true,
}, },
want: &http_util.DomainCtx{ want: &http_util.DomainCtx{
InstanceHost: "host.header", InstanceHost: "host.header",
@@ -144,7 +144,7 @@ func Test_composeOrigin(t *testing.T) {
h: http.Header{ h: http.Header{
"X-Forwarded-Host": []string{"x-forwarded.host"}, "X-Forwarded-Host": []string{"x-forwarded.host"},
}, },
fallBackToHttps: false, enforceHttps: false,
}, },
want: &http_util.DomainCtx{ want: &http_util.DomainCtx{
InstanceHost: "x-forwarded.host", InstanceHost: "x-forwarded.host",
@@ -157,7 +157,7 @@ func Test_composeOrigin(t *testing.T) {
"X-Forwarded-Proto": []string{"https"}, "X-Forwarded-Proto": []string{"https"},
"X-Forwarded-Host": []string{"x-forwarded.host"}, "X-Forwarded-Host": []string{"x-forwarded.host"},
}, },
fallBackToHttps: false, enforceHttps: false,
}, },
want: &http_util.DomainCtx{ want: &http_util.DomainCtx{
InstanceHost: "x-forwarded.host", InstanceHost: "x-forwarded.host",
@@ -170,7 +170,7 @@ func Test_composeOrigin(t *testing.T) {
"Forwarded": []string{"host=forwarded.host"}, "Forwarded": []string{"host=forwarded.host"},
"X-Forwarded-Host": []string{"x-forwarded.host"}, "X-Forwarded-Host": []string{"x-forwarded.host"},
}, },
fallBackToHttps: false, enforceHttps: false,
}, },
want: &http_util.DomainCtx{ want: &http_util.DomainCtx{
InstanceHost: "forwarded.host", InstanceHost: "forwarded.host",
@@ -183,7 +183,7 @@ func Test_composeOrigin(t *testing.T) {
"Forwarded": []string{"host=forwarded.host"}, "Forwarded": []string{"host=forwarded.host"},
"X-Forwarded-Proto": []string{"https"}, "X-Forwarded-Proto": []string{"https"},
}, },
fallBackToHttps: false, enforceHttps: false,
}, },
want: &http_util.DomainCtx{ want: &http_util.DomainCtx{
InstanceHost: "forwarded.host", InstanceHost: "forwarded.host",
@@ -198,10 +198,10 @@ func Test_composeOrigin(t *testing.T) {
Host: "host.header", Host: "host.header",
Header: tt.args.h, Header: tt.args.h,
}, },
tt.args.fallBackToHttps, tt.args.enforceHttps,
[]string{http_util.Forwarded, http_util.ForwardedFor, http_util.ForwardedHost, http_util.ForwardedProto}, []string{http_util.Forwarded, http_util.ForwardedFor, http_util.ForwardedHost, http_util.ForwardedProto},
[]string{"x-zitadel-public-host"}, []string{"x-zitadel-public-host"},
), "headers: %+v, fallBackToHttps: %t", tt.args.h, tt.args.fallBackToHttps) ), "headers: %+v, enforceHttps: %t", tt.args.h, tt.args.enforceHttps)
}) })
} }
} }