VYPR
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.

PackageAffected versionsPatched versions
github.com/dadrus/heimdallGo
< 0.17.140.17.14

Affected products

1

Patches

1
3d05e56a9e7e

fix: Case-insensitive host matching (#3208)

https://github.com/dadrus/heimdallDimitrij DrusApr 19, 2026via ghsa
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

News mentions

0

No linked articles in our index yet.