ORY Oathkeeper is an Identity & Access Proxy (IAP) and Access Control Decision API that authorizes HTTP requests based on sets of Access Rules. When you make a request to an endpoint that requires the scope `foo` using an access token granted with that `foo` scope, introspection will be valid and that token will be cached. The problem comes when a second requests to an endpoint that requires the scope `bar` is made before the cache has expired. Whether the token is granted or not to the `bar` scope, introspection will be valid. A patch will be released with `v0.38.12-beta.1`. Per default, caching is disabled for the `oauth2_introspection` authenticator. When caching is disabled, this vulnerability does not exist. The cache is checked in [`func (a *AuthenticatorOAuth2Introspection) Authenticate(...)`](https://github.com/ory/oathkeeper/blob/6a31df1c3779425e05db1c2a381166b087cb29a4/pipeline/authn/authenticator_oauth2_introspection.go#L152). From [`tokenFromCache()`](https://github.com/ory/oathkeeper/blob/6a31df1c3779425e05db1c2a381166b087cb29a4/pipeline/authn/authenticator_oauth2_introspection.go#L97) it seems that it only validates the token expiration date, but ignores whether the token has or not the proper scopes. The vulnerability was introduced in PR #424. During review, we failed to require appropriate test coverage by the submitter which is the primary reason that the vulnerability passed the review process.

func (a *AuthenticatorOAuth2Introspection) tokenFromCache(config *AuthenticatorOAuth2IntrospectionConfiguration, token string) (*AuthenticatorOAuth2IntrospectionResult, bool) {
	if !config.Cache.Enabled {
		return nil, false
	}

	item, found := a.tokenCache.Get(token)
	if !found {
		return nil, false
	}

	i := item.(*AuthenticatorOAuth2IntrospectionResult)
	expires := time.Unix(i.Expires, 0)
	if expires.Before(time.Now()) {
		a.tokenCache.Del(token)
		return nil, false
	}

	return i, true
}

func (a *AuthenticatorOAuth2Introspection) tokenToCache(config *AuthenticatorOAuth2IntrospectionConfiguration, i *AuthenticatorOAuth2IntrospectionResult, token string) {
	if !config.Cache.Enabled {
		return
	}

	if a.cacheTTL != nil {
		a.tokenCache.SetWithTTL(token, i, 1, *a.cacheTTL)
	} else {
		a.tokenCache.Set(token, i, 1)
	}
}

func (a *AuthenticatorOAuth2Introspection) Authenticate(r *http.Request, session *AuthenticationSession, config json.RawMessage, _ pipeline.Rule) error {
	cf, client, err := a.Config(config)
	if err != nil {
		return err
	}

	token := helper.BearerTokenFromRequest(r, cf.BearerTokenLocation)
	if token == "" {
		return errors.WithStack(ErrAuthenticatorNotResponsible)
	}

	ss := a.c.ToScopeStrategy(cf.ScopeStrategy, "authenticators.oauth2_introspection.scope_strategy")

	i, ok := a.tokenFromCache(cf, token)
	if !ok {
		body := url.Values{"token": {token}}

		if ss == nil {
			body.Add("scope", strings.Join(cf.Scopes, " "))
		}

		introspectReq, err := http.NewRequest(http.MethodPost, cf.IntrospectionURL, strings.NewReader(body.Encode()))
		if err != nil {
			return errors.WithStack(err)
		}
		for key, value := range cf.IntrospectionRequestHeaders {
			introspectReq.Header.Set(key, value)
		}
		// set/override the content-type header
		introspectReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")

		// add tracing
		closeSpan := a.traceRequest(r.Context(), introspectReq)

		resp, err := client.Do(introspectReq.WithContext(r.Context()))

		// close the span so it represents just the http request
		closeSpan()
		if err != nil {
			return errors.WithStack(err)
		}
		defer resp.Body.Close()

		if resp.StatusCode != http.StatusOK {
			return errors.Errorf("Introspection returned status code %d but expected %d", resp.StatusCode, http.StatusOK)
		}

		if err := json.NewDecoder(resp.Body).Decode(&i); err != nil {
			return errors.WithStack(err)
		}

		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.Active {
			return errors.WithStack(helper.ErrUnauthorized.WithReason("Access token i says token is not active"))
		}

		for _, audience := range cf.Audience {
			if !stringslice.Has(i.Audience, audience) {
				return errors.WithStack(helper.ErrForbidden.WithReason(fmt.Sprintf("Token audience is not intended for target audience %s", audience)))
			}
		}

		if len(cf.Issuers) > 0 {
			if !stringslice.Has(cf.Issuers, i.Issuer) {
				return errors.WithStack(helper.ErrForbidden.WithReason(fmt.Sprintf("Token issuer does not match any trusted issuer")))
			}
		}

		if ss != nil {
			for _, scope := range cf.Scopes {
				if !ss(strings.Split(i.Scope, " "), scope) {
					return errors.WithStack(helper.ErrForbidden.WithReason(fmt.Sprintf("Scope %s was not granted", scope)))
				}
			}
		}

		if len(i.Extra) == 0 {
			i.Extra = map[string]interface{}{}
		}

		i.Extra["username"] = i.Username
		i.Extra["client_id"] = i.ClientID
		i.Extra["scope"] = i.Scope

		if len(i.Audience) != 0 {
			i.Extra["aud"] = i.Audience
		}

		a.tokenToCache(cf, i, token)
	}

	session.Subject = i.Subject
	session.Extra = i.Extra

	return nil
}

func (a *AuthenticatorOAuth2Introspection) Config(config json.RawMessage) (*AuthenticatorOAuth2IntrospectionConfiguration, *http.Client, error) {
	var c AuthenticatorOAuth2IntrospectionConfiguration
	if err := a.c.AuthenticatorConfig(a.GetID(), config, &c); err != nil {
		return nil, nil, NewErrAuthenticatorMisconfigured(a, err)
	}

	clientKey := fmt.Sprintf("%x", md5.Sum([]byte(config)))
	a.mu.RLock()
	client, ok := a.clientMap[clientKey]
	a.mu.RUnlock()

	if !ok {
		a.logger.Debug("Initializing http client")
		var rt http.RoundTripper
		if c.PreAuth != nil && c.PreAuth.Enabled {
			var ep url.Values

			if c.PreAuth.Audience != "" {
				ep = url.Values{"audience": {c.PreAuth.Audience}}
			}

			rt = (&clientcredentials.Config{
				ClientID:       c.PreAuth.ClientID,
				ClientSecret:   c.PreAuth.ClientSecret,
				Scopes:         c.PreAuth.Scope,
				EndpointParams: ep,
				TokenURL:       c.PreAuth.TokenURL,
			}).Client(context.Background()).Transport
		}

		if c.Retry == nil {
			c.Retry = &AuthenticatorOAuth2IntrospectionRetryConfiguration{Timeout: "500ms", MaxWait: "1s"}
		} else {
			if c.Retry.Timeout == "" {
				c.Retry.Timeout = "500ms"
			}
			if c.Retry.MaxWait == "" {
				c.Retry.MaxWait = "1s"
			}
		}
		duration, err := time.ParseDuration(c.Retry.Timeout)
		if err != nil {
			return nil, nil, err
		}
		timeout := time.Millisecond * duration

		maxWait, err := time.ParseDuration(c.Retry.MaxWait)
		if err != nil {
			return nil, nil, err
		}

		client = httpx.NewResilientClientLatencyToleranceConfigurable(rt, timeout, maxWait)
		a.mu.Lock()
		a.clientMap[clientKey] = client
		a.mu.Unlock()
	}

	if c.Cache.TTL != "" {
		cacheTTL, err := time.ParseDuration(c.Cache.TTL)
		if err != nil {
			return nil, nil, err
		}
		a.cacheTTL = &cacheTTL
	}

	if a.tokenCache == nil {
		a.logger.Debugf("Creating cache with max cost: %d", c.Cache.MaxCost)
		cache, _ := ristretto.NewCache(&ristretto.Config{
			// This will hold about 1000 unique mutation responses.
			NumCounters: 10000,
			// Allocate a max
			MaxCost: int64(c.Cache.MaxCost),
			// This is a best-practice value.
			BufferItems: 64,
		})

		a.tokenCache = cache
	}

	return &c, client, nil
}

import (
	"context"
	"crypto/md5"
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"strings"
	"sync"
	"time"

	"github.com/ory/fosite"

	"github.com/dgraph-io/ristretto"

	"github.com/opentracing/opentracing-go"
	"github.com/opentracing/opentracing-go/ext"

	"github.com/pkg/errors"
	"golang.org/x/oauth2/clientcredentials"

	"github.com/ory/go-convenience/stringslice"
	"github.com/ory/x/httpx"
	"github.com/ory/x/logrusx"

	"github.com/ory/oathkeeper/driver/configuration"
	"github.com/ory/oathkeeper/helper"
	"github.com/ory/oathkeeper/pipeline"
)

func (a *AuthenticatorOAuth2Introspection) tokenFromCache(config *AuthenticatorOAuth2IntrospectionConfiguration, token string, ss fosite.ScopeStrategy) *AuthenticatorOAuth2IntrospectionResult {
	if !config.Cache.Enabled {
		return nil
	}

	if ss == nil && len(config.Scopes) > 0 {
		return nil
	}

	item, found := a.tokenCache.Get(token)
	if !found {
		return nil
	}

	i, ok := item.(*AuthenticatorOAuth2IntrospectionResult)
	if !ok {
		return nil
	}

	return i
}

func (a *AuthenticatorOAuth2Introspection) tokenToCache(config *AuthenticatorOAuth2IntrospectionConfiguration, i *AuthenticatorOAuth2IntrospectionResult, token string, ss fosite.ScopeStrategy) {
	if !config.Cache.Enabled {
		return
	}

	if ss == nil && len(config.Scopes) > 0 {
		return
	}

	if a.cacheTTL != nil {
		a.tokenCache.SetWithTTL(token, i, 1, *a.cacheTTL)
	} else {
		a.tokenCache.Set(token, i, 1)
	}
}

func (a *AuthenticatorOAuth2Introspection) Authenticate(r *http.Request, session *AuthenticationSession, config json.RawMessage, _ pipeline.Rule) error {
	cf, client, err := a.Config(config)
	if err != nil {
		return err
	}

	token := helper.BearerTokenFromRequest(r, cf.BearerTokenLocation)
	if token == "" {
		return errors.WithStack(ErrAuthenticatorNotResponsible)
	}

	ss := a.c.ToScopeStrategy(cf.ScopeStrategy, "authenticators.oauth2_introspection.config.scope_strategy")

	i := a.tokenFromCache(cf, token, ss)

	// If the token can not be found, and the scope strategy is nil, and the required scope list
	// is not empty, then we can not use the cache.
	if i == nil {
		body := url.Values{"token": {token}}
		if ss == nil {
			body.Add("scope", strings.Join(cf.Scopes, " "))
		}

		introspectReq, err := http.NewRequest(http.MethodPost, cf.IntrospectionURL, strings.NewReader(body.Encode()))
		if err != nil {
			return errors.WithStack(err)
		}

		for key, value := range cf.IntrospectionRequestHeaders {
			introspectReq.Header.Set(key, value)
		}
		// set/override the content-type header
		introspectReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")

		// add tracing
		closeSpan := a.traceRequest(r.Context(), introspectReq)

		resp, err := client.Do(introspectReq.WithContext(r.Context()))

		// close the span so it represents just the http request
		closeSpan()
		if err != nil {
			return errors.WithStack(err)
		}
		defer resp.Body.Close()

		if resp.StatusCode != http.StatusOK {
			return errors.Errorf("Introspection returned status code %d but expected %d", resp.StatusCode, http.StatusOK)
		}

		if err := json.NewDecoder(resp.Body).Decode(&i); err != nil {
			return errors.WithStack(err)
		}
	}

	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.Active {
		return errors.WithStack(helper.ErrUnauthorized.WithReason("Access token is not active"))
	}

	if i.Expires > 0 && time.Unix(i.Expires, 0).Before(time.Now()) {
		return errors.WithStack(helper.ErrUnauthorized.WithReason("Access token expired"))
	}

	for _, audience := range cf.Audience {
		if !stringslice.Has(i.Audience, audience) {
			return errors.WithStack(helper.ErrForbidden.WithReason(fmt.Sprintf("Token audience is not intended for target audience %s", audience)))
		}
	}

	if len(cf.Issuers) > 0 {
		if !stringslice.Has(cf.Issuers, i.Issuer) {
			return errors.WithStack(helper.ErrForbidden.WithReason(fmt.Sprintf("Token issuer does not match any trusted issuer")))
		}
	}

	if ss != nil {
		for _, scope := range cf.Scopes {
			if !ss(strings.Split(i.Scope, " "), scope) {
				return errors.WithStack(helper.ErrForbidden.WithReason(fmt.Sprintf("Scope %s was not granted", scope)))
			}
		}
	}

	a.tokenToCache(cf, i, token, ss)

	if len(i.Extra) == 0 {
		i.Extra = map[string]interface{}{}
	}

	i.Extra["username"] = i.Username
	i.Extra["client_id"] = i.ClientID
	i.Extra["scope"] = i.Scope

	if len(i.Audience) != 0 {
		i.Extra["aud"] = i.Audience
	}

	session.Subject = i.Subject
	session.Extra = i.Extra

	return nil
}

func (a *AuthenticatorOAuth2Introspection) Config(config json.RawMessage) (*AuthenticatorOAuth2IntrospectionConfiguration, *http.Client, error) {
	var c AuthenticatorOAuth2IntrospectionConfiguration
	if err := a.c.AuthenticatorConfig(a.GetID(), config, &c); err != nil {
		return nil, nil, NewErrAuthenticatorMisconfigured(a, err)
	}

	clientKey := fmt.Sprintf("%x", md5.Sum([]byte(config)))
	a.mu.RLock()
	client, ok := a.clientMap[clientKey]
	a.mu.RUnlock()

	if !ok {
		a.logger.Debug("Initializing http client")
		var rt http.RoundTripper
		if c.PreAuth != nil && c.PreAuth.Enabled {
			var ep url.Values

			if c.PreAuth.Audience != "" {
				ep = url.Values{"audience": {c.PreAuth.Audience}}
			}

			rt = (&clientcredentials.Config{
				ClientID:       c.PreAuth.ClientID,
				ClientSecret:   c.PreAuth.ClientSecret,
				Scopes:         c.PreAuth.Scope,
				EndpointParams: ep,
				TokenURL:       c.PreAuth.TokenURL,
			}).Client(context.Background()).Transport
		}

		if c.Retry == nil {
			c.Retry = &AuthenticatorOAuth2IntrospectionRetryConfiguration{Timeout: "500ms", MaxWait: "1s"}
		} else {
			if c.Retry.Timeout == "" {
				c.Retry.Timeout = "500ms"
			}
			if c.Retry.MaxWait == "" {
				c.Retry.MaxWait = "1s"
			}
		}
		duration, err := time.ParseDuration(c.Retry.Timeout)
		if err != nil {
			return nil, nil, err
		}
		timeout := time.Millisecond * duration

		maxWait, err := time.ParseDuration(c.Retry.MaxWait)
		if err != nil {
			return nil, nil, err
		}

		client = httpx.NewResilientClientLatencyToleranceConfigurable(rt, timeout, maxWait)
		a.mu.Lock()
		a.clientMap[clientKey] = client
		a.mu.Unlock()
	}

	if c.Cache.TTL != "" {
		cacheTTL, err := time.ParseDuration(c.Cache.TTL)
		if err != nil {
			return nil, nil, err
		}
		a.cacheTTL = &cacheTTL
	}

	if a.tokenCache == nil {
		cost := int64(c.Cache.MaxCost)
		if cost == 0 {
			cost = 100000000
		}
		a.logger.Debugf("Creating cache with max cost: %d", c.Cache.MaxCost)
		cache, err := ristretto.NewCache(&ristretto.Config{
			// This will hold about 1000 unique mutation responses.
			NumCounters: 10000,
			// Allocate a max
			MaxCost: cost,
			// This is a best-practice value.
			BufferItems: 64,
		})
		if err != nil {
			return nil, nil, err
		}

		a.tokenCache = cache
	}

	return &c, client, nil
}

--- a/./pipeline/authn/authenticator_oauth2_introspection.go
+++ b/./pipeline/authn/authenticator_oauth2_introspection.go
@@ -1,19 +1,21 @@
-func (a *AuthenticatorOAuth2Introspection) tokenFromCache(config *AuthenticatorOAuth2IntrospectionConfiguration, token string) (*AuthenticatorOAuth2IntrospectionResult, bool) {
+func (a *AuthenticatorOAuth2Introspection) tokenFromCache(config *AuthenticatorOAuth2IntrospectionConfiguration, token string, ss fosite.ScopeStrategy) *AuthenticatorOAuth2IntrospectionResult {
 	if !config.Cache.Enabled {
-		return nil, false
+		return nil
+	}
+
+	if ss == nil && len(config.Scopes) > 0 {
+		return nil
 	}

 	item, found := a.tokenCache.Get(token)
 	if !found {
-		return nil, false
+		return nil
 	}

-	i := item.(*AuthenticatorOAuth2IntrospectionResult)
-	expires := time.Unix(i.Expires, 0)
-	if expires.Before(time.Now()) {
-		a.tokenCache.Del(token)
-		return nil, false
+	i, ok := item.(*AuthenticatorOAuth2IntrospectionResult)
+	if !ok {
+		return nil
 	}

-	return i, true
+	return i
 }

--- a/./pipeline/authn/authenticator_oauth2_introspection.go
+++ b/./pipeline/authn/authenticator_oauth2_introspection.go
@@ -1,5 +1,9 @@
-func (a *AuthenticatorOAuth2Introspection) tokenToCache(config *AuthenticatorOAuth2IntrospectionConfiguration, i *AuthenticatorOAuth2IntrospectionResult, token string) {
+func (a *AuthenticatorOAuth2Introspection) tokenToCache(config *AuthenticatorOAuth2IntrospectionConfiguration, i *AuthenticatorOAuth2IntrospectionResult, token string, ss fosite.ScopeStrategy) {
 	if !config.Cache.Enabled {
+		return
+	}
+
+	if ss == nil && len(config.Scopes) > 0 {
 		return
 	}

--- a/./pipeline/authn/authenticator_oauth2_introspection.go
+++ b/./pipeline/authn/authenticator_oauth2_introspection.go
@@ -9,12 +9,14 @@
 		return errors.WithStack(ErrAuthenticatorNotResponsible)
 	}

-	ss := a.c.ToScopeStrategy(cf.ScopeStrategy, "authenticators.oauth2_introspection.scope_strategy")
+	ss := a.c.ToScopeStrategy(cf.ScopeStrategy, "authenticators.oauth2_introspection.config.scope_strategy")

-	i, ok := a.tokenFromCache(cf, token)
-	if !ok {
+	i := a.tokenFromCache(cf, token, ss)
+
+	// If the token can not be found, and the scope strategy is nil, and the required scope list
+	// is not empty, then we can not use the cache.
+	if i == nil {
 		body := url.Values{"token": {token}}
-
 		if ss == nil {
 			body.Add("scope", strings.Join(cf.Scopes, " "))
 		}
@@ -23,6 +25,7 @@
 		if err != nil {
 			return errors.WithStack(err)
 		}
+
 		for key, value := range cf.IntrospectionRequestHeaders {
 			introspectReq.Header.Set(key, value)
 		}
@@ -48,48 +51,52 @@
 		if err := json.NewDecoder(resp.Body).Decode(&i); err != nil {
 			return errors.WithStack(err)
 		}
+	}

-		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 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.Active {
+		return errors.WithStack(helper.ErrUnauthorized.WithReason("Access token is not active"))
+	}
+
+	if i.Expires > 0 && time.Unix(i.Expires, 0).Before(time.Now()) {
+		return errors.WithStack(helper.ErrUnauthorized.WithReason("Access token expired"))
+	}
+
+	for _, audience := range cf.Audience {
+		if !stringslice.Has(i.Audience, audience) {
+			return errors.WithStack(helper.ErrForbidden.WithReason(fmt.Sprintf("Token audience is not intended for target audience %s", audience)))
 		}
+	}

-		if !i.Active {
-			return errors.WithStack(helper.ErrUnauthorized.WithReason("Access token i says token is not active"))
+	if len(cf.Issuers) > 0 {
+		if !stringslice.Has(cf.Issuers, i.Issuer) {
+			return errors.WithStack(helper.ErrForbidden.WithReason(fmt.Sprintf("Token issuer does not match any trusted issuer")))
 		}
+	}

-		for _, audience := range cf.Audience {
-			if !stringslice.Has(i.Audience, audience) {
-				return errors.WithStack(helper.ErrForbidden.WithReason(fmt.Sprintf("Token audience is not intended for target audience %s", audience)))
+	if ss != nil {
+		for _, scope := range cf.Scopes {
+			if !ss(strings.Split(i.Scope, " "), scope) {
+				return errors.WithStack(helper.ErrForbidden.WithReason(fmt.Sprintf("Scope %s was not granted", scope)))
 			}
 		}
+	}

-		if len(cf.Issuers) > 0 {
-			if !stringslice.Has(cf.Issuers, i.Issuer) {
-				return errors.WithStack(helper.ErrForbidden.WithReason(fmt.Sprintf("Token issuer does not match any trusted issuer")))
-			}
-		}
+	a.tokenToCache(cf, i, token, ss)

-		if ss != nil {
-			for _, scope := range cf.Scopes {
-				if !ss(strings.Split(i.Scope, " "), scope) {
-					return errors.WithStack(helper.ErrForbidden.WithReason(fmt.Sprintf("Scope %s was not granted", scope)))
-				}
-			}
-		}
+	if len(i.Extra) == 0 {
+		i.Extra = map[string]interface{}{}
+	}

-		if len(i.Extra) == 0 {
-			i.Extra = map[string]interface{}{}
-		}
+	i.Extra["username"] = i.Username
+	i.Extra["client_id"] = i.ClientID
+	i.Extra["scope"] = i.Scope

-		i.Extra["username"] = i.Username
-		i.Extra["client_id"] = i.ClientID
-		i.Extra["scope"] = i.Scope
-
-		if len(i.Audience) != 0 {
-			i.Extra["aud"] = i.Audience
-		}
-
-		a.tokenToCache(cf, i, token)
+	if len(i.Audience) != 0 {
+		i.Extra["aud"] = i.Audience
 	}

 	session.Subject = i.Subject

--- a/./pipeline/authn/authenticator_oauth2_introspection.go
+++ b/./pipeline/authn/authenticator_oauth2_introspection.go
@@ -64,15 +64,22 @@
 	}

 	if a.tokenCache == nil {
+		cost := int64(c.Cache.MaxCost)
+		if cost == 0 {
+			cost = 100000000
+		}
 		a.logger.Debugf("Creating cache with max cost: %d", c.Cache.MaxCost)
-		cache, _ := ristretto.NewCache(&ristretto.Config{
+		cache, err := ristretto.NewCache(&ristretto.Config{
 			// This will hold about 1000 unique mutation responses.
 			NumCounters: 10000,
 			// Allocate a max
-			MaxCost: int64(c.Cache.MaxCost),
+			MaxCost: cost,
 			// This is a best-practice value.
 			BufferItems: 64,
 		})
+		if err != nil {
+			return nil, nil, err
+		}

 		a.tokenCache = cache
 	}

--- /dev/null
+++ b/./pipeline/authn/authenticator_oauth2_introspection.go
@@ -0,0 +1,29 @@
+import (
+	"context"
+	"crypto/md5"
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"net/url"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/ory/fosite"
+
+	"github.com/dgraph-io/ristretto"
+
+	"github.com/opentracing/opentracing-go"
+	"github.com/opentracing/opentracing-go/ext"
+
+	"github.com/pkg/errors"
+	"golang.org/x/oauth2/clientcredentials"
+
+	"github.com/ory/go-convenience/stringslice"
+	"github.com/ory/x/httpx"
+	"github.com/ory/x/logrusx"
+
+	"github.com/ory/oathkeeper/driver/configuration"
+	"github.com/ory/oathkeeper/helper"
+	"github.com/ory/oathkeeper/pipeline"
+)

#!/bin/bash
# From ghcr.io/anonymous2578-data/cve-2021-32701:latest
# bash /workspace/fix-run.sh
set -e

cd /workspace/oathkeeper
rm -rf ./pipeline/authn/authenticator_oauth2_introspection_test.go
git apply --whitespace=nowarn /workspace/test.patch /workspace/fix.patch
go test  -timeout 30s -run ^TestAuthenticatorOAuth2Introspection$/^method=authenticate-with-cache$ github.com/ory/oathkeeper/pipeline/authn

#!/bin/bash
# From ghcr.io/anonymous2578-data/cve-2021-32701:latest
# bash /workspace/unit_test.sh
set -e

cd /workspace/oathkeeper
git apply --whitespace=nowarn /workspace/fix.patch
 go test  -timeout 30s -run ^TestAuthenticatorOAuth2Introspection$ github.com/ory/oathkeeper/pipeline/authn


MinIO is a Kubernetes native application for cloud storage. Prior to version `RELEASE.2021-12-27T07-23-18Z`, a malicious client can hand-craft an HTTP API call that allows for updating policy for a user and gaining higher privileges. The patch in version `RELEASE.2021-12-27T07-23-18Z` changes the accepted request body type and removes the ability to apply policy changes through this API. There is a workaround for this vulnerability: Changing passwords can be disabled by adding an explicit `Deny` rule to disable the API for users.

Incorrect Authorization

The product performs an authorization check when an actor attempts to access a resource or perform an action, but it does not correctly perform the check.

func (a adminAPIHandlers) AddUser(w http.ResponseWriter, r *http.Request) {
	ctx := newContext(r, w, "AddUser")

	defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))

	vars := mux.Vars(r)
	accessKey := vars["accessKey"]

	// Get current object layer instance.
	objectAPI := newObjectLayerFn()
	if objectAPI == nil || globalNotificationSys == nil {
		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
		return
	}

	cred, claims, owner, s3Err := validateAdminSignature(ctx, r, "")
	if s3Err != ErrNone {
		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
		return
	}

	// Not allowed to add a user with same access key as root credential
	if owner && accessKey == cred.AccessKey {
		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAddUserInvalidArgument), r.URL)
		return
	}

	userCred, exists := globalIAMSys.GetUser(ctx, accessKey)
	if exists && (userCred.IsTemp() || userCred.IsServiceAccount()) {
		// Updating STS credential is not allowed, and this API does not
		// support updating service accounts.
		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAddUserInvalidArgument), r.URL)
		return
	}

	if (cred.IsTemp() || cred.IsServiceAccount()) && cred.ParentUser == accessKey {
		// Incoming access key matches parent user then we should
		// reject password change requests.
		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAddUserInvalidArgument), r.URL)
		return
	}

	checkDenyOnly := false
	if accessKey == cred.AccessKey {
		// Check that there is no explicit deny - otherwise it's allowed
		// to change one's own password.
		checkDenyOnly = true
	}

	if !globalIAMSys.IsAllowed(iampolicy.Args{
		AccountName:     cred.AccessKey,
		Groups:          cred.Groups,
		Action:          iampolicy.CreateUserAdminAction,
		ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
		IsOwner:         owner,
		Claims:          claims,
		DenyOnly:        checkDenyOnly,
	}) {
		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
		return
	}

	if r.ContentLength > maxEConfigJSONSize || r.ContentLength == -1 {
		// More than maxConfigSize bytes were available
		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigTooLarge), r.URL)
		return
	}

	password := cred.SecretKey
	configBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength))
	if err != nil {
		logger.LogIf(ctx, err)
		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), r.URL)
		return
	}

	var uinfo madmin.UserInfo
	if err = json.Unmarshal(configBytes, &uinfo); err != nil {
		logger.LogIf(ctx, err)
		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), r.URL)
		return
	}

	if err = globalIAMSys.CreateUser(ctx, accessKey, uinfo); err != nil {
		writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
		return
	}
}

