Kyverno Cross-Namespace Privilege Escalation via Policy apiCall
Description
Kyverno is a policy engine designed for cloud native platform engineering teams. Versions prior to 1.16.3 and 1.15.3 have a critical authorization boundary bypass in namespaced Kyverno Policy apiCall. The resolved urlPath is executed using the Kyverno admission controller ServiceAccount, with no enforcement that the request is limited to the policy’s namespace. As a result, any authenticated user with permission to create a namespaced Policy can cause Kyverno to perform Kubernetes API requests using Kyverno’s admission controller identity, targeting any API path allowed by that ServiceAccount’s RBAC. This breaks namespace isolation by enabling cross-namespace reads (for example, ConfigMaps and, where permitted, Secrets) and allows cluster-scoped or cross-namespace writes (for example, creating ClusterPolicies) by controlling the urlPath through context variable substitution. Versions 1.16.3 and 1.15.3 contain a patch for the vulnerability.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/kyverno/kyvernoGo | < 1.15.3 | 1.15.3 |
github.com/kyverno/kyvernoGo | >= 1.16.0-rc.1, < 1.16.3 | 1.16.3 |
Affected products
1Patches
2e0ba4de4f1e0Merge commit from fork (#14845)
4 files changed · +159 −46
pkg/engine/apicall/apiCall.go+30 −10 modified@@ -4,6 +4,8 @@ import ( "context" "encoding/json" "fmt" + "path" + "regexp" "github.com/go-logr/logr" kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1" @@ -12,12 +14,15 @@ import ( "github.com/kyverno/kyverno/pkg/engine/variables" ) +var namespacePathRegex = regexp.MustCompile(`^/api(s)?/.*?/namespaces/([^/]+)/?.*$`) + type apiCall struct { - logger logr.Logger - jp jmespath.Interface - entry kyvernov1.ContextEntry - jsonCtx enginecontext.Interface - executor Executor + logger logr.Logger + jp jmespath.Interface + entry kyvernov1.ContextEntry + jsonCtx enginecontext.Interface + executor Executor + policyNamespace string } func New( @@ -27,6 +32,7 @@ func New( jsonCtx enginecontext.Interface, client ClientInterface, apiCallConfig APICallConfiguration, + policyNamespace string, ) (*apiCall, error) { if entry.APICall == nil { return nil, fmt.Errorf("missing APICall in context entry %v", entry) @@ -35,11 +41,12 @@ func New( executor := NewExecutor(logger, entry.Name, client, apiCallConfig) return &apiCall{ - logger: logger, - jp: jp, - entry: entry, - jsonCtx: jsonCtx, - executor: executor, + logger: logger, + jp: jp, + entry: entry, + jsonCtx: jsonCtx, + executor: executor, + policyNamespace: policyNamespace, }, nil } @@ -62,6 +69,19 @@ func (a *apiCall) Fetch(ctx context.Context) ([]byte, error) { if err != nil { return nil, fmt.Errorf("failed to substitute variables in context entry %s %s: %v", a.entry.Name, a.entry.APICall.URLPath, err) } + + if a.policyNamespace != "" { + cleanPath := path.Clean(call.APICall.URLPath) + if matches := namespacePathRegex.FindStringSubmatch(cleanPath); len(matches) > 2 { + ns := matches[2] + if ns != a.policyNamespace { + return nil, fmt.Errorf("path %s refers to namespace %s, which is different from the policy namespace %s", cleanPath, ns, a.policyNamespace) + } + } else { + return nil, fmt.Errorf("path %s does not contain a namespace segment, which is required for namespaced policies", cleanPath) + } + } + data, err := a.Execute(ctx, &call.APICall) if err != nil { if data == nil && a.entry.APICall.Default != nil {
pkg/engine/apicall/apiCall_test.go+95 −11 modified@@ -86,7 +86,7 @@ func Test_serviceGetRequest(t *testing.T) { entry := kyvernov1.ContextEntry{} ctx := enginecontext.NewContext(jp) - _, err := New(logr.Discard(), jp, entry, ctx, nil, apiConfig) + _, err := New(logr.Discard(), jp, entry, ctx, nil, apiConfig, "") assert.ErrorContains(t, err, "missing APICall") entry.Name = "test" @@ -102,32 +102,32 @@ func Test_serviceGetRequest(t *testing.T) { }, } - call, err := New(logr.Discard(), jp, entry, ctx, nil, apiConfig) + call, err := New(logr.Discard(), jp, entry, ctx, nil, apiConfig, "") assert.NilError(t, err) _, err = call.FetchAndLoad(context.TODO()) assert.ErrorContains(t, err, "invalid request type") entry.APICall.Method = "GET" - call, err = New(logr.Discard(), jp, entry, ctx, nil, apiConfig) + call, err = New(logr.Discard(), jp, entry, ctx, nil, apiConfig, "") assert.NilError(t, err) _, err = call.FetchAndLoad(context.TODO()) assert.ErrorContains(t, err, "HTTP 404") entry.APICall.Service.URL = s.URL + "/resource" - call, err = New(logr.Discard(), jp, entry, ctx, nil, apiConfig) + call, err = New(logr.Discard(), jp, entry, ctx, nil, apiConfig, "") assert.NilError(t, err) data, err := call.FetchAndLoad(context.TODO()) assert.NilError(t, err) assert.Assert(t, data != nil, "nil data") assert.Equal(t, string(serverResponse), string(data)) - call, err = New(logr.Discard(), jp, entry, ctx, nil, apiConfigMaxSizeExceed) + call, err = New(logr.Discard(), jp, entry, ctx, nil, apiConfigMaxSizeExceed, "") assert.NilError(t, err) _, err = call.FetchAndLoad(context.TODO()) assert.ErrorContains(t, err, "response length must be less than max allowed response length of 10") - call, err = New(logr.Discard(), jp, entry, ctx, nil, apiConfigWithoutSecurityCheck) + call, err = New(logr.Discard(), jp, entry, ctx, nil, apiConfigWithoutSecurityCheck, "") assert.NilError(t, err) data, err = call.FetchAndLoad(context.TODO()) assert.NilError(t, err) @@ -157,7 +157,7 @@ func Test_servicePostRequest(t *testing.T) { } ctx := enginecontext.NewContext(jp) - call, err := New(logr.Discard(), jp, entry, ctx, nil, apiConfig) + call, err := New(logr.Discard(), jp, entry, ctx, nil, apiConfig, "") assert.NilError(t, err) data, err := call.FetchAndLoad(context.TODO()) assert.NilError(t, err) @@ -190,7 +190,7 @@ func Test_servicePostRequest(t *testing.T) { "name": "busybox", "tag": "latest" } - } + } }` err = ctx.AddContextEntry("images", []byte(imageData)) @@ -205,7 +205,7 @@ func Test_servicePostRequest(t *testing.T) { }, } - call, err = New(logr.Discard(), jp, entry, ctx, nil, apiConfig) + call, err = New(logr.Discard(), jp, entry, ctx, nil, apiConfig, "") assert.NilError(t, err) data, err = call.FetchAndLoad(context.TODO()) assert.NilError(t, err) @@ -240,7 +240,7 @@ func Test_fallbackToDefault(t *testing.T) { } entry.APICall.Method = "GET" - call, err := New(logr.Discard(), jp, entry, ctx, nil, apiConfig) + call, err := New(logr.Discard(), jp, entry, ctx, nil, apiConfig, "") assert.NilError(t, err) jsonData, err := call.Fetch(context.TODO()) @@ -292,7 +292,7 @@ func Test_serviceHeaders(t *testing.T) { } entry.APICall.Service.URL = s.URL + "/resource" - call, err := New(logr.Discard(), jp, entry, ctx, nil, apiConfig) + call, err := New(logr.Discard(), jp, entry, ctx, nil, apiConfig, "") assert.NilError(t, err) data, err := call.FetchAndLoad(context.TODO()) assert.NilError(t, err) @@ -305,3 +305,87 @@ func Test_serviceHeaders(t *testing.T) { assert.Equal(t, "application/json", responseHeaders["Content-Type"][0]) assert.Equal(t, "CustomVal", responseHeaders["Custom-Key"][0]) } + +type mockClient struct{} + +func (c *mockClient) RawAbsPath(ctx context.Context, path string, method string, dataReader io.Reader) ([]byte, error) { + return []byte("{}"), nil +} + +func Test_CrossNamespaceAccess(t *testing.T) { + entry := kyvernov1.ContextEntry{ + Name: "test", + APICall: &kyvernov1.ContextAPICall{ + APICall: kyvernov1.APICall{ + URLPath: "/api/v1/namespaces/kube-system/secrets/top-secret", + Method: "GET", + }, + }, + } + ctx := enginecontext.NewContext(jp) + client := &mockClient{} + + // Namespaced policy in 'default' trying to access 'kube-system' - should fail + call, err := New(logr.Discard(), jp, entry, ctx, client, apiConfig, "default") + assert.NilError(t, err) + _, err = call.Fetch(context.TODO()) + assert.ErrorContains(t, err, "refers to namespace kube-system, which is different from the policy namespace default") + + // Namespaced policy in 'kube-system' trying to access 'kube-system' - should pass + call, err = New(logr.Discard(), jp, entry, ctx, client, apiConfig, "kube-system") + assert.NilError(t, err) + _, err = call.Fetch(context.TODO()) + assert.NilError(t, err) + + // Namespaced policy in 'default' trying to access a cluster-scoped resource - should fail + clusterScopedEntry := kyvernov1.ContextEntry{ + Name: "test", + APICall: &kyvernov1.ContextAPICall{ + APICall: kyvernov1.APICall{ + URLPath: "/api/v1/nodes", + Method: "GET", + }, + }, + } + call, err = New(logr.Discard(), jp, clusterScopedEntry, ctx, client, apiConfig, "default") + assert.NilError(t, err) + _, err = call.Fetch(context.TODO()) + assert.ErrorContains(t, err, "does not contain a namespace segment, which is required for namespaced policies") + + // ClusterPolicy (empty namespace) accessing any namespace - should pass + call, err = New(logr.Discard(), jp, entry, ctx, client, apiConfig, "") + assert.NilError(t, err) + _, err = call.Fetch(context.TODO()) + assert.NilError(t, err) +} + +func Test_CrossNamespaceAccess_WithVariableSubstitution(t *testing.T) { + // URLPath with a variable that will be substituted + entry := kyvernov1.ContextEntry{ + Name: "test", + APICall: &kyvernov1.ContextAPICall{ + APICall: kyvernov1.APICall{ + URLPath: "/api/v1/namespaces/{{ targetNs }}/secrets/mysecret", + Method: "GET", + }, + }, + } + ctx := enginecontext.NewContext(jp) + client := &mockClient{} + + // Set up context so variable resolves to 'kube-system' + err := ctx.AddContextEntry("targetNs", []byte(`"kube-system"`)) + assert.NilError(t, err) + + // Policy in 'default' - variable resolves to 'kube-system' - should fail + call, err := New(logr.Discard(), jp, entry, ctx, client, apiConfig, "default") + assert.NilError(t, err) + _, err = call.Fetch(context.TODO()) + assert.ErrorContains(t, err, "refers to namespace kube-system, which is different from the policy namespace default") + + // Policy in 'kube-system' - variable resolves to 'kube-system' - should pass + call, err = New(logr.Discard(), jp, entry, ctx, client, apiConfig, "kube-system") + assert.NilError(t, err) + _, err = call.Fetch(context.TODO()) + assert.NilError(t, err) +}
pkg/engine/context/loaders/apicall.go+19 −16 modified@@ -13,14 +13,15 @@ import ( ) type apiLoader struct { - ctx context.Context //nolint:containedctx - logger logr.Logger - entry kyvernov1.ContextEntry - enginectx enginecontext.Interface - jp jmespath.Interface - client engineapi.RawClient - config apicall.APICallConfiguration - data []byte + ctx context.Context //nolint:containedctx + logger logr.Logger + entry kyvernov1.ContextEntry + enginectx enginecontext.Interface + jp jmespath.Interface + client engineapi.RawClient + config apicall.APICallConfiguration + data []byte + policyNamespace string } func NewAPILoader( @@ -31,15 +32,17 @@ func NewAPILoader( jp jmespath.Interface, client engineapi.RawClient, apiCallConfig apicall.APICallConfiguration, + policyNamespace string, ) enginecontext.Loader { return &apiLoader{ - ctx: ctx, - logger: logger, - entry: entry, - enginectx: enginectx, - jp: jp, - client: client, - config: apiCallConfig, + ctx: ctx, + logger: logger, + entry: entry, + enginectx: enginectx, + jp: jp, + client: client, + config: apiCallConfig, + policyNamespace: policyNamespace, } } @@ -48,7 +51,7 @@ func (a *apiLoader) HasLoaded() bool { } func (a *apiLoader) LoadData() error { - executor, err := apicall.New(a.logger, a.jp, a.entry, a.enginectx, a.client, a.config) + executor, err := apicall.New(a.logger, a.jp, a.entry, a.enginectx, a.client, a.config, a.policyNamespace) if err != nil { return fmt.Errorf("failed to initiaize APICal: %w", err) }
pkg/engine/factories/contextloaderfactory.go+15 −9 modified@@ -18,10 +18,15 @@ import ( type ContextLoaderFactoryOptions func(*contextLoader) func DefaultContextLoaderFactory(cmResolver engineapi.ConfigmapResolver, opts ...ContextLoaderFactoryOptions) engineapi.ContextLoaderFactory { - return func(_ kyvernov1.PolicyInterface, _ kyvernov1.Rule) engineapi.ContextLoader { + return func(policy kyvernov1.PolicyInterface, _ kyvernov1.Rule) engineapi.ContextLoader { + policyNamespace := "" + if policy != nil && policy.IsNamespaced() { + policyNamespace = policy.GetNamespace() + } cl := &contextLoader{ - logger: logging.WithName("DefaultContextLoaderFactory"), - cmResolver: cmResolver, + logger: logging.WithName("DefaultContextLoaderFactory"), + cmResolver: cmResolver, + policyNamespace: policyNamespace, } for _, o := range opts { o(cl) @@ -49,11 +54,12 @@ func WithGlobalContextStore(gctxStore loaders.Store) ContextLoaderFactoryOptions } type contextLoader struct { - logger logr.Logger - cmResolver engineapi.ConfigmapResolver - initializers []engineapi.Initializer - apiCallConfig apicall.APICallConfiguration - gctxStore loaders.Store + logger logr.Logger + cmResolver engineapi.ConfigmapResolver + initializers []engineapi.Initializer + apiCallConfig apicall.APICallConfiguration + gctxStore loaders.Store + policyNamespace string } func (l *contextLoader) Load( @@ -108,7 +114,7 @@ func (l *contextLoader) newLoader( } } else if entry.APICall != nil { if client != nil { - ldr := loaders.NewAPILoader(ctx, l.logger, entry, jsonContext, jp, client, l.apiCallConfig) + ldr := loaders.NewAPILoader(ctx, l.logger, entry, jsonContext, jp, client, l.apiCallConfig, l.policyNamespace) return enginecontext.NewDeferredLoader(entry.Name, ldr, l.logger) } else { l.logger.V(3).Info("disabled loading of APICall context entry", "name", entry.Name)
eba60fa856c7Merge commit from fork (#14843)
4 files changed · +159 −46
pkg/engine/apicall/apiCall.go+30 −10 modified@@ -4,6 +4,8 @@ import ( "context" "encoding/json" "fmt" + "path" + "regexp" "github.com/go-logr/logr" kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1" @@ -12,12 +14,15 @@ import ( "github.com/kyverno/kyverno/pkg/engine/variables" ) +var namespacePathRegex = regexp.MustCompile(`^/api(s)?/.*?/namespaces/([^/]+)/?.*$`) + type apiCall struct { - logger logr.Logger - jp jmespath.Interface - entry kyvernov1.ContextEntry - jsonCtx enginecontext.Interface - executor Executor + logger logr.Logger + jp jmespath.Interface + entry kyvernov1.ContextEntry + jsonCtx enginecontext.Interface + executor Executor + policyNamespace string } func New( @@ -27,6 +32,7 @@ func New( jsonCtx enginecontext.Interface, client ClientInterface, apiCallConfig APICallConfiguration, + policyNamespace string, ) (*apiCall, error) { if entry.APICall == nil { return nil, fmt.Errorf("missing APICall in context entry %v", entry) @@ -35,11 +41,12 @@ func New( executor := NewExecutor(logger, entry.Name, client, apiCallConfig) return &apiCall{ - logger: logger, - jp: jp, - entry: entry, - jsonCtx: jsonCtx, - executor: executor, + logger: logger, + jp: jp, + entry: entry, + jsonCtx: jsonCtx, + executor: executor, + policyNamespace: policyNamespace, }, nil } @@ -62,6 +69,19 @@ func (a *apiCall) Fetch(ctx context.Context) ([]byte, error) { if err != nil { return nil, fmt.Errorf("failed to substitute variables in context entry %s %s: %v", a.entry.Name, a.entry.APICall.URLPath, err) } + + if a.policyNamespace != "" { + cleanPath := path.Clean(call.APICall.URLPath) + if matches := namespacePathRegex.FindStringSubmatch(cleanPath); len(matches) > 2 { + ns := matches[2] + if ns != a.policyNamespace { + return nil, fmt.Errorf("path %s refers to namespace %s, which is different from the policy namespace %s", cleanPath, ns, a.policyNamespace) + } + } else { + return nil, fmt.Errorf("path %s does not contain a namespace segment, which is required for namespaced policies", cleanPath) + } + } + data, err := a.Execute(ctx, &call.APICall) if err != nil { if data == nil && a.entry.APICall.Default != nil {
pkg/engine/apicall/apiCall_test.go+95 −11 modified@@ -86,7 +86,7 @@ func Test_serviceGetRequest(t *testing.T) { entry := kyvernov1.ContextEntry{} ctx := enginecontext.NewContext(jp) - _, err := New(logr.Discard(), jp, entry, ctx, nil, apiConfig) + _, err := New(logr.Discard(), jp, entry, ctx, nil, apiConfig, "") assert.ErrorContains(t, err, "missing APICall") entry.Name = "test" @@ -102,32 +102,32 @@ func Test_serviceGetRequest(t *testing.T) { }, } - call, err := New(logr.Discard(), jp, entry, ctx, nil, apiConfig) + call, err := New(logr.Discard(), jp, entry, ctx, nil, apiConfig, "") assert.NilError(t, err) _, err = call.FetchAndLoad(context.TODO()) assert.ErrorContains(t, err, "invalid request type") entry.APICall.Method = "GET" - call, err = New(logr.Discard(), jp, entry, ctx, nil, apiConfig) + call, err = New(logr.Discard(), jp, entry, ctx, nil, apiConfig, "") assert.NilError(t, err) _, err = call.FetchAndLoad(context.TODO()) assert.ErrorContains(t, err, "HTTP 404") entry.APICall.Service.URL = s.URL + "/resource" - call, err = New(logr.Discard(), jp, entry, ctx, nil, apiConfig) + call, err = New(logr.Discard(), jp, entry, ctx, nil, apiConfig, "") assert.NilError(t, err) data, err := call.FetchAndLoad(context.TODO()) assert.NilError(t, err) assert.Assert(t, data != nil, "nil data") assert.Equal(t, string(serverResponse), string(data)) - call, err = New(logr.Discard(), jp, entry, ctx, nil, apiConfigMaxSizeExceed) + call, err = New(logr.Discard(), jp, entry, ctx, nil, apiConfigMaxSizeExceed, "") assert.NilError(t, err) _, err = call.FetchAndLoad(context.TODO()) assert.ErrorContains(t, err, "response length must be less than max allowed response length of 10") - call, err = New(logr.Discard(), jp, entry, ctx, nil, apiConfigWithoutSecurityCheck) + call, err = New(logr.Discard(), jp, entry, ctx, nil, apiConfigWithoutSecurityCheck, "") assert.NilError(t, err) data, err = call.FetchAndLoad(context.TODO()) assert.NilError(t, err) @@ -157,7 +157,7 @@ func Test_servicePostRequest(t *testing.T) { } ctx := enginecontext.NewContext(jp) - call, err := New(logr.Discard(), jp, entry, ctx, nil, apiConfig) + call, err := New(logr.Discard(), jp, entry, ctx, nil, apiConfig, "") assert.NilError(t, err) data, err := call.FetchAndLoad(context.TODO()) assert.NilError(t, err) @@ -190,7 +190,7 @@ func Test_servicePostRequest(t *testing.T) { "name": "busybox", "tag": "latest" } - } + } }` err = ctx.AddContextEntry("images", []byte(imageData)) @@ -205,7 +205,7 @@ func Test_servicePostRequest(t *testing.T) { }, } - call, err = New(logr.Discard(), jp, entry, ctx, nil, apiConfig) + call, err = New(logr.Discard(), jp, entry, ctx, nil, apiConfig, "") assert.NilError(t, err) data, err = call.FetchAndLoad(context.TODO()) assert.NilError(t, err) @@ -240,7 +240,7 @@ func Test_fallbackToDefault(t *testing.T) { } entry.APICall.Method = "GET" - call, err := New(logr.Discard(), jp, entry, ctx, nil, apiConfig) + call, err := New(logr.Discard(), jp, entry, ctx, nil, apiConfig, "") assert.NilError(t, err) jsonData, err := call.Fetch(context.TODO()) @@ -292,7 +292,7 @@ func Test_serviceHeaders(t *testing.T) { } entry.APICall.Service.URL = s.URL + "/resource" - call, err := New(logr.Discard(), jp, entry, ctx, nil, apiConfig) + call, err := New(logr.Discard(), jp, entry, ctx, nil, apiConfig, "") assert.NilError(t, err) data, err := call.FetchAndLoad(context.TODO()) assert.NilError(t, err) @@ -305,3 +305,87 @@ func Test_serviceHeaders(t *testing.T) { assert.Equal(t, "application/json", responseHeaders["Content-Type"][0]) assert.Equal(t, "CustomVal", responseHeaders["Custom-Key"][0]) } + +type mockClient struct{} + +func (c *mockClient) RawAbsPath(ctx context.Context, path string, method string, dataReader io.Reader) ([]byte, error) { + return []byte("{}"), nil +} + +func Test_CrossNamespaceAccess(t *testing.T) { + entry := kyvernov1.ContextEntry{ + Name: "test", + APICall: &kyvernov1.ContextAPICall{ + APICall: kyvernov1.APICall{ + URLPath: "/api/v1/namespaces/kube-system/secrets/top-secret", + Method: "GET", + }, + }, + } + ctx := enginecontext.NewContext(jp) + client := &mockClient{} + + // Namespaced policy in 'default' trying to access 'kube-system' - should fail + call, err := New(logr.Discard(), jp, entry, ctx, client, apiConfig, "default") + assert.NilError(t, err) + _, err = call.Fetch(context.TODO()) + assert.ErrorContains(t, err, "refers to namespace kube-system, which is different from the policy namespace default") + + // Namespaced policy in 'kube-system' trying to access 'kube-system' - should pass + call, err = New(logr.Discard(), jp, entry, ctx, client, apiConfig, "kube-system") + assert.NilError(t, err) + _, err = call.Fetch(context.TODO()) + assert.NilError(t, err) + + // Namespaced policy in 'default' trying to access a cluster-scoped resource - should fail + clusterScopedEntry := kyvernov1.ContextEntry{ + Name: "test", + APICall: &kyvernov1.ContextAPICall{ + APICall: kyvernov1.APICall{ + URLPath: "/api/v1/nodes", + Method: "GET", + }, + }, + } + call, err = New(logr.Discard(), jp, clusterScopedEntry, ctx, client, apiConfig, "default") + assert.NilError(t, err) + _, err = call.Fetch(context.TODO()) + assert.ErrorContains(t, err, "does not contain a namespace segment, which is required for namespaced policies") + + // ClusterPolicy (empty namespace) accessing any namespace - should pass + call, err = New(logr.Discard(), jp, entry, ctx, client, apiConfig, "") + assert.NilError(t, err) + _, err = call.Fetch(context.TODO()) + assert.NilError(t, err) +} + +func Test_CrossNamespaceAccess_WithVariableSubstitution(t *testing.T) { + // URLPath with a variable that will be substituted + entry := kyvernov1.ContextEntry{ + Name: "test", + APICall: &kyvernov1.ContextAPICall{ + APICall: kyvernov1.APICall{ + URLPath: "/api/v1/namespaces/{{ targetNs }}/secrets/mysecret", + Method: "GET", + }, + }, + } + ctx := enginecontext.NewContext(jp) + client := &mockClient{} + + // Set up context so variable resolves to 'kube-system' + err := ctx.AddContextEntry("targetNs", []byte(`"kube-system"`)) + assert.NilError(t, err) + + // Policy in 'default' - variable resolves to 'kube-system' - should fail + call, err := New(logr.Discard(), jp, entry, ctx, client, apiConfig, "default") + assert.NilError(t, err) + _, err = call.Fetch(context.TODO()) + assert.ErrorContains(t, err, "refers to namespace kube-system, which is different from the policy namespace default") + + // Policy in 'kube-system' - variable resolves to 'kube-system' - should pass + call, err = New(logr.Discard(), jp, entry, ctx, client, apiConfig, "kube-system") + assert.NilError(t, err) + _, err = call.Fetch(context.TODO()) + assert.NilError(t, err) +}
pkg/engine/context/loaders/apicall.go+19 −16 modified@@ -13,14 +13,15 @@ import ( ) type apiLoader struct { - ctx context.Context //nolint:containedctx - logger logr.Logger - entry kyvernov1.ContextEntry - enginectx enginecontext.Interface - jp jmespath.Interface - client engineapi.RawClient - config apicall.APICallConfiguration - data []byte + ctx context.Context //nolint:containedctx + logger logr.Logger + entry kyvernov1.ContextEntry + enginectx enginecontext.Interface + jp jmespath.Interface + client engineapi.RawClient + config apicall.APICallConfiguration + data []byte + policyNamespace string } func NewAPILoader( @@ -31,15 +32,17 @@ func NewAPILoader( jp jmespath.Interface, client engineapi.RawClient, apiCallConfig apicall.APICallConfiguration, + policyNamespace string, ) enginecontext.Loader { return &apiLoader{ - ctx: ctx, - logger: logger, - entry: entry, - enginectx: enginectx, - jp: jp, - client: client, - config: apiCallConfig, + ctx: ctx, + logger: logger, + entry: entry, + enginectx: enginectx, + jp: jp, + client: client, + config: apiCallConfig, + policyNamespace: policyNamespace, } } @@ -48,7 +51,7 @@ func (a *apiLoader) HasLoaded() bool { } func (a *apiLoader) LoadData() error { - executor, err := apicall.New(a.logger, a.jp, a.entry, a.enginectx, a.client, a.config) + executor, err := apicall.New(a.logger, a.jp, a.entry, a.enginectx, a.client, a.config, a.policyNamespace) if err != nil { return fmt.Errorf("failed to initiaize APICal: %w", err) }
pkg/engine/factories/contextloaderfactory.go+15 −9 modified@@ -18,10 +18,15 @@ import ( type ContextLoaderFactoryOptions func(*contextLoader) func DefaultContextLoaderFactory(cmResolver engineapi.ConfigmapResolver, opts ...ContextLoaderFactoryOptions) engineapi.ContextLoaderFactory { - return func(_ kyvernov1.PolicyInterface, _ kyvernov1.Rule) engineapi.ContextLoader { + return func(policy kyvernov1.PolicyInterface, _ kyvernov1.Rule) engineapi.ContextLoader { + policyNamespace := "" + if policy != nil && policy.IsNamespaced() { + policyNamespace = policy.GetNamespace() + } cl := &contextLoader{ - logger: logging.WithName("DefaultContextLoaderFactory"), - cmResolver: cmResolver, + logger: logging.WithName("DefaultContextLoaderFactory"), + cmResolver: cmResolver, + policyNamespace: policyNamespace, } for _, o := range opts { o(cl) @@ -49,11 +54,12 @@ func WithGlobalContextStore(gctxStore loaders.Store) ContextLoaderFactoryOptions } type contextLoader struct { - logger logr.Logger - cmResolver engineapi.ConfigmapResolver - initializers []engineapi.Initializer - apiCallConfig apicall.APICallConfiguration - gctxStore loaders.Store + logger logr.Logger + cmResolver engineapi.ConfigmapResolver + initializers []engineapi.Initializer + apiCallConfig apicall.APICallConfiguration + gctxStore loaders.Store + policyNamespace string } func (l *contextLoader) Load( @@ -108,7 +114,7 @@ func (l *contextLoader) newLoader( } } else if entry.APICall != nil { if client != nil { - ldr := loaders.NewAPILoader(ctx, l.logger, entry, jsonContext, jp, client, l.apiCallConfig) + ldr := loaders.NewAPILoader(ctx, l.logger, entry, jsonContext, jp, client, l.apiCallConfig, l.policyNamespace) return enginecontext.NewDeferredLoader(entry.Name, ldr, l.logger) } else { l.logger.V(3).Info("disabled loading of APICall 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
5- github.com/advisories/GHSA-8p9x-46gm-qfx2ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-22039ghsaADVISORY
- github.com/kyverno/kyverno/commit/e0ba4de4f1e0ca325066d5095db51aec45b1407bghsax_refsource_MISCWEB
- github.com/kyverno/kyverno/commit/eba60fa856c781bcb9c3be066061a3df03ae4e3eghsax_refsource_MISCWEB
- github.com/kyverno/kyverno/security/advisories/GHSA-8p9x-46gm-qfx2ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.