High severityGHSA Advisory· Published May 8, 2026· Updated May 8, 2026
CVE-2026-42273
CVE-2026-42273
Description
Heimdall is a cloud native Identity Aware Proxy and Access Control Decision service. Prior to version 0.17.14, Heimdall performs host matching in a case-sensitive manner, while HTTP hostnames are case-insensitive. This discrepancy can result in heimdall failing to match a rule for a request host that differs only in letter casing, potentially causing the request to be classified differently than intended. This issue has been patched in version 0.17.14.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/dadrus/heimdallGo | < 0.17.14 | 0.17.14 |
Affected products
1Patches
13d05e56a9e7efix: Case-insensitive host matching (#3208)
9 files changed · +79 −11
docs/content/docs/rules/regular_rule.adoc+9 −1 modified@@ -60,6 +60,15 @@ WARNING: This property is deprecated, is unused since v0.17.0, and it will be re ** *`hosts`*: _HostMatcher array_ (optional) + Defines a set of hosts to match against the HTTP `Host` header. These conditions are "OR" conditions, meaning that at least one must match for a successful match. If not defined, any host will be matched. Each entry has the following properties: ++ +[NOTE] +==== +The host component of a URI is case-insensitive (see https://www.rfc-editor.org/rfc/rfc3986#section-3.2.2[RFC 3986, Section 3.2.2]). To preserve this behavior consistently during rule matching, heimdall: + +* normalizes the incoming request host to lowercase before rule lookup, +* normalizes `exact` and `wildcard` host expressions to lowercase, and +* evaluates `glob` and `regex` host expressions as configured. Therefore, define these expressions in lowercase to avoid ambiguity. +==== *** *`type`*: _string_ (mandatory) + @@ -432,4 +441,3 @@ As with the authentication & authorization pipeline, each step can optionally ha ==== This example uses two error handlers, named `foo` and `bar`. `bar` will only be executed if `foo` 's error condition does not match. `bar` does also override the error handler configuration as required by the given rule. -
internal/handler/envoyextauth/grpcv3/request_context.go+2 −1 modified@@ -111,11 +111,12 @@ func (r *RequestContext) Init(ctx context.Context, req *envoy_auth.CheckRequest) r.hmdlReq.Method = httpReq.GetMethod() r.hmdlReq.URL.URL = url.URL{ Scheme: httpReq.GetScheme(), - Host: httpReq.GetHost(), + Host: strings.ToLower(httpReq.GetHost()), RawPath: parsed.RawPath, Path: parsed.Path, RawQuery: parsed.RawQuery, } + r.reqHeaders["Host"] = r.hmdlReq.URL.Host r.hmdlReq.ClientIPAddresses = clientIPs }
internal/handler/envoyextauth/grpcv3/request_context_test.go+4 −3 modified@@ -40,7 +40,7 @@ func TestNewRequestContext(t *testing.T) { httpReq := &envoy_auth.AttributeContext_HttpRequest{ Method: http.MethodPatch, Scheme: "https", - Host: "foo.bar:8080", + Host: "FoO.Bar:8080", Path: "/test/baz?bar=moo#foobar", Query: "", // documented to be empty Fragment: "", // documented to be empty @@ -76,13 +76,14 @@ func TestNewRequestContext(t *testing.T) { // THEN require.Equal(t, httpReq.GetMethod(), ctx.Request().Method) require.Equal(t, httpReq.GetScheme(), ctx.Request().URL.Scheme) - require.Equal(t, httpReq.GetHost(), ctx.Request().URL.Host) + require.Equal(t, "foo.bar:8080", ctx.Request().URL.Host) require.Equal(t, "/test/baz", ctx.Request().URL.Path) require.Empty(t, ctx.Request().URL.Fragment) require.Equal(t, "bar=moo#foobar", ctx.Request().URL.RawQuery) require.Equal(t, "moo#foobar", ctx.Request().URL.URL.Query().Get("bar")) require.Equal(t, map[string]any{"content": []string{"heimdall"}}, ctx.Request().Body()) - require.Len(t, ctx.Request().Headers(), 3) + require.Len(t, ctx.Request().Headers(), 4) + require.Equal(t, "foo.bar:8080", ctx.Request().Header("Host")) require.Equal(t, "barfoo", ctx.Request().Header("X-Foo-Bar")) require.Equal(t, "foo", ctx.Request().Cookie("bar")) require.Equal(t, "baz", ctx.Request().Cookie("foo"))
internal/handler/requestcontext/extract_url.go+3 −0 modified@@ -19,6 +19,7 @@ package requestcontext import ( "net/http" "net/url" + "strings" "github.com/dadrus/heimdall/internal/x" ) @@ -40,6 +41,8 @@ func extractURL(req *http.Request) url.URL { host = req.Host } + host = strings.ToLower(host) + if val := req.Header.Get("X-Forwarded-Uri"); len(val) != 0 { if forwardedURI, err := url.Parse(val); err == nil { rawPath = forwardedURI.EscapedPath()
internal/handler/requestcontext/extract_url_test.go+24 −0 modified@@ -64,6 +64,30 @@ func TestExtractURL(t *testing.T) { assert.Equal(t, url.Values{"foo": []string{"bar"}}, extracted.Query()) }, }, + "X-Forwarded-Host is normalized to lowercase": { + configureRequest: func(t *testing.T, req *http.Request) { + t.Helper() + + req.Header.Set("X-Forwarded-Host", "FoObAr.Example.COM:8443") + }, + assert: func(t *testing.T, extracted url.URL) { + t.Helper() + + assert.Equal(t, "foobar.example.com:8443", extracted.Host) + }, + }, + "request host fallback is normalized to lowercase": { + configureRequest: func(t *testing.T, req *http.Request) { + t.Helper() + + req.Host = "FooBar.Example.Local:9443" + }, + assert: func(t *testing.T, extracted url.URL) { + t.Helper() + + assert.Equal(t, "foobar.example.local:9443", extracted.Host) + }, + }, "X-Forwarded-Path is ignored": { configureRequest: func(t *testing.T, req *http.Request) { t.Helper()
internal/handler/requestcontext/request_context.go+2 −2 modified@@ -86,7 +86,7 @@ func (r *RequestContext) Reset() { func (r *RequestContext) Header(name string) string { key := textproto.CanonicalMIMEHeaderKey(name) if key == "Host" { - return r.req.Host + return r.hmdlReq.URL.Host } return strings.Join(r.req.Header.Values(key), ",") @@ -102,7 +102,7 @@ func (r *RequestContext) Cookie(name string) string { func (r *RequestContext) Headers() map[string]string { if len(r.headers) == 0 { - r.headers["Host"] = r.req.Host + r.headers["Host"] = r.hmdlReq.URL.Host for k, v := range r.req.Header { r.headers[textproto.CanonicalMIMEHeaderKey(k)] = strings.Join(v, ",") }
internal/handler/requestcontext/request_context_test.go+3 −3 modified@@ -148,7 +148,7 @@ func TestRequestContextHeaders(t *testing.T) { t.Parallel() // GIVEN - req := httptest.NewRequest(http.MethodHead, "https://foo.baz/test", nil) + req := httptest.NewRequest(http.MethodHead, "https://FoO.Baz/test", nil) req.Header.Set("X-Foo-Bar", "foo") req.Header.Add("X-Foo-Bar", "bar") @@ -168,10 +168,10 @@ func TestRequestContextHeader(t *testing.T) { t.Parallel() // GIVEN - req := httptest.NewRequest(http.MethodHead, "https://foo.bar/test", nil) + req := httptest.NewRequest(http.MethodHead, "https://Foo.bar/test", nil) req.Header.Set("X-Foo-Bar", "foo") req.Header.Add("X-Foo-Bar", "bar") - req.Host = "bar.foo" + req.Host = "Bar.foo" ctx := New() ctx.Init(req)
internal/rules/rule_factory_impl.go+2 −1 modified@@ -20,6 +20,7 @@ import ( "errors" "fmt" "slices" + "strings" "github.com/rs/zerolog" @@ -160,7 +161,7 @@ func (f *ruleFactory) CreateRule(version, srcID string, rc config2.Rule) (rule.R rul.routes = append(rul.routes, &routeImpl{ rule: rul, - host: host.Value, + host: strings.ToLower(host.Value), path: rc.Path, matcher: andMatcher{sm, mm, hm, ppm}, })
internal/rules/rule_factory_impl_test.go+30 −0 modified@@ -628,6 +628,36 @@ func TestRuleFactoryCreateRule(t *testing.T) { assert.Empty(t, rul.eh) }, }, + "normalizes trie host matchers to lowercase": { + config: config2.Rule{ + ID: "foobar", + Matcher: config2.Matcher{ + Routes: []config2.Route{{Path: "/foo/bar"}}, + Hosts: []config2.HostMatcher{ + {Type: "exact", Value: "FoO.ExAmPlE.cOm"}, + {Type: "wildcard", Value: "*.ExAmPlE.cOm"}, + }, + }, + Execute: []config.MechanismConfig{ + {"authenticator": "foo"}, + }, + }, + configureMocks: func(t *testing.T, mhf *mocks3.MechanismFactoryMock) { + t.Helper() + + mhf.EXPECT().CreateAuthenticator("test", "foo", "", mock.Anything). + Return(&mocks2.AuthenticatorMock{}, nil) + }, + assert: func(t *testing.T, err error, rul *ruleImpl) { + t.Helper() + + require.NoError(t, err) + require.NotNil(t, rul) + require.Len(t, rul.Routes(), 2) + assert.Equal(t, "foo.example.com", rul.Routes()[0].Host()) + assert.Equal(t, "*.example.com", rul.Routes()[1].Host()) + }, + }, "without default rule and minimum required configuration in proxy mode": { opMode: config.ProxyMode, config: config2.Rule{
Vulnerability mechanics
AI mechanics synthesis has not run for this CVE yet.
References
6- github.com/advisories/GHSA-72h4-mxfc-jx37ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-42273ghsaADVISORY
- github.com/dadrus/heimdall/commit/3d05e56a9e7ef0355f17482b4322054af4e85943nvdWEB
- github.com/dadrus/heimdall/pull/3208nvdWEB
- github.com/dadrus/heimdall/releases/tag/v0.17.14nvdWEB
- github.com/dadrus/heimdall/security/advisories/GHSA-72h4-mxfc-jx37nvdWEB
News mentions
0No linked articles in our index yet.