func (store *IAMStoreSys) AddUser(ctx context.Context, accessKey string, uinfo madmin.UserInfo) error {
	cache := store.lock()
	defer store.unlock()

	cr, ok := cache.iamUsersMap[accessKey]

	// It is not possible to update an STS account.
	if ok && cr.IsTemp() {
		return errIAMActionNotAllowed
	}

	u := newUserIdentity(auth.Credentials{
		AccessKey: accessKey,
		SecretKey: uinfo.SecretKey,
		Status: func() string {
			if uinfo.Status == madmin.AccountEnabled {
				return auth.AccountOn
			}
			return auth.AccountOff
		}(),
	})

	if err := store.saveUserIdentity(ctx, accessKey, regUser, u); err != nil {
		return err
	}

	cache.iamUsersMap[accessKey] = u.Credentials

	// Set policy if specified.
	if uinfo.PolicyName != "" {
		policy := uinfo.PolicyName
		// Handle policy mapping set/update
		mp := newMappedPolicy(policy)
		for _, p := range mp.toSlice() {
			if _, found := cache.iamPolicyDocsMap[policy]; !found {
				logger.LogIf(GlobalContext, fmt.Errorf("%w: (%s)", errNoSuchPolicy, p))
				return errNoSuchPolicy
			}
		}

		if err := store.saveMappedPolicy(ctx, accessKey, regUser, false, mp); err != nil {
			return err
		}
		cache.iamUserPolicyMap[accessKey] = mp
	}
	return nil

}

