VYPR
High severity8.1NVD Advisory· Published Mar 26, 2026· Updated Apr 7, 2026

CVE-2026-33496

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.

PackageAffected versionsPatched versions
github.com/ory/oathkeeperGo
< 0.40.10-0.20260320084801-198a2bc82a990.40.10-0.20260320084801-198a2bc82a99

Affected products

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

Patches

1
198a2bc82a99

fix: scope cache config key to introspection URL

https://github.com/ory/oathkeeperPatrikMar 12, 2026via ghsa
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

News mentions

0

No linked articles in our index yet.