CVE-2026-33496
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 authentication bypass due to cache key confusion. The oauth2_introspection authenticator cache does not distinguish tokens that were validated with different introspection URLs. An attacker can therefore legitimately use a token to prime the cache, and subsequently use the same token for rules that use a different introspection server. Ory Oathkeeper has to be configured with multiple oauth2_introspection authenticator servers, each accepting different tokens. The authenticators also must be configured to use caching. An attacker has to have a way to gain a valid token for one of the configured introspection servers. Starting in version 26.2.0, Ory Oathkeeper includes the introspection server URL in the cache key, preventing confusion of tokens. Update to the patched version of Ory Oathkeeper. If that is not immediately possible, disable caching for oauth2_introspection authenticators.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/ory/oathkeeperGo | < 0.40.10-0.20260320084801-198a2bc82a99 | 0.40.10-0.20260320084801-198a2bc82a99 |
Affected products
1Patches
1198a2bc82a99fix: scope cache config key to introspection URL
4 files changed · +207 −194
pipeline/authn/authenticator_oauth2_introspection_cache_test.go+73 −63 modified@@ -4,25 +4,25 @@ package authn import ( - "context" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "go.opentelemetry.io/otel/trace" + "go.opentelemetry.io/otel/trace/noop" "github.com/ory/fosite" "github.com/ory/oathkeeper/driver/configuration" "github.com/ory/x/configx" "github.com/ory/x/logrusx" + "github.com/ory/x/uuidx" ) func TestCache(t *testing.T) { t.Parallel() logger := logrusx.New("", "") c, err := configuration.NewKoanfProvider( - context.Background(), + t.Context(), nil, logger, configx.WithValues(map[string]interface{}{ @@ -31,74 +31,84 @@ func TestCache(t *testing.T) { })) require.NoError(t, err) - a := NewAuthenticatorOAuth2Introspection(c, logger, trace.NewNoopTracerProvider()) //nolint:staticcheck // tests only need noop tracer + a := NewAuthenticatorOAuth2Introspection(c, logger, noop.NewTracerProvider()) assert.Equal(t, "oauth2_introspection", a.GetID()) config, _, err := a.Config(nil) require.NoError(t, err) - t.Run("method=tokenToCache", func(t *testing.T) { - t.Run("case=cache value", func(t *testing.T) { - i := &AuthenticatorOAuth2IntrospectionResult{ - Active: true, - Extra: map[string]interface{}{"extra": "foo"}, - } + t.Run("case=cache value", func(t *testing.T) { + i := &AuthenticatorOAuth2IntrospectionResult{ + Active: true, + Extra: map[string]interface{}{"extra": "foo"}, + } - a.tokenToCache(config, i, "token", fosite.WildcardScopeStrategy) - // wait cache to save value - time.Sleep(time.Millisecond * 10) + a.tokenToCache(config, i, "token", fosite.WildcardScopeStrategy) + a.WaitForCache() - // modify struct should not affect cached value - i.Active = false + // modify struct should not affect cached value + i.Active = false + v := a.tokenFromCache(config, "token", fosite.WildcardScopeStrategy) + require.NotNil(t, v) + require.True(t, v.Active) + }) + + t.Run("case=value cannot be marshaled to json should not be cached", func(t *testing.T) { + i := &AuthenticatorOAuth2IntrospectionResult{ + Active: false, + Extra: map[string]interface{}{"extra": make(chan bool, 1)}, + } + + a.tokenToCache(config, i, "invalid-token", fosite.WildcardScopeStrategy) + a.WaitForCache() + + v := a.tokenFromCache(config, "invalid-token", fosite.WildcardScopeStrategy) + require.Nil(t, v) + }) + + t.Run("case=cached invalid json", func(t *testing.T) { + ok := a.tokenCache.Set(tokenCacheKey("invalid-json", config.IntrospectionURL), []byte("invalid-json-string"), 1) + require.True(t, ok) + a.WaitForCache() + + v := a.tokenFromCache(config, "invalid-json", fosite.WildcardScopeStrategy) + require.Nil(t, v) + }) + + t.Run("case=cache with ttl", func(t *testing.T) { + i := &AuthenticatorOAuth2IntrospectionResult{Active: true} + + config, _, err := a.Config([]byte(`{ "cache": { "ttl": "500ms" } }`)) + require.NoError(t, err) + a.tokenToCache(config, i, "token", fosite.WildcardScopeStrategy) + a.tokenCache.Wait() + + assert.EventuallyWithT(t, func(t *assert.CollectT) { v := a.tokenFromCache(config, "token", fosite.WildcardScopeStrategy) - require.NotNil(t, v) - require.True(t, v.Active) - }) - - t.Run("case=value cannot be marshaled to json should not be cached", func(t *testing.T) { - i := &AuthenticatorOAuth2IntrospectionResult{ - Active: false, - Extra: map[string]interface{}{"extra": make(chan bool, 1)}, - } - - a.tokenToCache(config, i, "invalid-token", fosite.WildcardScopeStrategy) - // wait cache to save value - time.Sleep(time.Millisecond * 10) - - v := a.tokenFromCache(config, "invalid-token", fosite.WildcardScopeStrategy) - require.Nil(t, v) - }) - - t.Run("case=cached invalid json value should not working", func(t *testing.T) { - ok := a.tokenCache.Set("invalid-json", []byte("invalid-json-string"), 1) - require.True(t, ok) - // wait cache to save value - time.Sleep(time.Millisecond * 10) - - v := a.tokenFromCache(config, "invalid-json", fosite.WildcardScopeStrategy) - require.Nil(t, v) - }) - - t.Run("case=cache with ttl", func(t *testing.T) { - i := &AuthenticatorOAuth2IntrospectionResult{ - Active: true, - } - - config, _, _ := a.Config([]byte(`{ "cache": { "ttl": "500ms" } }`)) - a.tokenToCache(config, i, "token", fosite.WildcardScopeStrategy) - a.tokenCache.Wait() - - assert.EventuallyWithT(t, func(t *assert.CollectT) { - v := a.tokenFromCache(config, "token", fosite.WildcardScopeStrategy) - assert.NotNil(t, v) - }, 490*time.Millisecond, 10*time.Millisecond) - - // wait cache to be expired - assert.EventuallyWithT(t, func(t *assert.CollectT) { - v := a.tokenFromCache(config, "token", fosite.WildcardScopeStrategy) - assert.Nil(t, v) - }, 700*time.Millisecond, 10*time.Millisecond) - }) + assert.NotNil(t, v) + }, 490*time.Millisecond, 10*time.Millisecond) + + // wait cache to be expired + assert.EventuallyWithT(t, func(t *assert.CollectT) { + v := a.tokenFromCache(config, "token", fosite.WildcardScopeStrategy) + assert.Nil(t, v) + }, 700*time.Millisecond, 10*time.Millisecond) }) + t.Run("case=token with different introspection URL", func(t *testing.T) { + i := &AuthenticatorOAuth2IntrospectionResult{Active: true} + + config, _, err := a.Config([]byte(`{ "cache": { "ttl": "0s" }, "introspection_url": "http://localhost/oauth2/token" }`)) + require.NoError(t, err) + + token := uuidx.NewV4().String() + a.tokenToCache(config, i, token, fosite.WildcardScopeStrategy) + a.WaitForCache() + + config, _, err = a.Config([]byte(`{ "cache": { "ttl": "0s" }, "introspection_url": "http://localhost/oauth2/token2" }`)) + require.NoError(t, err) + + v := a.tokenFromCache(config, token, fosite.WildcardScopeStrategy) + require.Nil(t, v) + }) }
pipeline/authn/authenticator_oauth2_introspection.go+24 −11 modified@@ -82,8 +82,12 @@ func NewAuthenticatorOAuth2Introspection(c configuration.Provider, l *logrusx.Lo return &AuthenticatorOAuth2Introspection{c: c, logger: l, provider: p, clientMap: make(map[string]*http.Client)} } -func (a *AuthenticatorOAuth2Introspection) GetID() string { - return "oauth2_introspection" +func (a *AuthenticatorOAuth2Introspection) GetID() string { return "oauth2_introspection" } + +func (a *AuthenticatorOAuth2Introspection) WaitForCache() { + if a.tokenCache != nil { + a.tokenCache.Wait() + } } type Audience []string @@ -131,6 +135,10 @@ func (a *Audience) UnmarshalJSON(b []byte) error { return errUnsupportedType } +func tokenCacheKey(token, endpoint string) string { + return fmt.Sprintf("%s|%s", token, endpoint) +} + func (a *AuthenticatorOAuth2Introspection) tokenFromCache(config *AuthenticatorOAuth2IntrospectionConfiguration, token string, ss fosite.ScopeStrategy) *AuthenticatorOAuth2IntrospectionResult { if !config.Cache.Enabled { return nil @@ -140,7 +148,8 @@ func (a *AuthenticatorOAuth2Introspection) tokenFromCache(config *AuthenticatorO return nil } - i, found := a.tokenCache.Get(token) + key := tokenCacheKey(token, config.IntrospectionURL) + i, found := a.tokenCache.Get(key) if !found { return nil } @@ -161,12 +170,16 @@ func (a *AuthenticatorOAuth2Introspection) tokenToCache(config *AuthenticatorOAu return } - if v, err := json.Marshal(i); err != nil { + key := tokenCacheKey(token, config.IntrospectionURL) + v, err := json.Marshal(i) + if err != nil { return - } else if a.cacheTTL != nil { - a.tokenCache.SetWithTTL(token, v, 1, *a.cacheTTL) + } + + if a.cacheTTL != nil { + a.tokenCache.SetWithTTL(key, v, 1, *a.cacheTTL) } else { - a.tokenCache.Set(token, v, 1) + a.tokenCache.Set(key, v, 1) } } @@ -218,7 +231,7 @@ func (a *AuthenticatorOAuth2Introspection) Authenticate(r *http.Request, session if err != nil { return errors.WithStack(err) } - defer resp.Body.Close() //nolint:errcheck + defer func() { _ = resp.Body.Close() }() if resp.StatusCode == http.StatusTooManyRequests { return errors.WithStack(helper.NewErrTooManyRequestsWithHeaders(resp)) @@ -231,8 +244,8 @@ func (a *AuthenticatorOAuth2Introspection) Authenticate(r *http.Request, session } } - if len(i.TokenUse) > 0 && i.TokenUse != "access_token" { - return errors.WithStack(helper.ErrForbidden.WithReason(fmt.Sprintf("Use of introspected token is not an access token but \"%s\"", i.TokenUse))) + if i.TokenUse != "" && i.TokenUse != "access_token" { + return errors.WithStack(helper.ErrForbidden.WithReason(fmt.Sprintf("Use of introspected token is not an access token but %q", i.TokenUse))) } if !i.Active { @@ -244,7 +257,7 @@ func (a *AuthenticatorOAuth2Introspection) Authenticate(r *http.Request, session } for _, audience := range cf.Audience { - if !slices.Contains([]string(i.Audience), audience) { + if !slices.Contains(i.Audience, audience) { return errors.WithStack(helper.ErrForbidden.WithReason(fmt.Sprintf("Token audience is not intended for target audience %s", audience))) } }
pipeline/authn/authenticator_oauth2_introspection_test.go+100 −108 modified@@ -9,10 +9,11 @@ import ( "net/http" "net/http/httptest" "net/url" + "sync/atomic" "testing" "time" - "go.opentelemetry.io/otel/trace" + "go.opentelemetry.io/otel/trace/noop" "github.com/ory/x/assertx" "github.com/ory/x/configx" @@ -24,6 +25,7 @@ import ( "github.com/tidwall/sjson" "github.com/ory/oathkeeper/driver/configuration" + "github.com/ory/oathkeeper/helper" "github.com/ory/oathkeeper/internal" . "github.com/ory/oathkeeper/pipeline/authn" ) @@ -32,9 +34,11 @@ func TestAuthenticatorOAuth2Introspection(t *testing.T) { conf := internal.NewConfigurationWithDefaults(configx.SkipValidation()) reg := internal.NewRegistry(conf) - a, err := reg.PipelineAuthenticator("oauth2_introspection") + aa, err := reg.PipelineAuthenticator("oauth2_introspection") require.NoError(t, err) - assert.Equal(t, "oauth2_introspection", a.GetID()) + require.Equal(t, "oauth2_introspection", aa.GetID()) + a, ok := aa.(*AuthenticatorOAuth2Introspection) + require.Truef(t, ok, "got type %T, want %T", aa, a) t.Run("method=authenticate", func(t *testing.T) { for k, tc := range []struct { @@ -615,65 +619,58 @@ func TestAuthenticatorOAuth2Introspection(t *testing.T) { t.Run("method=authenticate-with-cache", func(t *testing.T) { conf.SetForTest(t, "authenticators.oauth2_introspection.config.cache.enabled", true) - var handlerWasCalled bool - assertHandlerWasCalled := func(t *testing.T) { - assert.True(t, handlerWasCalled, "expected the handler to have been called") - handlerWasCalled = false - } - assertCacheWasUsed := func(t *testing.T) { - assert.False(t, handlerWasCalled, "expected the cache to have been used") - handlerWasCalled = false - } - - setup := func(t *testing.T, config string) []byte { - router := httprouter.New() - router.POST("/oauth2/introspect", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { - handlerWasCalled = true - require.NoError(t, r.ParseForm()) - switch r.Form.Get("token") { - case "inactive-scope-b": - require.NoError(t, json.NewEncoder(w).Encode(&AuthenticatorOAuth2IntrospectionResult{ - Active: false, - })) - case "another-active-scope-a": - fallthrough - case "active-scope-a": - if r.Form.Get("scope") != "" && r.Form.Get("scope") != "scope-a" { - require.NoError(t, json.NewEncoder(w).Encode(&AuthenticatorOAuth2IntrospectionResult{ - Active: false, - })) - return - } - require.NoError(t, json.NewEncoder(w).Encode(&AuthenticatorOAuth2IntrospectionResult{ - Active: true, - Scope: "scope-a", - Subject: "subject", - Audience: []string{"audience"}, - Issuer: "foo", - Username: "username", - Expires: time.Now().Add(2 * time.Second).Unix(), - Extra: map[string]interface{}{"extra": "foo"}, - })) - case "refresh-token": - require.NoError(t, json.NewEncoder(w).Encode(&AuthenticatorOAuth2IntrospectionResult{ - Active: true, - Scope: "scope-a", - Subject: "subject", - Audience: []string{"audience"}, - Issuer: "foo", - Username: "username", - TokenUse: "refresh_token", - Extra: map[string]interface{}{"extra": "foo"}, - })) - default: + cacheUsed := atomic.Bool{} + cacheUsed.Store(true) + cacheWasUsed := func() bool { return cacheUsed.Swap(true) } + + router := http.NewServeMux() + router.HandleFunc("POST /oauth2/introspect", func(w http.ResponseWriter, r *http.Request) { + cacheUsed.Store(false) + require.NoError(t, r.ParseForm()) + switch r.Form.Get("token") { + case "inactive-scope-b": + require.NoError(t, json.NewEncoder(w).Encode(&AuthenticatorOAuth2IntrospectionResult{ + Active: false, + })) + case "another-active-scope-a", "active-scope-a": + if r.Form.Get("scope") != "" && r.Form.Get("scope") != "scope-a" { require.NoError(t, json.NewEncoder(w).Encode(&AuthenticatorOAuth2IntrospectionResult{ Active: false, })) + return } - }) - ts := httptest.NewServer(router) - t.Cleanup(ts.Close) + require.NoError(t, json.NewEncoder(w).Encode(&AuthenticatorOAuth2IntrospectionResult{ + Active: true, + Scope: "scope-a", + Subject: "subject", + Audience: []string{"audience"}, + Issuer: "foo", + Username: "username", + Expires: time.Now().Add(2 * time.Second).Unix(), + Extra: map[string]interface{}{"extra": "foo"}, + })) + case "refresh-token": + require.NoError(t, json.NewEncoder(w).Encode(&AuthenticatorOAuth2IntrospectionResult{ + Active: true, + Scope: "scope-a", + Subject: "subject", + Audience: []string{"audience"}, + Issuer: "foo", + Username: "username", + TokenUse: "refresh_token", + Extra: map[string]interface{}{"extra": "foo"}, + })) + default: + require.NoError(t, json.NewEncoder(w).Encode(&AuthenticatorOAuth2IntrospectionResult{ + Active: false, + })) + } + }) + ts := httptest.NewServer(router) + t.Cleanup(ts.Close) + setup := func(t *testing.T, config string) []byte { + var err error config, err = sjson.Set(config, "introspection_url", ts.URL+"/oauth2/introspect") require.NoError(t, err) config, err = sjson.Set(config, "pre_authorization.token_url", ts.URL+"/oauth2/token") @@ -689,9 +686,8 @@ func TestAuthenticatorOAuth2Introspection(t *testing.T) { t.Run("case=initial request succeeds and caches", func(t *testing.T) { config := setup(t, `{ "required_scope": [], "trusted_issuers": ["foo", "bar"], "target_audience": ["audience"] }`) - err = a.Authenticate(r, expected, config, nil) - require.NoError(t, err) - assertHandlerWasCalled(t) + require.NoError(t, a.Authenticate(r, expected, config, nil)) + assert.False(t, cacheWasUsed()) }) // We expect to use the cache here because we are not interested to validate the scope. Usually we would @@ -700,29 +696,26 @@ func TestAuthenticatorOAuth2Introspection(t *testing.T) { config := setup(t, `{ "trusted_issuers": ["foo", "bar"], "target_audience": ["audience"] }`) sess := new(AuthenticationSession) - err = a.Authenticate(r, sess, config, nil) - require.NoError(t, err) - assertCacheWasUsed(t) + require.NoError(t, a.Authenticate(r, sess, config, nil)) + assert.True(t, cacheWasUsed()) assertx.EqualAsJSON(t, expected, sess) }) t.Run("case=second request does not use cache because scope strategy is disabled and scope was requested request succeeds", func(t *testing.T) { config := setup(t, `{ "required_scope": ["scope-a"], "trusted_issuers": ["foo", "bar"], "target_audience": ["audience"] }`) sess := new(AuthenticationSession) - err = a.Authenticate(r, sess, config, nil) - require.NoError(t, err) - assertHandlerWasCalled(t) + require.NoError(t, a.Authenticate(r, sess, config, nil)) + assert.False(t, cacheWasUsed()) assertx.EqualAsJSON(t, expected, sess) }) t.Run("case=request fails because we requested a scope which the upstream does not validate", func(t *testing.T) { config := setup(t, `{ "required_scope": ["scope-b"], "trusted_issuers": ["foo", "bar"], "target_audience": ["audience"] }`) sess := new(AuthenticationSession) - err = a.Authenticate(r, sess, config, nil) - require.Error(t, err) - assertHandlerWasCalled(t) + require.ErrorIs(t, a.Authenticate(r, sess, config, nil), helper.ErrUnauthorized) + assert.False(t, cacheWasUsed()) }) }) @@ -737,10 +730,10 @@ func TestAuthenticatorOAuth2Introspection(t *testing.T) { config := setup(t, `{ "required_scope": ["scope-a"], "trusted_issuers": ["foo", "bar"], "target_audience": ["audience"] }`) // Also doesn't use the cache the second time - require.Error(t, a.Authenticate(r, expected, config, nil)) - assertHandlerWasCalled(t) - require.Error(t, a.Authenticate(r, expected, config, nil)) - assertHandlerWasCalled(t) + require.ErrorIs(t, a.Authenticate(r, expected, config, nil), helper.ErrUnauthorized) + assert.False(t, cacheWasUsed()) + require.ErrorIs(t, a.Authenticate(r, expected, config, nil), helper.ErrUnauthorized) + assert.False(t, cacheWasUsed()) }) } }) @@ -754,15 +747,15 @@ func TestAuthenticatorOAuth2Introspection(t *testing.T) { config := setup(t, `{ "required_scope": ["scope-a"], "trusted_issuers": ["foo", "bar"], "target_audience": ["audience"] }`) require.NoError(t, a.Authenticate(r, expected, config, nil)) - assertHandlerWasCalled(t) + assert.False(t, cacheWasUsed()) t.Run("case=request succeeds and uses the cache", func(t *testing.T) { config := setup(t, `{ "trusted_issuers": ["foo", "bar"], "target_audience": ["audience"] }`) sess := new(AuthenticationSession) err = a.Authenticate(r, sess, config, nil) require.NoError(t, err) - assertCacheWasUsed(t) + assert.True(t, cacheWasUsed()) assertx.EqualAsJSON(t, expected, sess) }) @@ -772,30 +765,26 @@ func TestAuthenticatorOAuth2Introspection(t *testing.T) { err = a.Authenticate(r, sess, config, nil) require.NoError(t, err) - assertCacheWasUsed(t) + assert.True(t, cacheWasUsed()) assertx.EqualAsJSON(t, expected, sess) }) t.Run("case=requests a scope the token does not have", func(t *testing.T) { - require.Error(t, a.Authenticate(r, new(AuthenticationSession), - setup(t, `{ "required_scope": ["scope-b"], "trusted_issuers": ["foo", "bar"], "target_audience": ["audience"] }`), - nil)) + config := setup(t, `{ "required_scope": ["scope-b"], "trusted_issuers": ["foo", "bar"], "target_audience": ["audience"] }`) + require.ErrorIs(t, a.Authenticate(r, new(AuthenticationSession), config, nil), helper.ErrForbidden) }) t.Run("case=requests an audience which the token does not have", func(t *testing.T) { - require.Error(t, a.Authenticate(r, new(AuthenticationSession), - setup(t, `{ "required_scope": ["scope-a"], "trusted_issuers": ["foo", "bar"], "target_audience": ["not-audience"] }`), - nil)) + config := setup(t, `{ "required_scope": ["scope-a"], "trusted_issuers": ["foo", "bar"], "target_audience": ["not-audience"] }`) + require.ErrorIs(t, a.Authenticate(r, new(AuthenticationSession), config, nil), helper.ErrForbidden) }) t.Run("case=does not trust the issuer", func(t *testing.T) { - require.Error(t, a.Authenticate(r, new(AuthenticationSession), - setup(t, `{ "required_scope": ["scope-a"], "trusted_issuers": ["not-foo", "bar"], "target_audience": ["audience"] }`), - nil)) + config := setup(t, `{ "required_scope": ["scope-a"], "trusted_issuers": ["not-foo", "bar"], "target_audience": ["audience"] }`) + require.ErrorIs(t, a.Authenticate(r, new(AuthenticationSession), config, nil), helper.ErrForbidden) }) t.Run("case=respects the expiry time", func(t *testing.T) { - setup(t, `{ "required_scope": ["scope-a"], "trusted_issuers": ["foo", "bar"], "target_audience": ["audience"] }`) require.NoError(t, a.Authenticate(r, new(AuthenticationSession), config, nil)) time.Sleep(2 * time.Second) require.Error(t, a.Authenticate(r, new(AuthenticationSession), config, nil)) @@ -806,22 +795,21 @@ func TestAuthenticatorOAuth2Introspection(t *testing.T) { config := setup(t, `{ "required_scope": ["scope-a"], "trusted_issuers": ["foo", "bar"], "target_audience": ["audience"], "cache": { "ttl": "100ms" } }`) require.NoError(t, a.Authenticate(r, expected, config, nil)) - assertHandlerWasCalled(t) + assert.False(t, cacheWasUsed()) - // wait cache to save value - time.Sleep(time.Millisecond * 10) + a.WaitForCache() require.NoError(t, a.Authenticate(r, new(AuthenticationSession), config, nil)) - assertCacheWasUsed(t) + assert.True(t, cacheWasUsed()) time.Sleep(50 * time.Millisecond) require.NoError(t, a.Authenticate(r, new(AuthenticationSession), config, nil)) - assertCacheWasUsed(t) + assert.True(t, cacheWasUsed()) time.Sleep(50 * time.Millisecond) // cache should have been cleared require.NoError(t, a.Authenticate(r, new(AuthenticationSession), config, nil)) - assertHandlerWasCalled(t) + assert.False(t, cacheWasUsed()) }) }) }) @@ -842,7 +830,7 @@ func TestAuthenticatorOAuth2Introspection(t *testing.T) { t.Run("method=config", func(t *testing.T) { logger := logrusx.New("test", "1") - authenticator := NewAuthenticatorOAuth2Introspection(conf, logger, trace.NewNoopTracerProvider()) //nolint:staticcheck // tests only need noop tracer + authenticator := NewAuthenticatorOAuth2Introspection(conf, logger, noop.NewTracerProvider()) noPreauthConfig := []byte(`{ "introspection_url":"http://localhost/oauth2/token" }`) preAuthConfigOne := []byte(`{ "introspection_url":"http://localhost/oauth2/token","pre_authorization":{"token_url":"http://localhost/oauth2/token","client_id":"some_id","client_secret":"some_secret","enabled":true} }`) @@ -892,22 +880,26 @@ func TestAuthenticatorOAuth2Introspection(t *testing.T) { require.NotEqual(t, noPreauthClient3, noPreauthClient) }) }) +} - t.Run("unmarshal-audience", func(t *testing.T) { - t.Run("Should pass because audience is a valid string", func(t *testing.T) { - var aud Audience - data := `"audience"` - json.Unmarshal([]byte(data), &aud) //nolint:errcheck,gosec // JSON unmarshalling errors ignored in table driven tests - require.NoError(t, err) - require.Equal(t, Audience{"audience"}, aud) - }) - - t.Run("Should pass because audience is a valid string array", func(t *testing.T) { +func TestAudienceUnmarshal(t *testing.T) { + for _, tc := range []struct { + name, + rawJSON string + expected Audience + }{{ + name: "string", + rawJSON: `"audience"`, + expected: Audience{"audience"}, + }, { + name: "string array", + rawJSON: `["audience1","audience2"]`, + expected: Audience{"audience1", "audience2"}, + }} { + t.Run("case="+tc.name, func(t *testing.T) { var aud Audience - data := `["audience1","audience2"]` - json.Unmarshal([]byte(data), &aud) //nolint:errcheck,gosec // JSON unmarshalling errors ignored in table driven tests - require.NoError(t, err) - require.Equal(t, Audience{"audience1", "audience2"}, aud) + require.NoError(t, json.Unmarshal([]byte(tc.rawJSON), &aud)) + require.Equal(t, tc.expected, aud) }) - }) + } }
pipeline/mutate/mutator_hydrator.go+10 −12 modified@@ -5,7 +5,7 @@ package mutate import ( "bytes" - "crypto/md5" //nolint:gosec + //nolint:gosec "encoding/json" "fmt" "net/http" @@ -99,11 +99,11 @@ func (a *MutatorHydrator) GetID() string { return "hydrator" } -func (a *MutatorHydrator) cacheKey(config *MutatorHydratorConfig, session string) string { - return fmt.Sprintf("%s|%x", config.Api.URL, md5.Sum([]byte(session))) //nolint:gosec +func (a *MutatorHydrator) cacheKey(config *MutatorHydratorConfig, session []byte) string { + return fmt.Sprintf("%s|%s", session, config.Api.URL) } -func (a *MutatorHydrator) hydrateFromCache(config *MutatorHydratorConfig, session string) (*authn.AuthenticationSession, bool) { +func (a *MutatorHydrator) hydrateFromCache(config *MutatorHydratorConfig, session []byte) (*authn.AuthenticationSession, bool) { if !config.Cache.Enabled { return nil, false } @@ -116,12 +116,12 @@ func (a *MutatorHydrator) hydrateFromCache(config *MutatorHydratorConfig, sessio return item.Copy(), true } -func (a *MutatorHydrator) hydrateToCache(config *MutatorHydratorConfig, key string, session *authn.AuthenticationSession) { +func (a *MutatorHydrator) hydrateToCache(config *MutatorHydratorConfig, rawSession []byte, session *authn.AuthenticationSession) { if !config.Cache.Enabled { return } - if a.hydrateCache.SetWithTTL(a.cacheKey(config, key), session.Copy(), 0, config.Cache.ttl) { + if a.hydrateCache.SetWithTTL(a.cacheKey(config, rawSession), session.Copy(), 0, config.Cache.ttl) { a.d.Logger().Debug("Cache reject item") } } @@ -132,12 +132,10 @@ func (a *MutatorHydrator) Mutate(r *http.Request, session *authn.AuthenticationS return err } - var b bytes.Buffer - if err := json.NewEncoder(&b).Encode(session); err != nil { + encodedSession, err := json.Marshal(session) + if err != nil { return errors.WithStack(err) } - - encodedSession := b.String() if cacheSession, ok := a.hydrateFromCache(cfg, encodedSession); ok { *session = *cacheSession return nil @@ -148,7 +146,7 @@ func (a *MutatorHydrator) Mutate(r *http.Request, session *authn.AuthenticationS } else if _, err := url.ParseRequestURI(cfg.Api.URL); err != nil { return errors.New(ErrInvalidAPIURL) } - req, err := http.NewRequest("POST", cfg.Api.URL, &b) + req, err := http.NewRequest("POST", cfg.Api.URL, bytes.NewReader(encodedSession)) if err != nil { return errors.WithStack(err) } @@ -200,7 +198,7 @@ func (a *MutatorHydrator) Mutate(r *http.Request, session *authn.AuthenticationS if err != nil { return errors.WithStack(err) } - defer res.Body.Close() //nolint:errcheck + defer func() { _ = res.Body.Close() }() switch res.StatusCode { case http.StatusOK:
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- github.com/ory/oathkeeper/commit/198a2bc82a99e0a77bd0ffe290cbdd5285a1b17cnvdPatchWEB
- github.com/advisories/GHSA-4mq7-pvjg-xp2rghsaADVISORY
- github.com/ory/oathkeeper/security/advisories/GHSA-4mq7-pvjg-xp2rnvdMitigationVendor AdvisoryWEB
- nvd.nist.gov/vuln/detail/CVE-2026-33496ghsaADVISORY
News mentions
0No linked articles in our index yet.