VYPR
High severity7.7NVD Advisory· Published Apr 24, 2026· Updated Apr 27, 2026

CVE-2026-41068

CVE-2026-41068

Description

Kyverno is a policy engine designed for cloud native platform engineering teams. The patch for CVE-2026-22039 fixed cross-namespace privilege escalation in Kyverno's apiCall context by validating the URLPath field. However, the ConfigMap context loader has the identical vulnerability — the configMap.namespace field accepts any namespace with zero validation, allowing a namespace admin to read ConfigMaps from any namespace using Kyverno's privileged service account. This is a complete RBAC bypass in multi-tenant Kubernetes clusters. An updated fix is available in version 1.17.2.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/kyverno/kyvernoGo
<= 1.17.1

Affected products

1
  • cpe:2.3:a:kyverno:kyverno:*:-:*:*:*:*:*:*
    Range: <1.17.2

Patches

1
bbf3e5c01391

restrict configmap access for namespaced policies (#15850)

https://github.com/kyverno/kyvernoJim BugwadiaApr 15, 2026via ghsa
3 files changed · +182 17
  • pkg/engine/context/loaders/configmap.go+38 16 modified
    @@ -13,12 +13,13 @@ import (
     )
     
     type configMapLoader struct {
    -	ctx       context.Context //nolint:containedctx
    -	logger    logr.Logger
    -	entry     kyvernov1.ContextEntry
    -	resolver  engineapi.ConfigmapResolver
    -	enginectx enginecontext.Interface
    -	data      []byte
    +	ctx             context.Context //nolint:containedctx
    +	logger          logr.Logger
    +	entry           kyvernov1.ContextEntry
    +	resolver        engineapi.ConfigmapResolver
    +	enginectx       enginecontext.Interface
    +	data            []byte
    +	policyNamespace string
     }
     
     func NewConfigMapLoader(
    @@ -27,13 +28,15 @@ func NewConfigMapLoader(
     	entry kyvernov1.ContextEntry,
     	resolver engineapi.ConfigmapResolver,
     	enginectx enginecontext.Interface,
    +	policyNamespace string,
     ) enginecontext.Loader {
     	return &configMapLoader{
    -		ctx:       ctx,
    -		logger:    logger,
    -		entry:     entry,
    -		resolver:  resolver,
    -		enginectx: enginectx,
    +		ctx:             ctx,
    +		logger:          logger,
    +		entry:           entry,
    +		resolver:        resolver,
    +		enginectx:       enginectx,
    +		policyNamespace: policyNamespace,
     	}
     }
     
    @@ -73,23 +76,42 @@ func (cml *configMapLoader) fetchConfigMap() ([]byte, error) {
     	if err != nil {
     		return nil, fmt.Errorf("failed to substitute variables in context %s configMap.name %s: %v", entryName, cmName, err)
     	}
    +	nameStr, ok := name.(string)
    +	if !ok {
    +		return nil, fmt.Errorf("failed to substitute variables in context %s configMap.name %s: expected string, got %T", entryName, cmName, name)
    +	}
     	namespace, err := variables.SubstituteAll(logger, cml.enginectx, cml.entry.ConfigMap.Namespace)
     	if err != nil {
     		return nil, fmt.Errorf("failed to substitute variables in context %s configMap.namespace %s: %v", entryName, cmNamespace, err)
     	}
    -	if namespace == "" {
    -		namespace = "default"
    +	namespaceStr, ok := namespace.(string)
    +	if !ok {
    +		return nil, fmt.Errorf("failed to substitute variables in context %s configMap.namespace %s: expected string, got %T", entryName, cmNamespace, namespace)
    +	}
    +	if namespaceStr == "" {
    +		// For namespaced policies, default to the policy's own namespace.
    +		// For ClusterPolicies (policyNamespace == ""), preserve the existing default of "default".
    +		if cml.policyNamespace != "" {
    +			namespaceStr = cml.policyNamespace
    +		} else {
    +			namespaceStr = "default"
    +		}
    +	}
    +	// For namespaced policies, reject cross-namespace ConfigMap access.
    +	// This mirrors the protection applied to apiCall.URLPath in CVE-2026-22039.
    +	if cml.policyNamespace != "" && namespaceStr != cml.policyNamespace {
    +		return nil, fmt.Errorf("context entry %s: configMap namespace %q is different from policy namespace %q", entryName, namespaceStr, cml.policyNamespace)
     	}
    -	obj, err := cml.resolver.Get(cml.ctx, namespace.(string), name.(string))
    +	obj, err := cml.resolver.Get(cml.ctx, namespaceStr, nameStr)
     	if err != nil {
    -		return nil, fmt.Errorf("failed to get configmap %s/%s : %v", namespace, name, err)
    +		return nil, fmt.Errorf("failed to get configmap %s/%s : %v", namespaceStr, nameStr, err)
     	}
     	// extract configmap data
     	contextData["data"] = obj.Data
     	contextData["metadata"] = obj.ObjectMeta
     	data, err := json.Marshal(contextData)
     	if err != nil {
    -		return nil, fmt.Errorf("failed to unmarshal configmap %s/%s: %v", namespace, name, err)
    +		return nil, fmt.Errorf("failed to unmarshal configmap %s/%s: %v", namespaceStr, nameStr, err)
     	}
     	return data, nil
     }
    
  • pkg/engine/context/loaders/configmap_test.go+143 0 added
    @@ -0,0 +1,143 @@
    +package loaders
    +
    +import (
    +	"context"
    +	"testing"
    +
    +	"github.com/go-logr/logr"
    +	kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
    +	"github.com/kyverno/kyverno/pkg/config"
    +	enginecontext "github.com/kyverno/kyverno/pkg/engine/context"
    +	"github.com/kyverno/kyverno/pkg/engine/jmespath"
    +	"gotest.tools/assert"
    +	corev1 "k8s.io/api/core/v1"
    +	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    +)
    +
    +var jp = jmespath.New(config.NewDefaultConfiguration(false))
    +
    +type mockConfigMapResolver struct {
    +	cm      *corev1.ConfigMap
    +	err     error
    +	called  bool
    +	gotNS   string
    +	gotName string
    +}
    +
    +func (m *mockConfigMapResolver) Get(_ context.Context, namespace, name string) (*corev1.ConfigMap, error) {
    +	m.called = true
    +	m.gotNS = namespace
    +	m.gotName = name
    +	if m.err != nil {
    +		return nil, m.err
    +	}
    +	return m.cm, nil
    +}
    +
    +func makeEntry(namespace string) kyvernov1.ContextEntry {
    +	return kyvernov1.ContextEntry{
    +		Name: "testcm",
    +		ConfigMap: &kyvernov1.ConfigMapReference{
    +			Name:      "sensitive-config",
    +			Namespace: namespace,
    +		},
    +	}
    +}
    +
    +func makeCM(namespace string) *corev1.ConfigMap {
    +	return &corev1.ConfigMap{
    +		ObjectMeta: metav1.ObjectMeta{
    +			Name:      "sensitive-config",
    +			Namespace: namespace,
    +		},
    +		Data: map[string]string{"key": "value"},
    +	}
    +}
    +
    +// Test_CrossNamespaceConfigMapAccess verifies that a namespaced policy cannot
    +// read ConfigMaps from a different namespace (GHSA-cvq5-hhx3-f99p).
    +func Test_CrossNamespaceConfigMapAccess(t *testing.T) {
    +	resolver := &mockConfigMapResolver{cm: makeCM("victim-ns")}
    +	entry := makeEntry("victim-ns")
    +	ctx := enginecontext.NewContext(jp)
    +	ldr := NewConfigMapLoader(context.TODO(), logr.Discard(), entry, resolver, ctx, "attacker-ns")
    +	err := ldr.LoadData()
    +	assert.ErrorContains(t, err, `configMap namespace "victim-ns" is different from policy namespace "attacker-ns"`)
    +	assert.Equal(t, resolver.called, false)
    +}
    +
    +func Test_SameNamespaceConfigMapAccess(t *testing.T) {
    +	resolver := &mockConfigMapResolver{cm: makeCM("app-ns")}
    +	entry := makeEntry("app-ns")
    +	ctx := enginecontext.NewContext(jp)
    +	ldr := NewConfigMapLoader(context.TODO(), logr.Discard(), entry, resolver, ctx, "app-ns")
    +	err := ldr.LoadData()
    +	assert.NilError(t, err)
    +	assert.Equal(t, resolver.called, true)
    +	assert.Equal(t, resolver.gotNS, "app-ns")
    +	assert.Equal(t, resolver.gotName, "sensitive-config")
    +}
    +
    +func Test_CrossNamespaceConfigMapAccess_EmptyNamespaceDefaultsToPolicyNS(t *testing.T) {
    +	resolver := &mockConfigMapResolver{cm: makeCM("app-ns")}
    +	entry := makeEntry("")
    +	ctx := enginecontext.NewContext(jp)
    +	ldr := NewConfigMapLoader(context.TODO(), logr.Discard(), entry, resolver, ctx, "app-ns")
    +	err := ldr.LoadData()
    +	assert.NilError(t, err)
    +	assert.Equal(t, resolver.called, true)
    +	assert.Equal(t, resolver.gotNS, "app-ns")
    +	assert.Equal(t, resolver.gotName, "sensitive-config")
    +}
    +
    +func Test_CrossNamespaceConfigMapAccess_ClusterPolicyUnrestricted(t *testing.T) {
    +	resolver := &mockConfigMapResolver{cm: makeCM("any-ns")}
    +	entry := makeEntry("any-ns")
    +	ctx := enginecontext.NewContext(jp)
    +	ldr := NewConfigMapLoader(context.TODO(), logr.Discard(), entry, resolver, ctx, "")
    +	err := ldr.LoadData()
    +	assert.NilError(t, err)
    +	assert.Equal(t, resolver.called, true)
    +	assert.Equal(t, resolver.gotNS, "any-ns")
    +	assert.Equal(t, resolver.gotName, "sensitive-config")
    +}
    +
    +func Test_CrossNamespaceConfigMapAccess_WithVariableSubstitution(t *testing.T) {
    +	resolver := &mockConfigMapResolver{cm: makeCM("victim-ns")}
    +	entry := makeEntry("{{ targetNs }}")
    +	ctx := enginecontext.NewContext(jp)
    +	assert.NilError(t, ctx.AddContextEntry("targetNs", []byte(`"victim-ns"`)))
    +	ldr := NewConfigMapLoader(context.TODO(), logr.Discard(), entry, resolver, ctx, "attacker-ns")
    +	err := ldr.LoadData()
    +	assert.ErrorContains(t, err, `configMap namespace "victim-ns" is different from policy namespace "attacker-ns"`)
    +	assert.Equal(t, resolver.called, false)
    +}
    +
    +func Test_ConfigMapAccess_WithNonStringNamespaceSubstitution(t *testing.T) {
    +	resolver := &mockConfigMapResolver{cm: makeCM("app-ns")}
    +	entry := makeEntry("{{ targetNs }}")
    +	ctx := enginecontext.NewContext(jp)
    +	assert.NilError(t, ctx.AddContextEntry("targetNs", []byte(`123`)))
    +
    +	ldr := NewConfigMapLoader(context.TODO(), logr.Discard(), entry, resolver, ctx, "app-ns")
    +	err := ldr.LoadData()
    +
    +	assert.ErrorContains(t, err, "configMap.namespace")
    +	assert.ErrorContains(t, err, "expected string")
    +	assert.Equal(t, resolver.called, false)
    +}
    +
    +func Test_ConfigMapAccess_WithNonStringNameSubstitution(t *testing.T) {
    +	resolver := &mockConfigMapResolver{cm: makeCM("app-ns")}
    +	entry := makeEntry("app-ns")
    +	entry.ConfigMap.Name = "{{ cmName }}"
    +	ctx := enginecontext.NewContext(jp)
    +	assert.NilError(t, ctx.AddContextEntry("cmName", []byte(`123`)))
    +
    +	ldr := NewConfigMapLoader(context.TODO(), logr.Discard(), entry, resolver, ctx, "app-ns")
    +	err := ldr.LoadData()
    +
    +	assert.ErrorContains(t, err, "configMap.name")
    +	assert.ErrorContains(t, err, "expected string")
    +	assert.Equal(t, resolver.called, false)
    +}
    
  • pkg/engine/factories/contextloaderfactory.go+1 1 modified
    @@ -106,7 +106,7 @@ func (l *contextLoader) newLoader(
     ) (enginecontext.DeferredLoader, error) {
     	if entry.ConfigMap != nil {
     		if l.cmResolver != nil {
    -			ldr := loaders.NewConfigMapLoader(ctx, l.logger, entry, l.cmResolver, jsonContext)
    +			ldr := loaders.NewConfigMapLoader(ctx, l.logger, entry, l.cmResolver, jsonContext, l.policyNamespace)
     			return enginecontext.NewDeferredLoader(entry.Name, ldr, l.logger)
     		} else {
     			l.logger.V(3).Info("disabled loading of ConfigMap context entry", "name", entry.Name)
    

Vulnerability mechanics

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

References

4

News mentions

0

No linked articles in our index yet.