From b75439e3702f375eed2ae0fc63182e9cc7036fd6 Mon Sep 17 00:00:00 2001 From: Madan Kumar Date: Tue, 11 Aug 2026 07:30:24 +0530 Subject: [PATCH] fix(ingress): match hostnames case-insensitively (RFC 4343) matchHost compared hostnames with == / strings.HasSuffix without normalizing case, but hostnames are case-insensitive (RFC 4343, RFC 3986 3.2.2). validateHostname accepts mixed-case hostnames, and req.Host reaches matching un-lowercased, so a mixed-case Host or config hostname would skip the intended rule and fall through to the catch-all. Lower-case both sides; add direct + end-to-end tests. --- ingress/ingress.go | 6 ++++ ingress/matchhost_case_test.go | 53 ++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 ingress/matchhost_case_test.go diff --git a/ingress/ingress.go b/ingress/ingress.go index a325271a7e7..cc4aeb04f35 100644 --- a/ingress/ingress.go +++ b/ingress/ingress.go @@ -62,6 +62,12 @@ func (ing Ingress) FindMatchingRule(hostname, path string) (*Rule, int) { } func matchHost(ruleHost, reqHost string) bool { + // Hostnames are case-insensitive (RFC 4343, RFC 3986 §3.2.2), so compare + // them case-insensitively. cloudflared accepts mixed-case hostnames in + // ingress config, and the request Host header can also be mixed-case. + ruleHost = strings.ToLower(ruleHost) + reqHost = strings.ToLower(reqHost) + if ruleHost == reqHost { return true } diff --git a/ingress/matchhost_case_test.go b/ingress/matchhost_case_test.go new file mode 100644 index 00000000000..e96baac4088 --- /dev/null +++ b/ingress/matchhost_case_test.go @@ -0,0 +1,53 @@ +package ingress + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +// Hostnames are case-insensitive (RFC 4343, RFC 3986 §3.2.2). cloudflared +// accepts mixed-case hostnames in ingress config and the request Host header +// can be mixed-case, so rule matching must ignore case. +func TestMatchHostCaseInsensitive(t *testing.T) { + tests := []struct { + rule, req string + want bool + }{ + {"myapp.example.com", "MyApp.example.com", true}, // mixed-case request Host + {"MyApp.example.com", "myapp.example.com", true}, // uppercase rule hostname + {"*.example.com", "foo.EXAMPLE.com", true}, // wildcard, mixed-case request + {"*.Example.com", "foo.example.com", true}, // wildcard, uppercase rule + {"a.example.com", "b.example.com", false}, // still a non-match + {"*.example.com", "example.com", false}, // wildcard does not match apex + } + for _, test := range tests { + assert.Equalf(t, test.want, matchHost(test.rule, test.req), + "matchHost(%q, %q)", test.rule, test.req) + } +} + +// End-to-end: a mixed-case request Host resolves to the correct rule rather +// than falling through to the catch-all. +func TestFindMatchingRuleCaseInsensitive(t *testing.T) { + ingress := Ingress{ + Rules: []Rule{ + {Hostname: "tunnel-a.example.com"}, + {Hostname: "*.wild.example.com"}, + {Hostname: "*"}, // catch-all + }, + } + tests := []struct { + host string + wantRuleIndex int + }{ + {"Tunnel-A.example.com", 0}, + {"TUNNEL-A.EXAMPLE.COM", 0}, + {"Foo.Wild.Example.com", 1}, + {"other.example.com", 2}, + } + for _, test := range tests { + _, ruleIndex := ingress.FindMatchingRule(test.host, "/") + assert.Equalf(t, test.wantRuleIndex, ruleIndex, "host=%s", test.host) + } +}