diff --git a/docs/user-guide/nginx-configuration/custom-template.md b/docs/user-guide/nginx-configuration/custom-template.md index 211223025e..d00804c36b 100644 --- a/docs/user-guide/nginx-configuration/custom-template.md +++ b/docs/user-guide/nginx-configuration/custom-template.md @@ -44,6 +44,7 @@ TODO: - buildDenyVariable: - buildUpstreamName: - buildForwardedFor: +- buildForwardedHost: - buildAuthSignURL: - buildNextUpstream: - filterRateLimits: diff --git a/internal/ingress/controller/config/config.go b/internal/ingress/controller/config/config.go index 2cbc10f6d9..1a1c1098a2 100644 --- a/internal/ingress/controller/config/config.go +++ b/internal/ingress/controller/config/config.go @@ -567,6 +567,10 @@ type Configuration struct { // Default is X-Forwarded-For ForwardedForHeader string `json:"forwarded-for-header,omitempty"` + // Sets the header field for identifying the originating Host header of a client + // Default is X-Forwarded-Host + ForwardedHostHeader string `json:"forwarded-host-header,omitempty"` + // Append the remote address to the X-Forwarded-For header instead of replacing it // Default: false ComputeFullForwardedFor bool `json:"compute-full-forwarded-for,omitempty"` @@ -778,6 +782,7 @@ func NewDefault() Configuration { UseForwardedHeaders: false, EnableRealIP: false, ForwardedForHeader: "X-Forwarded-For", + ForwardedHostHeader: "X-Forwarded-Host", ComputeFullForwardedFor: false, ProxyAddOriginalURIHeader: false, GenerateRequestID: true, diff --git a/internal/ingress/controller/template/template.go b/internal/ingress/controller/template/template.go index ed052e4ecf..70680ddaca 100644 --- a/internal/ingress/controller/template/template.go +++ b/internal/ingress/controller/template/template.go @@ -314,6 +314,7 @@ var funcMap = text_template.FuncMap{ }, "isValidByteSize": isValidByteSize, "buildForwardedFor": buildForwardedFor, + "buildForwardedHost": buildForwardedHost, "buildAuthSignURL": buildAuthSignURL, "buildAuthSignURLLocation": buildAuthSignURLLocation, "buildOpentelemetry": buildOpentelemetry, @@ -1153,6 +1154,18 @@ func buildForwardedFor(input interface{}) string { return fmt.Sprintf("$http_%v", ffh) } +func buildForwardedHost(input interface{}) string { + s, ok := input.(string) + if !ok { + klog.Errorf("expected a 'string' type but %T was returned", input) + return "" + } + + fhh := strings.ReplaceAll(s, "-", "_") + fhh = strings.ToLower(fhh) + return fmt.Sprintf("$http_%v", fhh) +} + func buildAuthSignURL(authSignURL, authRedirectParam string) string { u, err := url.Parse(authSignURL) if err != nil { diff --git a/internal/ingress/controller/template/template_test.go b/internal/ingress/controller/template/template_test.go index 6553f5daf9..428bdb0bc9 100644 --- a/internal/ingress/controller/template/template_test.go +++ b/internal/ingress/controller/template/template_test.go @@ -857,6 +857,24 @@ func TestBuildForwardedFor(t *testing.T) { } } +func TestBuildForwardedHost(t *testing.T) { + invalidType := &ingress.Ingress{} + expected := "" + actual := buildForwardedHost(invalidType) + + if expected != actual { + t.Errorf("Expected '%v' but returned '%v'", expected, actual) + } + + inputStr := "X-Forwarded-Host" + expected = "$http_x_forwarded_host" + actual = buildForwardedHost(inputStr) + + if expected != actual { + t.Errorf("Expected '%v' but returned '%v'", expected, actual) + } +} + func TestBuildResolvers(t *testing.T) { ipOne := net.ParseIP("192.0.0.1") ipTwo := net.ParseIP("2001:db8:1234:0000:0000:0000:0000:0000") diff --git a/rootfs/etc/nginx/template/nginx.tmpl b/rootfs/etc/nginx/template/nginx.tmpl index 6b8e750b06..536f0aa005 100644 --- a/rootfs/etc/nginx/template/nginx.tmpl +++ b/rootfs/etc/nginx/template/nginx.tmpl @@ -1279,7 +1279,9 @@ stream { {{ $proxySetHeader }} X-Scheme $pass_access_scheme; # Pass the original X-Forwarded-For - {{ $proxySetHeader }} X-Original-Forwarded-For {{ buildForwardedFor $all.Cfg.ForwardedForHeader }}; + {{ $proxySetHeader }} X-Original-Forwarded-For {{ buildForwardedFor $all.Cfg.ForwardedForHeader }}; + # Pass the original X-Forwarded-Host + {{ $proxySetHeader }} X-Original-Forwarded-Host {{ buildForwardedHost $all.Cfg.ForwardedHostHeader }}; # mitigate HTTPoxy Vulnerability # https://www.nginx.com/blog/mitigating-the-httpoxy-vulnerability-with-nginx/ diff --git a/test/e2e/settings/enable_real_ip.go b/test/e2e/settings/enable_real_ip.go index bf16e1ea04..c56a8aefa1 100644 --- a/test/e2e/settings/enable_real_ip.go +++ b/test/e2e/settings/enable_real_ip.go @@ -65,7 +65,8 @@ var _ = framework.DescribeSetting("enable-real-ip", func() { Body(). Raw() - assert.NotContains(ginkgo.GinkgoT(), body, "host=myhost") + // we use a regexp to prevent matching the expression in the middle of the x-original-forwarded-host header + assert.NotRegexp(ginkgo.GinkgoT(), `(\s)host=myhost`, body) assert.NotContains(ginkgo.GinkgoT(), body, "x-forwarded-host=myhost") assert.NotContains(ginkgo.GinkgoT(), body, "x-forwarded-proto=myproto") assert.NotContains(ginkgo.GinkgoT(), body, "x-forwarded-port=1234") @@ -105,7 +106,9 @@ var _ = framework.DescribeSetting("enable-real-ip", func() { assert.Contains(ginkgo.GinkgoT(), body, "x-forwarded-port=80") assert.Contains(ginkgo.GinkgoT(), body, "x-forwarded-proto=http") assert.Contains(ginkgo.GinkgoT(), body, "x-original-forwarded-for=1.2.3.4") - assert.NotContains(ginkgo.GinkgoT(), body, "host=myhost") + assert.Contains(ginkgo.GinkgoT(), body, "x-original-forwarded-host=myhost") + // we use a regexp to prevent matching the expression in the middle of the x-original-forwarded-host header + assert.NotRegexp(ginkgo.GinkgoT(), `(\s)host=myhost`, body) assert.NotContains(ginkgo.GinkgoT(), body, "x-forwarded-host=myhost") assert.NotContains(ginkgo.GinkgoT(), body, "x-forwarded-proto=myproto") assert.NotContains(ginkgo.GinkgoT(), body, "x-forwarded-port=1234") diff --git a/test/e2e/settings/forwarded_headers.go b/test/e2e/settings/forwarded_headers.go index 44460aca64..d0c29ebaa1 100644 --- a/test/e2e/settings/forwarded_headers.go +++ b/test/e2e/settings/forwarded_headers.go @@ -66,7 +66,8 @@ var _ = framework.DescribeSetting("use-forwarded-headers", func() { Body(). Raw() - assert.Contains(ginkgo.GinkgoT(), body, "host=myhost") + // we use a regexp to prevent matching the expression in the middle of the x-original-forwarded-host header + assert.Regexp(ginkgo.GinkgoT(), `(\s)host=myhost`, body) assert.Contains(ginkgo.GinkgoT(), body, "x-forwarded-host=myhost") assert.Contains(ginkgo.GinkgoT(), body, "x-forwarded-proto=myproto") assert.Contains(ginkgo.GinkgoT(), body, "x-forwarded-scheme=myproto") @@ -86,7 +87,8 @@ var _ = framework.DescribeSetting("use-forwarded-headers", func() { Body(). Raw() - assert.Contains(ginkgo.GinkgoT(), body, "host=myhost.com") + // we use a regexp to prevent matching the expression in the middle of the x-original-forwarded-host header + assert.Regexp(ginkgo.GinkgoT(), `(\s)host=myhost.com`, body) assert.Contains(ginkgo.GinkgoT(), body, "x-forwarded-host=myhost.com") }) @@ -121,7 +123,9 @@ var _ = framework.DescribeSetting("use-forwarded-headers", func() { assert.Contains(ginkgo.GinkgoT(), body, "x-forwarded-proto=http") assert.Contains(ginkgo.GinkgoT(), body, "x-forwarded-scheme=http") assert.Contains(ginkgo.GinkgoT(), body, "x-original-forwarded-for=1.2.3.4") - assert.NotContains(ginkgo.GinkgoT(), body, "host=myhost") + assert.Contains(ginkgo.GinkgoT(), body, "x-original-forwarded-host=myhost") + // we use a regexp to prevent matching the expression in the middle of the x-original-forwarded-host header + assert.NotRegexp(ginkgo.GinkgoT(), `(\s)host=myhost`, body) assert.NotContains(ginkgo.GinkgoT(), body, "x-forwarded-host=myhost") assert.NotContains(ginkgo.GinkgoT(), body, "x-forwarded-proto=myproto") assert.NotContains(ginkgo.GinkgoT(), body, "x-forwarded-scheme=myproto")