VYPR
Critical severity10.0NVD Advisory· Published Mar 26, 2026· Updated Apr 7, 2026

CVE-2026-33494

CVE-2026-33494

Description

ORY Oathkeeper is an Identity & Access Proxy (IAP) and Access Control Decision API that authorizes HTTP requests based on sets of Access Rules. Versions prior to 26.2.0 are vulnerable to an authorization bypass via HTTP path traversal. An attacker can craft a URL containing path traversal sequences (e.g. /public/../admin/secrets) that resolves to a protected path after normalization, but is matched against a permissive rule because the raw, un-normalized path is used during rule evaluation. Version 26.2.0 contains a patch.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/ory/oathkeeperGo
< 0.40.10-0.20260320084758-8e00021404910.40.10-0.20260320084758-8e0002140491

Affected products

1
  • cpe:2.3:a:ory:oathkeeper:*:*:*:*:*:*:*:*
    Range: <26.2.0

Patches

1
8e0002140491

fix: clean path while matching to prevent path traversal

https://github.com/ory/oathkeeperPatrikMar 12, 2026via ghsa
8 files changed · +86 63
  • proxy/proxy.go+2 1 modified
    @@ -9,6 +9,7 @@ import (
     	"net/http"
     	"net/http/httputil"
     	"net/url"
    +	"path"
     	"strings"
     
     	"github.com/ory/oathkeeper/driver/configuration"
    @@ -194,7 +195,7 @@ func ConfigureBackendURL(r *http.Request, rl *rule.Rule) error {
     	forwardURL := r.URL
     	forwardURL.Scheme = backendScheme
     	forwardURL.Host = backendHost
    -	forwardURL.Path = "/" + strings.TrimLeft("/"+strings.Trim(backendPath, "/")+"/"+strings.TrimLeft(proxyPath, "/"), "/")
    +	forwardURL.Path = path.Join(backendPath, proxyPath)
     
     	if rl.Upstream.StripPath != "" {
     		forwardURL.Path = strings.Replace(forwardURL.Path, "/"+strings.Trim(rl.Upstream.StripPath, "/"), "", 1)
    
  • proxy/proxy_test.go+4 4 modified
    @@ -496,10 +496,10 @@ func TestConfigureBackendURL(t *testing.T) {
     			eHost: "localhost:3000",
     		},
     		{
    -			r:     &http.Request{Host: "localhost:3000", URL: &url.URL{Path: "/api/users/1234", Scheme: "http"}},
    -			rl:    &rule.Rule{Upstream: rule.Upstream{URL: "http://localhost:4000/foo/", PreserveHost: true, StripPath: "api"}},
    -			eURL:  "http://localhost:4000/foo/users/1234",
    -			eHost: "localhost:3000",
    +			r:     &http.Request{Host: "localhost:3000", URL: &url.URL{Path: "/api/../users/1234", Scheme: "http"}},
    +			rl:    &rule.Rule{Upstream: rule.Upstream{URL: "http://localhost:4000"}},
    +			eURL:  "http://localhost:4000/users/1234",
    +			eHost: "localhost:4000",
     		},
     	} {
     		t.Run(fmt.Sprintf("case=%d", k), func(t *testing.T) {
    
  • proxy/request_handler.go+5 1 modified
    @@ -6,6 +6,7 @@ package proxy
     import (
     	"encoding/json"
     	"net/http"
    +	"path"
     
     	"github.com/ory/herodot"
     	"github.com/ory/x/errorsx"
    @@ -327,11 +328,14 @@ func (d *requestHandler) HandleRequest(r *http.Request, rl *rule.Rule) (session
     
     // InitializeAuthnSession creates an authentication session and initializes it with a Match context if possible
     func (d *requestHandler) InitializeAuthnSession(r *http.Request, rl *rule.Rule) *authn.AuthenticationSession {
    -
     	session := &authn.AuthenticationSession{
     		Subject: "",
     	}
     
    +	if r.URL.Path != "" {
    +		r.URL.Path = path.Clean(r.URL.Path)
    +	}
    +
     	values, err := rl.ExtractRegexGroups(d.c.AccessRuleMatchingStrategy(), r.URL)
     	if err != nil {
     		d.r.Logger().WithError(err).
    
  • proxy/request_handler_test.go+48 40 modified
    @@ -444,18 +444,18 @@ func TestRequestHandler(t *testing.T) {
     }
     
     func TestInitializeSession(t *testing.T) {
    -	for k, tc := range []struct {
    -		d                string
    -		ruleMatch        rule.Match
    -		matchingStrategy configuration.MatchingStrategy
    -		r                *http.Request
    -		expectContext    authn.MatchContext
    +	for _, tc := range []struct {
    +		desc          string
    +		match         rule.Match
    +		strategy      configuration.MatchingStrategy
    +		url           string
    +		expectContext authn.MatchContext
     	}{
     		{
    -			d:                "Rule without capture",
    -			r:                newTestRequest("http://localhost"),
    -			matchingStrategy: configuration.Regexp,
    -			ruleMatch: rule.Match{
    +			desc:     "Rule without capture",
    +			url:      "http://localhost",
    +			strategy: configuration.Regexp,
    +			match: rule.Match{
     				URL: "http://localhost",
     			},
     			expectContext: authn.MatchContext{
    @@ -466,10 +466,10 @@ func TestInitializeSession(t *testing.T) {
     			},
     		},
     		{
    -			d:                "Rule with one capture",
    -			r:                newTestRequest("http://localhost/user"),
    -			matchingStrategy: configuration.Regexp,
    -			ruleMatch: rule.Match{
    +			desc:     "Rule with one capture",
    +			url:      "http://localhost/user",
    +			strategy: configuration.Regexp,
    +			match: rule.Match{
     				URL: "http://localhost/<.*>",
     			},
     			expectContext: authn.MatchContext{
    @@ -480,10 +480,10 @@ func TestInitializeSession(t *testing.T) {
     			},
     		},
     		{
    -			d:                "Request with query params",
    -			r:                newTestRequest("http://localhost/user?param=test"),
    -			matchingStrategy: configuration.Regexp,
    -			ruleMatch: rule.Match{
    +			desc:     "Request with query params",
    +			url:      "http://localhost/user?param=test",
    +			strategy: configuration.Regexp,
    +			match: rule.Match{
     				URL: "http://localhost/<.*>",
     			},
     			expectContext: authn.MatchContext{
    @@ -494,10 +494,10 @@ func TestInitializeSession(t *testing.T) {
     			},
     		},
     		{
    -			d:                "Rule with 2 captures",
    -			r:                newTestRequest("http://localhost/user?param=test"),
    -			matchingStrategy: configuration.Regexp,
    -			ruleMatch: rule.Match{
    +			desc:     "Rule with 2 captures",
    +			url:      "http://localhost/user?param=test",
    +			strategy: configuration.Regexp,
    +			match: rule.Match{
     				URL: "<http|https>://localhost/<.*>",
     			},
     			expectContext: authn.MatchContext{
    @@ -508,10 +508,24 @@ func TestInitializeSession(t *testing.T) {
     			},
     		},
     		{
    -			d:                "Rule with Glob matching strategy",
    -			r:                newTestRequest("http://localhost/user?param=test"),
    -			matchingStrategy: configuration.Glob,
    -			ruleMatch: rule.Match{
    +			desc:     "Rule with 1 capture and path traversal",
    +			url:      "http://localhost/user/../admin/secrets?param=test",
    +			strategy: configuration.Regexp,
    +			match: rule.Match{
    +				URL: "http://localhost/admin/<.*>",
    +			},
    +			expectContext: authn.MatchContext{
    +				RegexpCaptureGroups: []string{"secrets"},
    +				URL:                 x.ParseURLOrPanic("http://localhost/admin/secrets?param=test"),
    +				Method:              "GET",
    +				Header:              TestHeader,
    +			},
    +		},
    +		{
    +			desc:     "Rule with Glob matching strategy",
    +			url:      "http://localhost/user?param=test",
    +			strategy: configuration.Glob,
    +			match: rule.Match{
     				URL: "<http|https>://localhost/<*>",
     			},
     			expectContext: authn.MatchContext{
    @@ -522,23 +536,17 @@ func TestInitializeSession(t *testing.T) {
     			},
     		},
     	} {
    -		t.Run(fmt.Sprintf("case=%d/description=%s", k, tc.d), func(t *testing.T) {
    -
    -			conf := internal.NewConfigurationWithDefaults()
    +		t.Run(fmt.Sprintf("description=%s", tc.desc), func(t *testing.T) {
    +			conf := internal.NewConfigurationWithDefaults(configx.WithValue(configuration.AccessRuleMatchingStrategy, tc.strategy))
     			reg := internal.NewRegistry(conf)
    -			conf.SetForTest(t, configuration.AccessRuleMatchingStrategy, string(tc.matchingStrategy))
    -
    -			rule := rule.Rule{
    -				Match:          &tc.ruleMatch,
    -				Authenticators: []rule.Handler{},
    -				Authorizer:     rule.Handler{},
    -				Mutators:       []rule.Handler{},
    -			}
     
    -			session := reg.ProxyRequestHandler().InitializeAuthnSession(tc.r, &rule)
    +			session := reg.ProxyRequestHandler().InitializeAuthnSession(
    +				newTestRequest(tc.url),
    +				&rule.Rule{Match: &tc.match},
    +			)
     
    -			assert.NotNil(t, session)
    -			assert.NotNil(t, session.MatchContext.Header)
    +			require.NotNil(t, session)
    +			require.NotNil(t, session.MatchContext.Header)
     			assert.EqualValues(t, tc.expectContext, session.MatchContext)
     		})
     	}
    
  • rule/engine_glob.go+1 1 modified
    @@ -35,7 +35,7 @@ func (ge *globMatchingEngine) ReplaceAllString(_, _, _ string) (string, error) {
     }
     
     // FindStringSubmatch is noop for now and always returns an empty array
    -func (ge *globMatchingEngine) FindStringSubmatch(pattern, matchAgainst string) ([]string, error) {
    +func (ge *globMatchingEngine) FindStringSubmatch(_, _ string) ([]string, error) {
     	return []string{}, nil
     }
     
    
  • rule/repository_memory.go+1 1 modified
    @@ -110,7 +110,7 @@ func (m *RepositoryMemory) Set(ctx context.Context, rules []Rule) error {
     	return nil
     }
     
    -func (m *RepositoryMemory) Match(ctx context.Context, method string, u *url.URL, protocol Protocol) (*Rule, error) {
    +func (m *RepositoryMemory) Match(_ context.Context, method string, u *url.URL, protocol Protocol) (*Rule, error) {
     	if u == nil {
     		return nil, errors.WithStack(errors.New("nil URL provided"))
     	}
    
  • rule/rule.go+4 0 modified
    @@ -7,6 +7,7 @@ import (
     	"encoding/json"
     	"fmt"
     	"net/url"
    +	"path"
     	"strings"
     
     	"github.com/pkg/errors"
    @@ -218,6 +219,9 @@ func (r *Rule) IsMatching(strategy configuration.MatchingStrategy, method string
     		return false, nil
     	}
     
    +	if u.Path != "" {
    +		u.Path = path.Clean(u.Path)
    +	}
     	matchAgainst := fmt.Sprintf("%s://%s%s", u.Scheme, u.Host, u.Path)
     	return r.matchingEngine.IsMatching(r.Match.GetURL(), matchAgainst)
     }
    
  • rule/rule_test.go+21 15 modified
    @@ -47,33 +47,29 @@ func TestRule(t *testing.T) {
     		method        string
     		url           string
     		expectedMatch bool
    -		expectedErr   error
     	}{
     		{
     			method:        "DELETE",
     			url:           "https://localhost/users/1234",
     			expectedMatch: true,
    -			expectedErr:   nil,
     		},
     		{
     			method:        "DELETE",
     			url:           "https://localhost/users/1234?key=value&key1=value1",
     			expectedMatch: true,
    -			expectedErr:   nil,
     		},
     		{
     			method:        "DELETE",
     			url:           "https://localhost/users/abcd",
     			expectedMatch: false,
    -			expectedErr:   nil,
     		},
     	}
     	for ind, tcase := range tests {
     		t.Run(strconv.FormatInt(int64(ind), 10), func(t *testing.T) {
     			testFunc := func(rule Rule, strategy configuration.MatchingStrategy) {
     				matched, err := rule.IsMatching(strategy, tcase.method, mustParse(t, tcase.url), ProtocolHTTP)
    +				require.NoError(t, err)
     				assert.Equal(t, tcase.expectedMatch, matched)
    -				assert.Equal(t, tcase.expectedErr, err)
     			}
     			t.Run("rule0", func(t *testing.T) {
     				testFunc(rules[0], configuration.Regexp)
    @@ -100,32 +96,28 @@ func TestRule1(t *testing.T) {
     		method        string
     		url           string
     		expectedMatch bool
    -		expectedErr   error
     	}{
     		{
     			method:        "DELETE",
     			url:           "https://localhost/users/manager",
     			expectedMatch: true,
    -			expectedErr:   nil,
     		},
     		{
     			method:        "DELETE",
     			url:           "https://localhost/users/1234?key=value&key1=value1",
     			expectedMatch: true,
    -			expectedErr:   nil,
     		},
     		{
     			method:        "DELETE",
     			url:           "https://localhost/users/admin",
     			expectedMatch: false,
    -			expectedErr:   nil,
     		},
     	}
     	for ind, tcase := range tests {
     		t.Run(strconv.FormatInt(int64(ind), 10), func(t *testing.T) {
     			matched, err := r.IsMatching(configuration.Regexp, tcase.method, mustParse(t, tcase.url), ProtocolHTTP)
    +			require.NoError(t, err)
     			assert.Equal(t, tcase.expectedMatch, matched)
    -			assert.Equal(t, tcase.expectedErr, err)
     		})
     	}
     }
    @@ -142,36 +134,50 @@ func TestRuleWithCustomMethod(t *testing.T) {
     		method        string
     		url           string
     		expectedMatch bool
    -		expectedErr   error
     	}{
     		{
     			method:        "CUSTOM",
     			url:           "https://localhost/users/manager",
     			expectedMatch: true,
    -			expectedErr:   nil,
     		},
     		{
     			method:        "CUSTOM",
     			url:           "https://localhost/users/1234?key=value&key1=value1",
     			expectedMatch: true,
    -			expectedErr:   nil,
     		},
     		{
     			method:        "DELETE",
     			url:           "https://localhost/users/admin",
     			expectedMatch: false,
    -			expectedErr:   nil,
     		},
     	}
     	for ind, tcase := range tests {
     		t.Run(strconv.FormatInt(int64(ind), 10), func(t *testing.T) {
     			matched, err := r.IsMatching(configuration.Regexp, tcase.method, mustParse(t, tcase.url), ProtocolHTTP)
    +			require.NoError(t, err)
     			assert.Equal(t, tcase.expectedMatch, matched)
    -			assert.Equal(t, tcase.expectedErr, err)
     		})
     	}
     }
     
    +func TestRulePathTraversal(t *testing.T) {
    +	r := &Rule{
    +		Match: &Match{
    +			Methods: []string{"POST"},
    +			URL:     "https://localhost/admin/<.*>",
    +		},
    +	}
    +
    +	u := &url.URL{
    +		Scheme: "https",
    +		Host:   "localhost",
    +		Path:   "/public/../admin/secrets",
    +	}
    +	matched, err := r.IsMatching(configuration.Regexp, "POST", u, ProtocolHTTP)
    +	require.NoError(t, err)
    +	assert.True(t, matched)
    +}
    +
     func TestRule_UnmarshalJSON(t *testing.T) {
     	var tests = []struct {
     		name     string
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

0

No linked articles in our index yet.