func (sys *IAMSys) CreateUser(ctx context.Context, accessKey string, uinfo madmin.UserInfo) error {
	if !sys.Initialized() {
		return errServerNotInitialized
	}

	if sys.usersSysType != MinIOUsersSysType {
		return errIAMActionNotAllowed
	}

	if !auth.IsAccessKeyValid(accessKey) {
		return auth.ErrInvalidAccessKeyLength
	}

	if !auth.IsSecretKeyValid(uinfo.SecretKey) {
		return auth.ErrInvalidSecretKeyLength
	}

	err := sys.store.AddUser(ctx, accessKey, uinfo)
	if err != nil {
		return err
	}

	sys.notifyForUser(ctx, accessKey, false)
	return nil
}

func (a adminAPIHandlers) AddUser(w http.ResponseWriter, r *http.Request) {
	ctx := newContext(r, w, "AddUser")

	defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r))

	vars := mux.Vars(r)
	accessKey := vars["accessKey"]

	// Get current object layer instance.
	objectAPI := newObjectLayerFn()
	if objectAPI == nil || globalNotificationSys == nil {
		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrServerNotInitialized), r.URL)
		return
	}

	cred, claims, owner, s3Err := validateAdminSignature(ctx, r, "")
	if s3Err != ErrNone {
		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
		return
	}

	// Not allowed to add a user with same access key as root credential
	if owner && accessKey == cred.AccessKey {
		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAddUserInvalidArgument), r.URL)
		return
	}

	userCred, exists := globalIAMSys.GetUser(ctx, accessKey)
	if exists && (userCred.IsTemp() || userCred.IsServiceAccount()) {
		// Updating STS credential is not allowed, and this API does not
		// support updating service accounts.
		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAddUserInvalidArgument), r.URL)
		return
	}

	if (cred.IsTemp() || cred.IsServiceAccount()) && cred.ParentUser == accessKey {
		// Incoming access key matches parent user then we should
		// reject password change requests.
		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAddUserInvalidArgument), r.URL)
		return
	}

	checkDenyOnly := false
	if accessKey == cred.AccessKey {
		// Check that there is no explicit deny - otherwise it's allowed
		// to change one's own password.
		checkDenyOnly = true
	}

	if !globalIAMSys.IsAllowed(iampolicy.Args{
		AccountName:     cred.AccessKey,
		Groups:          cred.Groups,
		Action:          iampolicy.CreateUserAdminAction,
		ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
		IsOwner:         owner,
		Claims:          claims,
		DenyOnly:        checkDenyOnly,
	}) {
		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
		return
	}

	if r.ContentLength > maxEConfigJSONSize || r.ContentLength == -1 {
		// More than maxConfigSize bytes were available
		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigTooLarge), r.URL)
		return
	}

	password := cred.SecretKey
	configBytes, err := madmin.DecryptData(password, io.LimitReader(r.Body, r.ContentLength))
	if err != nil {
		logger.LogIf(ctx, err)
		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), r.URL)
		return
	}

	var ureq madmin.AddOrUpdateUserReq
	if err = json.Unmarshal(configBytes, &ureq); err != nil {
		logger.LogIf(ctx, err)
		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), r.URL)
		return
	}

	if err = globalIAMSys.CreateUser(ctx, accessKey, ureq); err != nil {
		writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
		return
	}
}

func (store *IAMStoreSys) AddUser(ctx context.Context, accessKey string, ureq madmin.AddOrUpdateUserReq) error {
	cache := store.lock()
	defer store.unlock()

	cr, ok := cache.iamUsersMap[accessKey]

	// It is not possible to update an STS account.
	if ok && cr.IsTemp() {
		return errIAMActionNotAllowed
	}

	u := newUserIdentity(auth.Credentials{
		AccessKey: accessKey,
		SecretKey: ureq.SecretKey,
		Status: func() string {
			if ureq.Status == madmin.AccountEnabled {
				return auth.AccountOn
			}
			return auth.AccountOff
		}(),
	})

	if err := store.saveUserIdentity(ctx, accessKey, regUser, u); err != nil {
		return err
	}

	cache.iamUsersMap[accessKey] = u.Credentials

	return nil
}

func (sys *IAMSys) CreateUser(ctx context.Context, accessKey string, ureq madmin.AddOrUpdateUserReq) error {
	if !sys.Initialized() {
		return errServerNotInitialized
	}

	if sys.usersSysType != MinIOUsersSysType {
		return errIAMActionNotAllowed
	}

	if !auth.IsAccessKeyValid(accessKey) {
		return auth.ErrInvalidAccessKeyLength
	}

	if !auth.IsSecretKeyValid(ureq.SecretKey) {
		return auth.ErrInvalidSecretKeyLength
	}

	err := sys.store.AddUser(ctx, accessKey, ureq)
	if err != nil {
		return err
	}

	sys.notifyForUser(ctx, accessKey, false)
	return nil
}

--- a/cmd/admin-handlers-users.go
+++ b/cmd/admin-handlers-users.go
@@ -74,14 +74,14 @@
 		return
 	}

-	var uinfo madmin.UserInfo
-	if err = json.Unmarshal(configBytes, &uinfo); err != nil {
+	var ureq madmin.AddOrUpdateUserReq
+	if err = json.Unmarshal(configBytes, &ureq); err != nil {
 		logger.LogIf(ctx, err)
 		writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrAdminConfigBadJSON), r.URL)
 		return
 	}

