Files
zitadel/internal/api/http/middleware/origin_interceptor_test.go
Livio Spring 7520450e11 fix: sanitize host headers before use
# Which Problems Are Solved

Host headers used to identify the instance and further used in public responses such as OIDC discovery endpoints, email links and more were not correctly handled. While they were matched against existing instances, they were not properly sanitized.

# How the Problems Are Solved

Sanitize host header including port validation (if provided).

# Additional Changes

None

# Additional Context

- requires backports

(cherry picked from commit 72a5c33e6a)
2025-10-29 10:07:05 +01:00

280 lines
6.1 KiB
Go

package middleware
import (
"net/http"
"testing"
"github.com/stretchr/testify/assert"
http_util "github.com/zitadel/zitadel/internal/api/http"
)
func Test_composeOrigin(t *testing.T) {
type args struct {
h http.Header
enforceHttps bool
}
tests := []struct {
name string
args args
want *http_util.DomainCtx
}{{
name: "no proxy headers",
want: &http_util.DomainCtx{
InstanceHost: "host.header",
Protocol: "http",
},
}, {
name: "forwarded proto",
args: args{
h: http.Header{
"Forwarded": []string{"proto=https"},
},
enforceHttps: false,
},
want: &http_util.DomainCtx{
InstanceHost: "host.header",
Protocol: "https",
},
}, {
name: "forwarded host",
args: args{
h: http.Header{
"Forwarded": []string{"host=forwarded.host"},
},
enforceHttps: false,
},
want: &http_util.DomainCtx{
InstanceHost: "forwarded.host",
Protocol: "http",
},
}, {
name: "forwarded proto and host",
args: args{
h: http.Header{
"Forwarded": []string{"proto=https;host=forwarded.host"},
},
enforceHttps: false,
},
want: &http_util.DomainCtx{
InstanceHost: "forwarded.host",
Protocol: "https",
},
}, {
name: "forwarded proto and host with multiple complete entries",
args: args{
h: http.Header{
"Forwarded": []string{"proto=https;host=forwarded.host, proto=http;host=forwarded.host2"},
},
enforceHttps: false,
},
want: &http_util.DomainCtx{
InstanceHost: "forwarded.host",
Protocol: "https",
},
}, {
name: "forwarded proto and host with multiple incomplete entries",
args: args{
h: http.Header{
"Forwarded": []string{"proto=https;host=forwarded.host, proto=http"},
},
enforceHttps: false,
},
want: &http_util.DomainCtx{
InstanceHost: "forwarded.host",
Protocol: "https",
},
}, {
name: "forwarded proto and host with incomplete entries in different values",
args: args{
h: http.Header{
"Forwarded": []string{"proto=http", "proto=https;host=forwarded.host", "proto=http"},
},
enforceHttps: true,
},
want: &http_util.DomainCtx{
InstanceHost: "forwarded.host",
Protocol: "https",
},
}, {
name: "x-forwarded-proto https",
args: args{
h: http.Header{
"X-Forwarded-Proto": []string{"https"},
},
enforceHttps: false,
},
want: &http_util.DomainCtx{
InstanceHost: "host.header",
Protocol: "https",
},
}, {
name: "x-forwarded-proto http",
args: args{
h: http.Header{
"X-Forwarded-Proto": []string{"http"},
},
enforceHttps: true,
},
want: &http_util.DomainCtx{
InstanceHost: "host.header",
Protocol: "https",
},
}, {
name: "fallback to http",
args: args{
enforceHttps: false,
},
want: &http_util.DomainCtx{
InstanceHost: "host.header",
Protocol: "http",
},
}, {
name: "enforce https",
args: args{
enforceHttps: true,
},
want: &http_util.DomainCtx{
InstanceHost: "host.header",
Protocol: "https",
},
}, {
name: "x-forwarded-host",
args: args{
h: http.Header{
"X-Forwarded-Host": []string{"x-forwarded.host"},
},
enforceHttps: false,
},
want: &http_util.DomainCtx{
InstanceHost: "x-forwarded.host",
Protocol: "http",
},
}, {
name: "x-forwarded-proto and x-forwarded-host",
args: args{
h: http.Header{
"X-Forwarded-Proto": []string{"https"},
"X-Forwarded-Host": []string{"x-forwarded.host"},
},
enforceHttps: false,
},
want: &http_util.DomainCtx{
InstanceHost: "x-forwarded.host",
Protocol: "https",
},
}, {
name: "forwarded host and x-forwarded-host",
args: args{
h: http.Header{
"Forwarded": []string{"host=forwarded.host"},
"X-Forwarded-Host": []string{"x-forwarded.host"},
},
enforceHttps: false,
},
want: &http_util.DomainCtx{
InstanceHost: "forwarded.host",
Protocol: "http",
},
}, {
name: "forwarded host and x-forwarded-proto",
args: args{
h: http.Header{
"Forwarded": []string{"host=forwarded.host"},
"X-Forwarded-Proto": []string{"https"},
},
enforceHttps: false,
},
want: &http_util.DomainCtx{
InstanceHost: "forwarded.host",
Protocol: "https",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equalf(t, tt.want, composeDomainContext(
&http.Request{
Host: "host.header",
Header: tt.args.h,
},
tt.args.enforceHttps,
[]string{http_util.Forwarded, http_util.ForwardedFor, http_util.ForwardedHost, http_util.ForwardedProto},
[]string{"x-zitadel-public-host"},
), "headers: %+v, enforceHttps: %t", tt.args.h, tt.args.enforceHttps)
})
}
}
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)
})
}
}