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.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/kyverno/kyvernoGo | <= 1.17.1 | — |
Affected products
1Patches
1bbf3e5c01391restrict configmap access for namespaced policies (#15850)
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- github.com/kyverno/kyverno/commit/bbf3e5c01391d612968440659028ae98e565a777nvdPatchWEB
- github.com/kyverno/kyverno/security/advisories/GHSA-cvq5-hhx3-f99pnvdExploitVendor AdvisoryWEB
- github.com/advisories/GHSA-cvq5-hhx3-f99pghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-41068ghsaADVISORY
News mentions
0No linked articles in our index yet.