-	if err = globalIAMSys.CreateUser(ctx, accessKey, uinfo); err != nil {
+	if err = globalIAMSys.CreateUser(ctx, accessKey, ureq); err != nil {
 		writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
 		return
 	}

--- a/cmd/iam-store.go
+++ b/cmd/iam-store.go
@@ -1,4 +1,4 @@
-func (store *IAMStoreSys) AddUser(ctx context.Context, accessKey string, uinfo madmin.UserInfo) error {
+func (store *IAMStoreSys) AddUser(ctx context.Context, accessKey string, ureq madmin.AddOrUpdateUserReq) error {
 	cache := store.lock()
 	defer store.unlock()

@@ -11,9 +11,9 @@

 	u := newUserIdentity(auth.Credentials{
 		AccessKey: accessKey,
-		SecretKey: uinfo.SecretKey,
+		SecretKey: ureq.SecretKey,
 		Status: func() string {
-			if uinfo.Status == madmin.AccountEnabled {
+			if ureq.Status == madmin.AccountEnabled {
 				return auth.AccountOn
 			}
 			return auth.AccountOff
@@ -26,23 +26,5 @@

 	cache.iamUsersMap[accessKey] = u.Credentials

-	// Set policy if specified.
-	if uinfo.PolicyName != "" {
-		policy := uinfo.PolicyName
-		// Handle policy mapping set/update
-		mp := newMappedPolicy(policy)
-		for _, p := range mp.toSlice() {
-			if _, found := cache.iamPolicyDocsMap[policy]; !found {
-				logger.LogIf(GlobalContext, fmt.Errorf("%w: (%s)", errNoSuchPolicy, p))
-				return errNoSuchPolicy
-			}
-		}
-
-		if err := store.saveMappedPolicy(ctx, accessKey, regUser, false, mp); err != nil {
-			return err
-		}
-		cache.iamUserPolicyMap[accessKey] = mp
-	}
 	return nil
-
 }

--- a/cmd/iam.go
+++ b/cmd/iam.go
@@ -1,4 +1,4 @@
-func (sys *IAMSys) CreateUser(ctx context.Context, accessKey string, uinfo madmin.UserInfo) error {
+func (sys *IAMSys) CreateUser(ctx context.Context, accessKey string, ureq madmin.AddOrUpdateUserReq) error {
 	if !sys.Initialized() {
 		return errServerNotInitialized
 	}
@@ -11,11 +11,11 @@
 		return auth.ErrInvalidAccessKeyLength
 	}

-	if !auth.IsSecretKeyValid(uinfo.SecretKey) {
+	if !auth.IsSecretKeyValid(ureq.SecretKey) {
 		return auth.ErrInvalidSecretKeyLength
 	}

-	err := sys.store.AddUser(ctx, accessKey, uinfo)
+	err := sys.store.AddUser(ctx, accessKey, ureq)
 	if err != nil {
 		return err
 	}


A directory traversal issue was found in reposync, a part of yum-utils, where reposync fails to sanitize paths in remote repository configuration files. If an attacker controls a repository, they may be able to copy files outside of the destination directory on the targeted system via path traversal. If reposync is running with heightened privileges on a targeted system, this flaw could potentially result in system compromise via the overwriting of critical system files. Version 1.1.31 and older are believed to be affected.

Improper Link Resolution Before File Access ('Link Following')

The product attempts to access a file based on the filename, but it does not properly prevent that filename from identifying a link or shortcut that resolves to an unintended resource.

def parseArgs():
    usage = _("""
    Reposync is used to synchronize a remote yum repository to a local
    directory using yum to retrieve the packages.

    %s [options]
    """) % sys.argv[0]

    parser = OptionParser(usage=usage)
    parser.add_option("-c", "--config", default='/etc/yum.conf',
        help=_('config file to use (defaults to /etc/yum.conf)'))
    parser.add_option("-a", "--arch", default=None,
        help=_('act as if running the specified arch (default: current arch, note: does not override $releasever. x86_64 is a superset for i*86.)'))
    parser.add_option("--source", default=False, dest="source", action="store_true",
                      help=_('operate on source packages'))
    parser.add_option("-r", "--repoid", default=[], action='append',
        help=_("specify repo ids to query, can be specified multiple times (default is all enabled)"))
    parser.add_option("-e", "--cachedir",
        help=_("directory in which to store metadata"))
    parser.add_option("-t", "--tempcache", default=False, action="store_true",
        help=_("Use a temp dir for storing/accessing yum-cache"))
    parser.add_option("-d", "--delete", default=False, action="store_true",
        help=_("delete local packages no longer present in repository"))
    parser.add_option("-p", "--download_path", dest='destdir',
        default=os.getcwd(), help=_("Path to download packages to: defaults to current dir"))
    parser.add_option("--norepopath", dest='norepopath', default=False, action="store_true",
        help=_("Don't add the reponame to the download path. Can only be used when syncing a single repository (default is to add the reponame)"))
    parser.add_option("-g", "--gpgcheck", default=False, action="store_true",
        help=_("Remove packages that fail GPG signature checking after downloading"))
    parser.add_option("-u", "--urls", default=False, action="store_true",
        help=_("Just list urls of what would be downloaded, don't download"))
    parser.add_option("-n", "--newest-only", dest='newest', default=False, action="store_true",
        help=_("Download only newest packages per-repo"))
    parser.add_option("-q", "--quiet", default=False, action="store_true",
        help=_("Output as little as possible"))
    parser.add_option("-l", "--plugins", default=False, action="store_true",
        help=_("enable yum plugin support"))
    parser.add_option("-m", "--downloadcomps", default=False, action="store_true",
        help=_("also download comps.xml"))
    parser.add_option("", "--download-metadata", dest="downloadmd",
        default=False, action="store_true",
        help=_("download all the non-default metadata"))
    (opts, args) = parser.parse_args()
    return (opts, args)

def main():
    (opts, dummy) = parseArgs()

    if not os.path.exists(opts.destdir) and not opts.urls:
        try:
            os.makedirs(opts.destdir)
        except OSError, e:
            print >> sys.stderr, _("Error: Cannot create destination dir %s") % opts.destdir
            sys.exit(1)

    if not os.access(opts.destdir, os.W_OK) and not opts.urls:
        print >> sys.stderr, _("Error: Cannot write to  destination dir %s") % opts.destdir
        sys.exit(1)

    my = RepoSync(opts=opts)
    my.doConfigSetup(fn=opts.config, init_plugins=opts.plugins)

    # Force unprivileged users to have a private temporary cachedir
    # if they've not given an explicit cachedir
    if os.getuid() != 0 and not opts.cachedir:
        opts.tempcache = True

    if opts.tempcache:
        if not my.setCacheDir(force=True, reuse=False):
            print >> sys.stderr, _("Error: Could not make cachedir, exiting")
            sys.exit(50)
        my.conf.uid = 1 # force locking of user cache
    elif opts.cachedir:
        my.repos.setCacheDir(opts.cachedir)

    # Lock if they've not given an explicit cachedir
    if not opts.cachedir:
        try:
            my.doLock()
        except yum.Errors.LockError, e:
            print >> sys.stderr, _("Error: %s") % e
            sys.exit(50)

    #  Use progress bar display when downloading repo metadata
    # and package files ... needs to be setup before .repos (ie. RHN/etc.).
    if not opts.quiet:
        my.repos.setProgressBar(TextMeter(fo=sys.stdout), TextMultiFileMeter(fo=sys.stdout))
    my.doRepoSetup()

    if len(opts.repoid) > 0:
        myrepos = []

        # find the ones we want
        for glob in opts.repoid:
            add_repos = my.repos.findRepos(glob)
            if not add_repos:
                print >> sys.stderr, _("Warning: cannot find repository %s") % glob
                continue
            myrepos.extend(add_repos)

        if not myrepos:
            print >> sys.stderr, _("No repositories found")
            sys.exit(1)

        # disable them all
        for repo in my.repos.repos.values():
            repo.disable()

        # enable the ones we like
        for repo in myrepos:
            repo.enable()

    # --norepopath can only be sensibly used with a single repository:
    if len(my.repos.listEnabled()) > 1 and opts.norepopath:
        print >> sys.stderr, _("Error: Can't use --norepopath with multiple repositories")
        sys.exit(1)

    try:
        arches = rpmUtils.arch.getArchList(opts.arch)
        if opts.source:
            arches += ['src']
        my.doSackSetup(arches)
    except yum.Errors.RepoError, e:
        print >> sys.stderr, _("Error setting up repositories: %s") % e
        # maybe this shouldn't be entirely fatal
        sys.exit(1)

    exit_code = 0
    for repo in my.repos.listEnabled():
        reposack = ListPackageSack(my.pkgSack.returnPackages(repoid=repo.id))

        if opts.newest:
            download_list = reposack.returnNewestByNameArch()
        else:
            download_list = list(reposack)

        if opts.norepopath:
            local_repo_path = opts.destdir
        else:
            local_repo_path = opts.destdir + '/' + repo.id

        if opts.delete and os.path.exists(local_repo_path):
            current_pkgs = localpkgs(local_repo_path)

            download_set = {}
            for pkg in download_list:
                rpmname = os.path.basename(pkg.remote_path)
                download_set[rpmname] = 1

            for pkg in current_pkgs:
                if pkg in download_set:
                    continue

                if not opts.quiet:
                    my.logger.info("Removing obsolete %s", pkg)
                os.unlink(current_pkgs[pkg]['path'])

        if opts.downloadcomps or opts.downloadmd:

            if not os.path.exists(local_repo_path):
                try:
                    os.makedirs(local_repo_path)
                except IOError, e:
                    my.logger.error("Could not make repo subdir: %s" % e)
                    my.closeRpmDB()
                    sys.exit(1)

            if opts.downloadcomps:
                wanted_types = ['group']

            if opts.downloadmd:
                wanted_types = repo.repoXML.fileTypes()

            for ftype in repo.repoXML.fileTypes():
                if ftype in ['primary', 'primary_db', 'filelists',
                             'filelists_db', 'other', 'other_db']:
                    continue
                if ftype not in wanted_types:
                    continue

                try:
                    resultfile = repo.retrieveMD(ftype)
                    basename = os.path.basename(resultfile)
                    if ftype == 'group' and opts.downloadcomps: # for compat with how --downloadcomps saved the comps file always as comps.xml
                        basename = 'comps.xml'
                    shutil.copyfile(resultfile, "%s/%s" % (local_repo_path, basename))
                except yum.Errors.RepoMDError, e:
                    if not opts.quiet:
                        my.logger.error("Unable to fetch metadata: %s" % e)

        remote_size = 0
        if not opts.urls:
            for pkg in download_list:
                local = os.path.join(local_repo_path, pkg.remote_path)
                sz = int(pkg.returnSimple('packagesize'))
                if os.path.exists(local) and os.path.getsize(local) == sz:
                    continue
                remote_size += sz

        if hasattr(urlgrabber.progress, 'text_meter_total_size'):
            urlgrabber.progress.text_meter_total_size(remote_size)

        download_list.sort(key=lambda pkg: pkg.name)
        if opts.urls:
            for pkg in download_list:
                local = os.path.join(local_repo_path, pkg.remote_path)
                if not (os.path.exists(local) and my.verifyPkg(local, pkg, False)):
                    print urljoin(pkg.repo.urls[0], pkg.remote_path)
            continue

        # create dest dir
        if not os.path.exists(local_repo_path):
            os.makedirs(local_repo_path)

        # set localpaths
        for pkg in download_list:
            pkg.localpath = os.path.join(local_repo_path, pkg.remote_path)
            pkg.repo.copy_local = True
            pkg.repo.cache = 0
            localdir = os.path.dirname(pkg.localpath)
            if not os.path.exists(localdir):
                os.makedirs(localdir)

        # use downloader from YumBase
        probs = my.downloadPkgs(download_list)
        if probs:
            exit_code = 1
            for key in probs:
                for error in probs[key]:
                    my.logger.error('%s: %s', key, error)

        if opts.gpgcheck:
            for pkg in download_list:
                result, error = my.sigCheckPkg(pkg)
                if result != 0:
                    rpmfn = os.path.basename(pkg.remote_path)
                    if result == 1:
                        my.logger.warning('Removing %s, due to missing GPG key.' % rpmfn)
                    elif result == 2:
                        my.logger.warning('Removing %s due to failed signature check.' % rpmfn)
                    else:
                        my.logger.warning('Removing %s due to failed signature check: %s' % rpmfn)
                    os.unlink(pkg.localpath)
                    exit_code = 1
                    continue

    my.closeRpmDB()
    sys.exit(exit_code)

def parseArgs():
    usage = _("""
    Reposync is used to synchronize a remote yum repository to a local
    directory using yum to retrieve the packages.

    %s [options]
    """) % sys.argv[0]

    parser = OptionParser(usage=usage)
    parser.add_option("-c", "--config", default='/etc/yum.conf',
        help=_('config file to use (defaults to /etc/yum.conf)'))
    parser.add_option("-a", "--arch", default=None,
        help=_('act as if running the specified arch (default: current arch, note: does not override $releasever. x86_64 is a superset for i*86.)'))
    parser.add_option("--source", default=False, dest="source", action="store_true",
                      help=_('operate on source packages'))
    parser.add_option("-r", "--repoid", default=[], action='append',
        help=_("specify repo ids to query, can be specified multiple times (default is all enabled)"))
    parser.add_option("-e", "--cachedir",
        help=_("directory in which to store metadata"))
    parser.add_option("-t", "--tempcache", default=False, action="store_true",
        help=_("Use a temp dir for storing/accessing yum-cache"))
    parser.add_option("-d", "--delete", default=False, action="store_true",
        help=_("delete local packages no longer present in repository"))
    parser.add_option("-p", "--download_path", dest='destdir',
        default=os.getcwd(), help=_("Path to download packages to: defaults to current dir"))
    parser.add_option("--norepopath", dest='norepopath', default=False, action="store_true",
        help=_("Don't add the reponame to the download path. Can only be used when syncing a single repository (default is to add the reponame)"))
    parser.add_option("-g", "--gpgcheck", default=False, action="store_true",
        help=_("Remove packages that fail GPG signature checking after downloading"))
    parser.add_option("-u", "--urls", default=False, action="store_true",
        help=_("Just list urls of what would be downloaded, don't download"))
    parser.add_option("-n", "--newest-only", dest='newest', default=False, action="store_true",
        help=_("Download only newest packages per-repo"))
    parser.add_option("-q", "--quiet", default=False, action="store_true",
        help=_("Output as little as possible"))
    parser.add_option("-l", "--plugins", default=False, action="store_true",
        help=_("enable yum plugin support"))
    parser.add_option("-m", "--downloadcomps", default=False, action="store_true",
        help=_("also download comps.xml"))
    parser.add_option("", "--download-metadata", dest="downloadmd",
        default=False, action="store_true",
        help=_("download all the non-default metadata"))
    parser.add_option("", "--allow-path-traversal", default=False,
        action="store_true",
        help=_("Allow packages stored outside their repo directory to be synced "
               "(UNSAFE, USE WITH CAUTION!)"))
    (opts, args) = parser.parse_args()
    return (opts, args)

def main():
    (opts, dummy) = parseArgs()

    if not os.path.exists(opts.destdir) and not opts.urls:
        try:
            os.makedirs(opts.destdir)
        except OSError, e:
            print >> sys.stderr, _("Error: Cannot create destination dir %s") % opts.destdir
            sys.exit(1)

    if not os.access(opts.destdir, os.W_OK) and not opts.urls:
        print >> sys.stderr, _("Error: Cannot write to  destination dir %s") % opts.destdir
        sys.exit(1)

    my = RepoSync(opts=opts)
    my.doConfigSetup(fn=opts.config, init_plugins=opts.plugins)

    # Force unprivileged users to have a private temporary cachedir
    # if they've not given an explicit cachedir
    if os.getuid() != 0 and not opts.cachedir:
        opts.tempcache = True

    if opts.tempcache:
        if not my.setCacheDir(force=True, reuse=False):
            print >> sys.stderr, _("Error: Could not make cachedir, exiting")
            sys.exit(50)
        my.conf.uid = 1 # force locking of user cache
    elif opts.cachedir:
        my.repos.setCacheDir(opts.cachedir)

    # Lock if they've not given an explicit cachedir
    if not opts.cachedir:
        try:
            my.doLock()
        except yum.Errors.LockError, e:
            print >> sys.stderr, _("Error: %s") % e
            sys.exit(50)

    #  Use progress bar display when downloading repo metadata
    # and package files ... needs to be setup before .repos (ie. RHN/etc.).
    if not opts.quiet:
        my.repos.setProgressBar(TextMeter(fo=sys.stdout), TextMultiFileMeter(fo=sys.stdout))
    my.doRepoSetup()

    if len(opts.repoid) > 0:
        myrepos = []

        # find the ones we want
        for glob in opts.repoid:
            add_repos = my.repos.findRepos(glob)
            if not add_repos:
                print >> sys.stderr, _("Warning: cannot find repository %s") % glob
                continue
            myrepos.extend(add_repos)

        if not myrepos:
            print >> sys.stderr, _("No repositories found")
            sys.exit(1)

        # disable them all
        for repo in my.repos.repos.values():
            repo.disable()

        # enable the ones we like
        for repo in myrepos:
            repo.enable()

    # --norepopath can only be sensibly used with a single repository:
    if len(my.repos.listEnabled()) > 1 and opts.norepopath:
        print >> sys.stderr, _("Error: Can't use --norepopath with multiple repositories")
        sys.exit(1)

    try:
        arches = rpmUtils.arch.getArchList(opts.arch)
        if opts.source:
            arches += ['src']
        my.doSackSetup(arches)
    except yum.Errors.RepoError, e:
        print >> sys.stderr, _("Error setting up repositories: %s") % e
        # maybe this shouldn't be entirely fatal
        sys.exit(1)

    exit_code = 0
    for repo in my.repos.listEnabled():
        reposack = ListPackageSack(my.pkgSack.returnPackages(repoid=repo.id))

        if opts.newest:
            download_list = reposack.returnNewestByNameArch()
        else:
            download_list = list(reposack)

        if opts.norepopath:
            local_repo_path = opts.destdir
        else:
            local_repo_path = opts.destdir + '/' + repo.id

        # Ensure we don't traverse out of local_repo_path by dropping any
        # packages whose remote_path is absolute or contains up-level
        # references (unless explicitly allowed).
        # See RHBZ#1600221 for details.
        if not opts.allow_path_traversal:
            newlist = []
            skipped = False
            for pkg in download_list:
                if is_subpath(pkg.remote_path, local_repo_path):
                    newlist.append(pkg)
                    continue
                my.logger.warning(
                    _('WARNING: skipping package %s: remote path "%s" not '
                      'within repodir, unsafe to mirror locally')
                    % (pkg, pkg.remote_path)
                )
                skipped = True
            if skipped:
                my.logger.info(
                    _('You can enable unsafe remote paths by using '
                      '--allow-path-traversal (see reposync(1) for details)')
                )
            download_list = newlist

        if opts.delete and os.path.exists(local_repo_path):
            current_pkgs = localpkgs(local_repo_path)

            download_set = {}
            for pkg in download_list:
                rpmname = os.path.basename(pkg.remote_path)
                download_set[rpmname] = 1

            for pkg in current_pkgs:
                if pkg in download_set:
                    continue

                if not opts.quiet:
                    my.logger.info("Removing obsolete %s", pkg)
                os.unlink(current_pkgs[pkg]['path'])

        if opts.downloadcomps or opts.downloadmd:

            if not os.path.exists(local_repo_path):
                try:
                    os.makedirs(local_repo_path)
                except IOError, e:
                    my.logger.error("Could not make repo subdir: %s" % e)
                    my.closeRpmDB()
                    sys.exit(1)

            if opts.downloadcomps:
                wanted_types = ['group']

            if opts.downloadmd:
                wanted_types = repo.repoXML.fileTypes()

            for ftype in repo.repoXML.fileTypes():
                if ftype in ['primary', 'primary_db', 'filelists',
                             'filelists_db', 'other', 'other_db']:
                    continue
                if ftype not in wanted_types:
                    continue

                try:
                    resultfile = repo.retrieveMD(ftype)
                    basename = os.path.basename(resultfile)
                    if ftype == 'group' and opts.downloadcomps: # for compat with how --downloadcomps saved the comps file always as comps.xml
                        basename = 'comps.xml'
                    shutil.copyfile(resultfile, "%s/%s" % (local_repo_path, basename))
                except yum.Errors.RepoMDError, e:
                    if not opts.quiet:
                        my.logger.error("Unable to fetch metadata: %s" % e)

        remote_size = 0
        if not opts.urls:
            for pkg in download_list:
                local = os.path.join(local_repo_path, pkg.remote_path)
                sz = int(pkg.returnSimple('packagesize'))
                if os.path.exists(local) and os.path.getsize(local) == sz:
                    continue
                remote_size += sz

        if hasattr(urlgrabber.progress, 'text_meter_total_size'):
            urlgrabber.progress.text_meter_total_size(remote_size)

        download_list.sort(key=lambda pkg: pkg.name)
        if opts.urls:
            for pkg in download_list:
                local = os.path.join(local_repo_path, pkg.remote_path)
                if not (os.path.exists(local) and my.verifyPkg(local, pkg, False)):
                    print urljoin(pkg.repo.urls[0], pkg.remote_path)
            continue

        # create dest dir
        if not os.path.exists(local_repo_path):
            os.makedirs(local_repo_path)

        # set localpaths
        for pkg in download_list:
            pkg.localpath = os.path.join(local_repo_path, pkg.remote_path)
            pkg.repo.copy_local = True
            pkg.repo.cache = 0
            localdir = os.path.dirname(pkg.localpath)
            if not os.path.exists(localdir):
                os.makedirs(localdir)

        # use downloader from YumBase
        probs = my.downloadPkgs(download_list)
        if probs:
            exit_code = 1
            for key in probs:
                for error in probs[key]:
                    my.logger.error('%s: %s', key, error)

        if opts.gpgcheck:
            for pkg in download_list:
                result, error = my.sigCheckPkg(pkg)
                if result != 0:
                    rpmfn = os.path.basename(pkg.remote_path)
                    if result == 1:
                        my.logger.warning('Removing %s, due to missing GPG key.' % rpmfn)
                    elif result == 2:
                        my.logger.warning('Removing %s due to failed signature check.' % rpmfn)
                    else:
                        my.logger.warning('Removing %s due to failed signature check: %s' % rpmfn)
                    os.unlink(pkg.localpath)
                    exit_code = 1
                    continue

    my.closeRpmDB()
    sys.exit(exit_code)

def is_subpath(path, root):
    root = os.path.realpath(root)
    path = os.path.realpath(os.path.join(root, path))
    # join() is used below to ensure root ends with a slash
    return path.startswith(os.path.join(root, ''))

--- a/reposync.py
+++ b/reposync.py
@@ -40,5 +40,9 @@
     parser.add_option("", "--download-metadata", dest="downloadmd",
         default=False, action="store_true",
         help=_("download all the non-default metadata"))
+    parser.add_option("", "--allow-path-traversal", default=False,
+        action="store_true",
+        help=_("Allow packages stored outside their repo directory to be synced "
+               "(UNSAFE, USE WITH CAUTION!)"))
     (opts, args) = parser.parse_args()
     return (opts, args)

--- a/reposync.py
+++ b/reposync.py
@@ -93,6 +93,30 @@
             local_repo_path = opts.destdir
         else:
             local_repo_path = opts.destdir + '/' + repo.id
+
+        # Ensure we don't traverse out of local_repo_path by dropping any
+        # packages whose remote_path is absolute or contains up-level
+        # references (unless explicitly allowed).
+        # See RHBZ#1600221 for details.
+        if not opts.allow_path_traversal:
+            newlist = []
+            skipped = False
+            for pkg in download_list:
+                if is_subpath(pkg.remote_path, local_repo_path):
+                    newlist.append(pkg)
+                    continue
+                my.logger.warning(
+                    _('WARNING: skipping package %s: remote path "%s" not '
+                      'within repodir, unsafe to mirror locally')
+                    % (pkg, pkg.remote_path)
+                )
+                skipped = True
+            if skipped:
+                my.logger.info(
+                    _('You can enable unsafe remote paths by using '
+                      '--allow-path-traversal (see reposync(1) for details)')
+                )
+            download_list = newlist

         if opts.delete and os.path.exists(local_repo_path):
             current_pkgs = localpkgs(local_repo_path)

--- /dev/null
+++ b/reposync.py
@@ -0,0 +1,5 @@
+def is_subpath(path, root):
+    root = os.path.realpath(root)
+    path = os.path.realpath(os.path.join(root, path))
+    # join() is used below to ensure root ends with a slash
+    return path.startswith(os.path.join(root, ''))


SAP BTP Security Services Integration Library ([Python] sap-xssec) - versions < 4.1.0, allow under certain conditions an escalation of privileges. On successful exploitation, an unauthenticated attacker can obtain arbitrary permissions within the application.

Improper Authorization

The product does not perform or incorrectly performs an authorization check when an actor attempts to access a resource or perform an action.

Execution with Unnecessary Privileges

The product performs an operation at a privilege level that is higher than the minimum level required, which creates new weaknesses or amplifies the consequences of other weaknesses.

Improper Privilege Management

The product does not properly assign, modify, track, or check privileges for an actor, creating an unintended sphere of control for that actor.

    def validate_audience(self):
        """
        check `aud` in jwt token
        """
        validation_result = self.audience_validator.validate_token(audiences_from_token=self.token_payload["aud"])
        if validation_result is False:
            raise RuntimeError('Audience Validation Failed')
        return self

    def validate_audience(self):
        """
        check `aud` in jwt token
        """

        # Make sure `aud` is a list
        aud = [self.token_payload["aud"]] if isinstance(self.token_payload["aud"], str) else self.token_payload["aud"]

        validation_result = self.audience_validator.validate_token(audiences_from_token=aud)
        if validation_result is False:
            raise RuntimeError('Audience Validation Failed')
        return self

--- a/sap/xssec/security_context_ias.py
+++ b/sap/xssec/security_context_ias.py
@@ -2,7 +2,11 @@
         """
         check `aud` in jwt token
         """
-        validation_result = self.audience_validator.validate_token(audiences_from_token=self.token_payload["aud"])
+
+        # Make sure `aud` is a list
+        aud = [self.token_payload["aud"]] if isinstance(self.token_payload["aud"], str) else self.token_payload["aud"]
+
+        validation_result = self.audience_validator.validate_token(audiences_from_token=aud)
         if validation_result is False:
             raise RuntimeError('Audience Validation Failed')
         return self


Spotipy is a lightweight Python library for the Spotify Web API. The `CacheHandler` class creates a cache file to store the auth token. Prior to version 2.25.1, the file created has `rw-r--r--` (644) permissions by default, when it could be locked down to `rw-------` (600) permissions. This leads to overly broad exposure of the spotify auth token. If this token can be read by an attacker (another user on the machine, or a process running as another user), it can be used to perform administrative actions on the Spotify account, depending on the scope granted to the token. Version 2.25.1 tightens the cache file permissions.

Incorrect Default Permissions

During installation, installed file permissions are set to allow anyone to modify those files.

    def save_token_to_cache(self, token_info):
        try:
            with open(self.cache_path, "w", encoding='utf-8') as f:
                f.write(json.dumps(token_info, cls=self.encoder_cls))
        except OSError:
            logger.warning(f"Couldn't write token to cache at: {self.cache_path}")

    def save_token_to_cache(self, token_info):
        try:
            with open(self.cache_path, "w", encoding='utf-8') as f:
                f.write(json.dumps(token_info, cls=self.encoder_cls))
            # https://github.com/spotipy-dev/spotipy/security/advisories/GHSA-pwhh-q4h6-w599
            os.chmod(self.cache_path, 0o600)
        except OSError:
            logger.warning(f"Couldn't write token to cache at: {self.cache_path}")
        except FileNotFoundError:
            logger.warning(f"Couldn't set permissions to cache file at: {self.cache_path}")

--- a/spotipy/cache_handler.py
+++ b/spotipy/cache_handler.py
@@ -2,5 +2,9 @@
         try:
             with open(self.cache_path, "w", encoding='utf-8') as f:
                 f.write(json.dumps(token_info, cls=self.encoder_cls))
+            # https://github.com/spotipy-dev/spotipy/security/advisories/GHSA-pwhh-q4h6-w599
+            os.chmod(self.cache_path, 0o600)
         except OSError:
             logger.warning(f"Couldn't write token to cache at: {self.cache_path}")
+        except FileNotFoundError:
+            logger.warning(f"Couldn't set permissions to cache file at: {self.cache_path}")

#!/bin/bash
# From ghcr.io/anonymous2578-data/cve-2025-27154:latest
# bash /workspace/fix-run.sh
set -e

cd /workspace/spotipy
git apply --whitespace=nowarn /workspace/test.patch /workspace/fix.patch
/workspace/PoC_env/CVE-2025-27154/bin/python  hand_test.py

#!/bin/bash
# From ghcr.io/anonymous2578-data/cve-2025-27154:latest
# bash /workspace/unit_test.sh
set -e

cd /workspace/spotipy
git apply --whitespace=nowarn /workspace/fix.patch
/workspace/PoC_env/CVE-2025-27154/bin/python -m pytest -v tests/ -k "not test_spotify_client_credentials_get_access_token"


A vulnerability in which attackers could forge HTTP requests to manipulate the `charm` data directory to access or delete anything on the server. This has been patched and is available in release [v0.12.1](https://github.com/charmbracelet/charm/releases/tag/v0.12.1). We recommend that all users running self-hosted `charm` instances update immediately. This vulnerability was found in-house and we haven't been notified of any potential exploiters. ### Additional notes * Encrypted user data uploaded to the Charm server is safe as Charm servers cannot decrypt user data. This includes filenames, paths, and all key-value data. * Users running the official Charm [Docker images](https://github.com/charmbracelet/charm/blob/main/docker.md) are at minimal risk because the exploit is limited to the containerized filesystem.

Server-Side Request Forgery (SSRF)

The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination.

func (s *HTTPServer) handlePostFile(w http.ResponseWriter, r *http.Request) {
	u := s.charmUserFromRequest(w, r)
	path := pattern.Path(r.Context())
	ms := r.URL.Query().Get("mode")
	m, err := strconv.ParseUint(ms, 10, 32)
	if err != nil {
		log.Printf("file mode not a number: %s", err)
		s.renderError(w)
		return
	}
	f, fh, err := r.FormFile("data")
	if err != nil {
		log.Printf("cannot parse form data: %s", err)
		s.renderError(w)
		return
	}
	defer f.Close() // nolint:errcheck
	if s.cfg.UserMaxStorage > 0 {
		stat, err := s.cfg.FileStore.Stat(u.CharmID, "")
		if err != nil {
			log.Printf("cannot stat user storage: %s", err)
			s.renderError(w)
			return
		}
		if stat.Size()+fh.Size > s.cfg.UserMaxStorage {
			s.renderCustomError(w, "user storage limit exceeded", http.StatusForbidden)
			return
		}
	}
	err = s.cfg.FileStore.Put(u.CharmID, path, f, fs.FileMode(m))
	if err != nil {
		log.Printf("cannot post file: %s", err)
		s.renderError(w)
		return
	}
}

func (s *HTTPServer) handleGetFile(w http.ResponseWriter, r *http.Request) {
	u := s.charmUserFromRequest(w, r)
	path := pattern.Path(r.Context())
	f, err := s.cfg.FileStore.Get(u.CharmID, path)
	if errors.Is(err, fs.ErrNotExist) {
		s.renderCustomError(w, "file not found", http.StatusNotFound)
		return
	}
	if err != nil {
		log.Printf("cannot get file: %s", err)
		s.renderError(w)
		return
	}
	defer f.Close() // nolint:errcheck
	fi, err := f.Stat()
	if err != nil {
		log.Printf("cannot get file info: %s", err)
		s.renderError(w)
		return
	}

	switch f.(type) {
	case *charmfs.DirFile:
		w.Header().Set("Content-Type", "application/json")
	default:
		w.Header().Set("Content-Type", "application/octet-stream")
		w.Header().Set("Last-Modified", fi.ModTime().Format(http.TimeFormat))
	}
	w.Header().Set("X-File-Mode", fmt.Sprintf("%d", fi.Mode()))
	_, err = io.Copy(w, f)
	if err != nil {
		log.Printf("cannot copy file: %s", err)
		s.renderError(w)
		return
	}
}

func (s *HTTPServer) handleDeleteFile(w http.ResponseWriter, r *http.Request) {
	u := s.charmUserFromRequest(w, r)
	path := pattern.Path(r.Context())
	err := s.cfg.FileStore.Delete(u.CharmID, path)
	if err != nil {
		log.Printf("cannot delete file: %s", err)
		s.renderError(w)
		return
	}
}

func (s *HTTPServer) handlePostFile(w http.ResponseWriter, r *http.Request) {
	u := s.charmUserFromRequest(w, r)
	path := filepath.Clean(pattern.Path(r.Context()))
	ms := r.URL.Query().Get("mode")
	m, err := strconv.ParseUint(ms, 10, 32)
	if err != nil {
		log.Printf("file mode not a number: %s", err)
		s.renderError(w)
		return
	}
	f, fh, err := r.FormFile("data")
	if err != nil {
		log.Printf("cannot parse form data: %s", err)
		s.renderError(w)
		return
	}
	defer f.Close() // nolint:errcheck
	if s.cfg.UserMaxStorage > 0 {
		stat, err := s.cfg.FileStore.Stat(u.CharmID, "")
		if err != nil {
			log.Printf("cannot stat user storage: %s", err)
			s.renderError(w)
			return
		}
		if stat.Size()+fh.Size > s.cfg.UserMaxStorage {
			s.renderCustomError(w, "user storage limit exceeded", http.StatusForbidden)
			return
		}
	}
	err = s.cfg.FileStore.Put(u.CharmID, path, f, fs.FileMode(m))
	if err != nil {
		log.Printf("cannot post file: %s", err)
		s.renderError(w)
		return
	}
}

func (s *HTTPServer) handleGetFile(w http.ResponseWriter, r *http.Request) {
	u := s.charmUserFromRequest(w, r)
	path := filepath.Clean(pattern.Path(r.Context()))
	f, err := s.cfg.FileStore.Get(u.CharmID, path)
	if errors.Is(err, fs.ErrNotExist) {
		s.renderCustomError(w, "file not found", http.StatusNotFound)
		return
	}
	if err != nil {
		log.Printf("cannot get file: %s", err)
		s.renderError(w)
		return
	}
	defer f.Close() // nolint:errcheck
	fi, err := f.Stat()
	if err != nil {
		log.Printf("cannot get file info: %s", err)
		s.renderError(w)
		return
	}

	switch f.(type) {
	case *charmfs.DirFile:
		w.Header().Set("Content-Type", "application/json")
	default:
		w.Header().Set("Content-Type", "application/octet-stream")
		w.Header().Set("Last-Modified", fi.ModTime().Format(http.TimeFormat))
	}
	w.Header().Set("X-File-Mode", fmt.Sprintf("%d", fi.Mode()))
	_, err = io.Copy(w, f)
	if err != nil {
		log.Printf("cannot copy file: %s", err)
		s.renderError(w)
		return
	}
}

func (s *HTTPServer) handleDeleteFile(w http.ResponseWriter, r *http.Request) {
	u := s.charmUserFromRequest(w, r)
	path := filepath.Clean(pattern.Path(r.Context()))
	err := s.cfg.FileStore.Delete(u.CharmID, path)
	if err != nil {
		log.Printf("cannot delete file: %s", err)
		s.renderError(w)
		return
	}
}

--- a/server/http.go
+++ b/server/http.go
@@ -1,6 +1,6 @@
 func (s *HTTPServer) handlePostFile(w http.ResponseWriter, r *http.Request) {
 	u := s.charmUserFromRequest(w, r)
-	path := pattern.Path(r.Context())
+	path := filepath.Clean(pattern.Path(r.Context()))
 	ms := r.URL.Query().Get("mode")
 	m, err := strconv.ParseUint(ms, 10, 32)
 	if err != nil {

--- a/server/http.go
+++ b/server/http.go
@@ -1,6 +1,6 @@
 func (s *HTTPServer) handleGetFile(w http.ResponseWriter, r *http.Request) {
 	u := s.charmUserFromRequest(w, r)
-	path := pattern.Path(r.Context())
+	path := filepath.Clean(pattern.Path(r.Context()))
 	f, err := s.cfg.FileStore.Get(u.CharmID, path)
 	if errors.Is(err, fs.ErrNotExist) {
 		s.renderCustomError(w, "file not found", http.StatusNotFound)

--- a/server/http.go
+++ b/server/http.go
@@ -1,6 +1,6 @@
 func (s *HTTPServer) handleDeleteFile(w http.ResponseWriter, r *http.Request) {
 	u := s.charmUserFromRequest(w, r)
-	path := pattern.Path(r.Context())
+	path := filepath.Clean(pattern.Path(r.Context()))
 	err := s.cfg.FileStore.Delete(u.CharmID, path)
 	if err != nil {
 		log.Printf("cannot delete file: %s", err)


OS Command Injection in GitHub repository mlflow/mlflow prior to 2.6.0.

Improper Control of Generation of Code ('Code Injection')

The product constructs all or part of a code segment using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the syntax or behavior of the intended code segment.

Improper Neutralization of Special Elements used in a Command ('Command Injection')

The product constructs all or part of a command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended command when it is sent to a downstream component.

Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')

The product constructs all or part of an OS command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended OS command when it is sent to a downstream component.

    def predict(self, model_uri, input_path, output_path, content_type):
        """
        Generate predictions using generic python model saved with MLflow. The expected format of
        the input JSON is the Mlflow scoring format.
        Return the prediction results as a JSON.
        """
        local_path = _download_artifact_from_uri(model_uri)
        # NB: Absolute windows paths do not work with mlflow apis, use file uri to ensure
        # platform compatibility.
        local_uri = path_to_local_file_uri(local_path)

        if self._env_manager != _EnvManager.LOCAL:
            command = (
                'python -c "from mlflow.pyfunc.scoring_server import _predict; _predict('
                "model_uri={model_uri}, "
                "input_path={input_path}, "
                "output_path={output_path}, "
                "content_type={content_type})"
                '"'
            ).format(
                model_uri=repr(local_uri),
                input_path=repr(input_path),
                output_path=repr(output_path),
                content_type=repr(content_type),
            )
            return self.prepare_env(local_path).execute(command)
        else:
            scoring_server._predict(local_uri, input_path, output_path, content_type)

            setup_pyenv_and_virtualenv=setup_pyenv_and_virtualenv,
            install_mlflow=install_mlflow,
            custom_setup_steps=custom_setup_steps,
            entrypoint=pyfunc_entrypoint,
        )
        _logger.debug("generated dockerfile text", extra={"dockerfile": dockerfile_text})

        with open(os.path.join(output_path, "Dockerfile"), "w") as dockerfile:
            dockerfile.write(dockerfile_text)

    def build_image(
        self, model_uri, image_name, install_mlflow=False, mlflow_home=None, enable_mlserver=False
    ):
        copy_model_into_container = self.copy_model_into_container_wrapper(
            model_uri, install_mlflow, enable_mlserver
        )
        pyfunc_entrypoint = _pyfunc_entrypoint(
            self._env_manager, model_uri, install_mlflow, enable_mlserver
        )
        _build_image(
            image_name=image_name,
            mlflow_home=mlflow_home,
            env_manager=self._env_manager,
            custom_setup_steps_hook=copy_model_into_container,
            entrypoint=pyfunc_entrypoint,
        )

    def copy_model_into_container_wrapper(self, model_uri, install_mlflow, enable_mlserver):
        def copy_model_into_container(dockerfile_context_dir):
            # This function have to be included in another,
            # since `_build_image` function in `docker_utils` accepts only
            # single-argument function like this
            model_cwd = os.path.join(dockerfile_context_dir, "model_dir")
            pathlib.Path(model_cwd).mkdir(parents=True, exist_ok=True)
            if model_uri:
                model_path = _download_artifact_from_uri(model_uri, output_path=model_cwd)
                return """
                    COPY {model_dir} /opt/ml/model

    def predict(self, model_uri, input_path, output_path, content_type):
        """
        Generate predictions using generic python model saved with MLflow. The expected format of
        the input JSON is the Mlflow scoring format.
        Return the prediction results as a JSON.
        """
        local_path = _download_artifact_from_uri(model_uri)
        # NB: Absolute windows paths do not work with mlflow apis, use file uri to ensure
        # platform compatibility.
        local_uri = path_to_local_file_uri(local_path)

        if self._env_manager != _EnvManager.LOCAL:
            predict_cmd = [
                "python",
                _mlflow_pyfunc_backend_predict.__file__,
                "--model-uri",
                str(local_uri),
                "--content-type",
                shlex.quote(str(content_type)),
            ]
            if input_path:
                predict_cmd += ["--input-path", shlex.quote(str(input_path))]
            if output_path:
                predict_cmd += ["--output-path", shlex.quote(str(output_path))]
            return self.prepare_env(local_path).execute(" ".join(predict_cmd))
        else:
            scoring_server._predict(local_uri, input_path, output_path, content_type)

            install_mlflow=install_mlflow,
            custom_setup_steps=custom_setup_steps,
            entrypoint=pyfunc_entrypoint,
        )
        _logger.debug("generated dockerfile text", extra={"dockerfile": dockerfile_text})

        with open(os.path.join(output_path, "Dockerfile"), "w") as dockerfile:
            dockerfile.write(dockerfile_text)

    def build_image(
        self, model_uri, image_name, install_mlflow=False, mlflow_home=None, enable_mlserver=False
    ):
        copy_model_into_container = self.copy_model_into_container_wrapper(
            model_uri, install_mlflow, enable_mlserver
        )
        pyfunc_entrypoint = _pyfunc_entrypoint(
            self._env_manager, model_uri, install_mlflow, enable_mlserver
        )
        _build_image(
            image_name=image_name,
            mlflow_home=mlflow_home,
            env_manager=self._env_manager,
            custom_setup_steps_hook=copy_model_into_container,
            entrypoint=pyfunc_entrypoint,
        )

    def copy_model_into_container_wrapper(self, model_uri, install_mlflow, enable_mlserver):
        def copy_model_into_container(dockerfile_context_dir):
            # This function have to be included in another,
            # since `_build_image` function in `docker_utils` accepts only
            # single-argument function like this
            model_cwd = os.path.join(dockerfile_context_dir, "model_dir")
            pathlib.Path(model_cwd).mkdir(parents=True, exist_ok=True)
            if model_uri:
                model_path = _download_artifact_from_uri(model_uri, output_path=model_cwd)
                return """
                    COPY {model_dir} /opt/ml/model
                    RUN python -c \
                    'from mlflow.models.container import _install_pyfunc_deps;\
                    _install_pyfunc_deps(\

--- a/mlflow/pyfunc/backend.py
+++ b/mlflow/pyfunc/backend.py
@@ -10,19 +10,18 @@
         local_uri = path_to_local_file_uri(local_path)

         if self._env_manager != _EnvManager.LOCAL:
-            command = (
-                'python -c "from mlflow.pyfunc.scoring_server import _predict; _predict('
-                "model_uri={model_uri}, "
-                "input_path={input_path}, "
-                "output_path={output_path}, "
-                "content_type={content_type})"
-                '"'
-            ).format(
-                model_uri=repr(local_uri),
-                input_path=repr(input_path),
-                output_path=repr(output_path),
-                content_type=repr(content_type),
-            )
-            return self.prepare_env(local_path).execute(command)
+            predict_cmd = [
+                "python",
+                _mlflow_pyfunc_backend_predict.__file__,
+                "--model-uri",
+                str(local_uri),
+                "--content-type",
+                shlex.quote(str(content_type)),
+            ]
+            if input_path:
+                predict_cmd += ["--input-path", shlex.quote(str(input_path))]
+            if output_path:
+                predict_cmd += ["--output-path", shlex.quote(str(output_path))]
+            return self.prepare_env(local_path).execute(" ".join(predict_cmd))
         else:
             scoring_server._predict(local_uri, input_path, output_path, content_type)

--- a/mlflow/pyfunc/backend.py
+++ b/mlflow/pyfunc/backend.py
@@ -1,4 +1,3 @@
-            setup_pyenv_and_virtualenv=setup_pyenv_and_virtualenv,
             install_mlflow=install_mlflow,
             custom_setup_steps=custom_setup_steps,
             entrypoint=pyfunc_entrypoint,
@@ -36,3 +35,6 @@
                 model_path = _download_artifact_from_uri(model_uri, output_path=model_cwd)
                 return """
                     COPY {model_dir} /opt/ml/model
+                    RUN python -c \
+                    'from mlflow.models.container import _install_pyfunc_deps;\
+                    _install_pyfunc_deps(\


An exploitable vulnerability exists in the yaml loading functionality of ansible-vault before 1.0.5. A specially crafted vault can execute arbitrary python commands resulting in command execution. An attacker can insert python into the vault to trigger this vulnerability.

    def load(self, stream):
        '''read vault steam and return python object'''
        return yaml.load(self.vault.decrypt(stream))

    def load(self, stream):
        '''read vault steam and return python object'''
        return yaml.safe_load(self.vault.decrypt(stream))

--- a/ansible_vault/api.py
+++ b/ansible_vault/api.py
@@ -1,3 +1,3 @@
     def load(self, stream):
         '''read vault steam and return python object'''
-        return yaml.load(self.vault.decrypt(stream))
+        return yaml.safe_load(self.vault.decrypt(stream))


Apache Airflow 2.4.0, and versions before 2.9.3, has a vulnerability that allows authenticated DAG authors to craft a doc_md parameter in a way that could execute arbitrary code in the scheduler context, which should be forbidden according to the Airflow Security model. Users should upgrade to version 2.9.3 or later which has removed the vulnerability.

    def get_doc_md(self, doc_md: str | None) -> str | None:
        if doc_md is None:
            return doc_md

        env = self.get_template_env(force_sandboxed=True)

        if not doc_md.endswith(".md"):
            template = jinja2.Template(doc_md)
        else:
            try:
                template = env.get_template(doc_md)
            except jinja2.exceptions.TemplateNotFound:
                return f"""
                # Templating Error!
                Not able to find the template file: `{doc_md}`.
                """

        return template.render()

    def get_doc_md(self, doc_md: str | None) -> str | None:
        if doc_md is None:
            return doc_md

        if doc_md.endswith(".md"):
            try:
                return open(doc_md).read()
            except FileNotFoundError:
                return doc_md

        return doc_md

--- a/airflow/models/dag.py
+++ b/airflow/models/dag.py
@@ -2,17 +2,10 @@
         if doc_md is None:
             return doc_md

-        env = self.get_template_env(force_sandboxed=True)
+        if doc_md.endswith(".md"):
+            try:
+                return open(doc_md).read()
+            except FileNotFoundError:
+                return doc_md

-        if not doc_md.endswith(".md"):
-            template = jinja2.Template(doc_md)
-        else:
-            try:
-                template = env.get_template(doc_md)
-            except jinja2.exceptions.TemplateNotFound:
-                return f"""
-                # Templating Error!
-                Not able to find the template file: `{doc_md}`.
-                """
-
-        return template.render()
+        return doc_md

#!/bin/bash
# From ghcr.io/anonymous2578-data/cve-2024-39877:latest
# bash /workspace/fix-run.sh
set -e

cd /workspace/airflow
git apply --whitespace=nowarn /workspace/test.patch /workspace/fix.patch
cd tests && /workspace/PoC_env/CVE-2024-39877/bin/python -m pytest models/test_dag.py -v -k "test_resolve_documentation_template_file_not_rendered" -p no:warning

#!/bin/bash
# From ghcr.io/anonymous2578-data/cve-2024-39877:latest
# bash /workspace/unit_test.sh
set -e

cd /workspace/airflow
git apply --whitespace=nowarn /workspace/fix.patch
cd tests && /workspace/PoC_env/CVE-2024-39877/bin/python -m pytest models/test_dag.py -v -k "not test_resolve_documentation_template_file_rendered" -p no:warning


Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection') vulnerability in Apache Airflow Pinot Provider, Apache Airflow allows an attacker to control commands executed in the task execution context, without write access to DAG files. This issue affects Apache Airflow Pinot Provider versions prior to 4.0.0. It also impacts any Apache Airflow versions prior to 2.3.0 in case Apache Airflow Pinot Provider is installed (Apache Airflow Pinot Provider 4.0.0 can only be installed for Airflow 2.3.0+). Note that you need to manually install the Pinot Provider version 4.0.0 in order to get rid of the vulnerability on top of Airflow 2.3.0+ version.

    def __init__(
        self,
        conn_id: str = "pinot_admin_default",
        cmd_path: str = "pinot-admin.sh",
        pinot_admin_system_exit: bool = False,
    ) -> None:
        super().__init__()
        conn = self.get_connection(conn_id)
        self.host = conn.host
        self.port = str(conn.port)
        self.cmd_path = conn.extra_dejson.get("cmd_path", cmd_path)
        self.pinot_admin_system_exit = conn.extra_dejson.get(
            "pinot_admin_system_exit", pinot_admin_system_exit
        )
        self.conn = conn

    def __init__(
        self,
        conn_id: str = "pinot_admin_default",
        cmd_path: str = "pinot-admin.sh",
        pinot_admin_system_exit: bool = False,
    ) -> None:
        super().__init__()
        conn = self.get_connection(conn_id)
        self.host = conn.host
        self.port = str(conn.port)
        if cmd_path != "pinot-admin.sh":
            raise RuntimeError(
                "In version 4.0.0 of the PinotAdminHook the cmd_path has been hard-coded to"
                " pinot-admin.sh. In order to avoid accidental using of this parameter as"
                " positional `pinot_admin_system_exit` the `cmd_parameter`"
                " parameter is left here but you should not modify it. Make sure that "
                " `pinot-admin.sh` is on your PATH and do not change cmd_path value."
            )
        self.cmd_path = "pinot-admin.sh"
        self.pinot_admin_system_exit = conn.extra_dejson.get(
            "pinot_admin_system_exit", pinot_admin_system_exit
        )
        self.conn = conn

--- a/airflow/providers/apache/pinot/hooks/pinot.py
+++ b/airflow/providers/apache/pinot/hooks/pinot.py
@@ -8,7 +8,15 @@
         conn = self.get_connection(conn_id)
         self.host = conn.host
         self.port = str(conn.port)
-        self.cmd_path = conn.extra_dejson.get("cmd_path", cmd_path)
+        if cmd_path != "pinot-admin.sh":
+            raise RuntimeError(
+                "In version 4.0.0 of the PinotAdminHook the cmd_path has been hard-coded to"
+                " pinot-admin.sh. In order to avoid accidental using of this parameter as"
+                " positional `pinot_admin_system_exit` the `cmd_parameter`"
+                " parameter is left here but you should not modify it. Make sure that "
+                " `pinot-admin.sh` is on your PATH and do not change cmd_path value."
+            )
+        self.cmd_path = "pinot-admin.sh"
         self.pinot_admin_system_exit = conn.extra_dejson.get(
             "pinot_admin_system_exit", pinot_admin_system_exit
         )


The _Rsa15 class in the RSA 1.5 algorithm implementation in jwa.py in jwcrypto before 0.3.2 lacks the Random Filling protection mechanism, which makes it easier for remote attackers to obtain cleartext data via a Million Message Attack (MMA).

Exposure of Sensitive Information to an Unauthorized Actor

The product exposes sensitive information to an actor that is not explicitly authorized to have access to that information.

class _Rsa15(_RSA, JWAAlgorithm):

    name = 'RSA1_5'
    description = "RSAES-PKCS1-v1_5"
    keysize = 2048
    algorithm_usage_location = 'alg'
    algorithm_use = 'kex'

    def __init__(self):
        super(_Rsa15, self).__init__(padding.PKCS1v15())

    def unwrap(self, key, bitsize, ek, headers):
        self._check_key(key)
        # Address MMA attack by implementing RFC 3218 - 2.3.2. Random Filling
        # provides a random cek that will cause the decryption engine to
        # run to the end, but will fail decryption later.

        # always generate a random cek so we spend roughly the
        # same time as in the exception side of the branch
        cek = _randombits(bitsize)
        try:
            cek = super(_Rsa15, self).unwrap(key, bitsize, ek, headers)
            # always raise so we always run through the exception handling
            # code in all cases
            raise Exception('Dummy')
        except Exception:  # pylint: disable=broad-except
            return cek

--- a/jwcrypto/jwa.py
+++ b/jwcrypto/jwa.py
@@ -8,3 +8,20 @@

     def __init__(self):
         super(_Rsa15, self).__init__(padding.PKCS1v15())
+
+    def unwrap(self, key, bitsize, ek, headers):
+        self._check_key(key)
+        # Address MMA attack by implementing RFC 3218 - 2.3.2. Random Filling
+        # provides a random cek that will cause the decryption engine to
+        # run to the end, but will fail decryption later.
+
+        # always generate a random cek so we spend roughly the
+        # same time as in the exception side of the branch
+        cek = _randombits(bitsize)
+        try:
+            cek = super(_Rsa15, self).unwrap(key, bitsize, ek, headers)
+            # always raise so we always run through the exception handling
+            # code in all cases
+            raise Exception('Dummy')
+        except Exception:  # pylint: disable=broad-except
+            return cek


matrix-appservice-irc is a Node.js IRC bridge for Matrix. The matrix-appservice-irc bridge up to version 3.0.3 contains a vulnerability which can lead to arbitrary IRC command execution as the puppeted user. The attacker can only inject commands executed as their own IRC user. The vulnerability has been patched in matrix-appservice-irc version 3.0.4.

Improper Neutralization of Argument Delimiters in a Command ('Argument Injection')

The product constructs a string for a command to be executed by a separate component
in another control sphere, but it does not properly delimit the
intended arguments, options, or switches within that command string.

    private async setTopic(room: IrcRoom, topic: string): Promise<void> {
        if (this.state.status !== BridgedClientStatus.CONNECTED) {
            throw Error("unsafeClient not ready yet");
        }
        // join the room if we haven't already
        await this.joinChannel(room.channel);
        this.log.info("Setting topic to %s in channel %s", topic, room.channel);
        return this.state.client.send("TOPIC", room.channel, topic);
    }

    private async setTopic(room: IrcRoom, topic: string): Promise<void> {
        // replace newline "\n" with a pipe "|" to follow IRC conventions
        const sanitized_topic = topic.replace(/\n/g, " | ").replace(/\r/g, "")

        if (this.state.status !== BridgedClientStatus.CONNECTED) {
            throw Error("unsafeClient not ready yet");
        }
        // join the room if we haven't already
        await this.joinChannel(room.channel);

        this.log.info("Setting topic to %s in channel %s", sanitized_topic, room.channel);
        return this.state.client.send("TOPIC", room.channel, sanitized_topic);
    }

--- a/src/irc/BridgedClient.ts
+++ b/src/irc/BridgedClient.ts
@@ -1,9 +1,13 @@
     private async setTopic(room: IrcRoom, topic: string): Promise<void> {
+        // replace newline "\n" with a pipe "|" to follow IRC conventions
+        const sanitized_topic = topic.replace(/\n/g, " | ").replace(/\r/g, "")
+
         if (this.state.status !== BridgedClientStatus.CONNECTED) {
             throw Error("unsafeClient not ready yet");
         }
         // join the room if we haven't already
         await this.joinChannel(room.channel);
-        this.log.info("Setting topic to %s in channel %s", topic, room.channel);
-        return this.state.client.send("TOPIC", room.channel, topic);
+
+        this.log.info("Setting topic to %s in channel %s", sanitized_topic, room.channel);
+        return this.state.client.send("TOPIC", room.channel, sanitized_topic);
     }


authentik is an open-source identity provider. Prior to versions 2024.8.3 and 2024.6.5, access tokens issued to one application can be stolen by that application and used to impersonate the user against any other proxy provider. Also, a user can steal an access token they were legitimately issued for one application and use it to access another application that they aren't allowed to access. Anyone who has more than one proxy provider application with different trust domains or different access control is affected. Versions 2024.8.3 and 2024.6.5 fix the issue.

    def from_request(request: HttpRequest) -> "TokenIntrospectionParams":
        """Extract required Parameters from HTTP Request"""
        raw_token = request.POST.get("token")
        provider = authenticate_provider(request)
        if not provider:
            raise TokenIntrospectionError

        access_token = AccessToken.objects.filter(token=raw_token).first()
        if access_token:
            return TokenIntrospectionParams(access_token, provider)
        refresh_token = RefreshToken.objects.filter(token=raw_token).first()
        if refresh_token:
            return TokenIntrospectionParams(refresh_token, provider)
        LOGGER.debug("Token does not exist", token=raw_token)
        raise TokenIntrospectionError()

    def from_request(request: HttpRequest) -> "TokenIntrospectionParams":
        """Extract required Parameters from HTTP Request"""
        raw_token = request.POST.get("token")
        provider = authenticate_provider(request)
        if not provider:
            raise TokenIntrospectionError

        access_token = AccessToken.objects.filter(token=raw_token, provider=provider).first()
        if access_token:
            return TokenIntrospectionParams(access_token, provider)
        refresh_token = RefreshToken.objects.filter(token=raw_token, provider=provider).first()
        if refresh_token:
            return TokenIntrospectionParams(refresh_token, provider)
        LOGGER.debug("Token does not exist", token=raw_token)
        raise TokenIntrospectionError()

--- a/authentik/providers/oauth2/views/introspection.py
+++ b/authentik/providers/oauth2/views/introspection.py
@@ -5,10 +5,10 @@
         if not provider:
             raise TokenIntrospectionError

-        access_token = AccessToken.objects.filter(token=raw_token).first()
+        access_token = AccessToken.objects.filter(token=raw_token, provider=provider).first()
         if access_token:
             return TokenIntrospectionParams(access_token, provider)
-        refresh_token = RefreshToken.objects.filter(token=raw_token).first()
+        refresh_token = RefreshToken.objects.filter(token=raw_token, provider=provider).first()
         if refresh_token:
             return TokenIntrospectionParams(refresh_token, provider)
         LOGGER.debug("Token does not exist", token=raw_token)


Authorization headers are cleared on cross-origin redirect. However, cookie headers which are sensitive headers and are official headers found in the spec, remain uncleared. There are active users using cookie headers in undici. This may lead to accidental leakage of cookie to a 3rd-party site or a malicious attacker who can control the redirection target (ie. an open redirector) to leak the cookie to the 3rd party site. This was patched in v5.7.1. By default, this vulnerability is not exploitable. Do not enable redirections, i.e. `maxRedirections: 0` (the default).

URL Redirection to Untrusted Site ('Open Redirect')

The web application accepts a user-controlled input that specifies a link to an external site, and uses that link in a redirect.

function shouldRemoveHeader (header, removeContent, unknownOrigin) {
  return (
    (header.length === 4 && header.toString().toLowerCase() === 'host') ||
    (removeContent && header.toString().toLowerCase().indexOf('content-') === 0) ||
    (unknownOrigin && header.length === 13 && header.toString().toLowerCase() === 'authorization')
  )
}

function shouldRemoveHeader (header, removeContent, unknownOrigin) {
  return (
    (header.length === 4 && header.toString().toLowerCase() === 'host') ||
    (removeContent && header.toString().toLowerCase().indexOf('content-') === 0) ||
    (unknownOrigin && header.length === 13 && header.toString().toLowerCase() === 'authorization') ||
    (unknownOrigin && header.length === 6 && header.toString().toLowerCase() === 'cookie')
  )
}

--- a/lib/handler/redirect.js
+++ b/lib/handler/redirect.js
@@ -2,6 +2,7 @@
   return (
     (header.length === 4 && header.toString().toLowerCase() === 'host') ||
     (removeContent && header.toString().toLowerCase().indexOf('content-') === 0) ||
-    (unknownOrigin && header.length === 13 && header.toString().toLowerCase() === 'authorization')
+    (unknownOrigin && header.length === 13 && header.toString().toLowerCase() === 'authorization') ||
+    (unknownOrigin && header.length === 6 && header.toString().toLowerCase() === 'cookie')
   )
 }