VYPR
High severityOSV Advisory· Published Jan 27, 2026· Updated Jan 27, 2026

Kyverno Denial of Service via Context Variable Amplification in Policy Engine

CVE-2026-23881

Description

Kyverno is a policy engine designed for cloud native platform engineering teams. Versions prior to 1.16.3 and 1.15.3 have unbounded memory consumption in Kyverno's policy engine that allows users with policy creation privileges to cause denial of service by crafting policies that exponentially amplify string data through context variables. Versions 1.16.3 and 1.15.3 contain a patch for the vulnerability.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/kyverno/kyvernoGo
< 1.15.31.15.3
github.com/kyverno/kyvernoGo
>= 1.16.0-rc.1, < 1.16.31.16.3

Affected products

1

Patches

2
7a651be3a8c7

Merge commit from fork (#14844)

https://github.com/kyverno/kyvernoshutingJan 26, 2026via ghsa
10 files changed · +739 11
  • charts/kyverno/README.md+1 0 modified
    @@ -300,6 +300,7 @@ The chart values are organised per component.
     | config.excludeRoles | list | `[]` | Exclude roles |
     | config.excludeClusterRoles | list | `[]` | Exclude roles |
     | config.generateSuccessEvents | bool | `false` | Generate success events. |
    +| config.maxContextSize | string | 2Mi | Maximum cumulative size of context data during policy evaluation. Supports Kubernetes quantity format (e.g., 100Mi, 2Gi) or plain bytes (e.g., 2097152). Limits memory used by context variables to prevent unbounded growth. Increase if policies legitimately need large context data (e.g., processing large ConfigMaps). Set to 0 to disable the limit (not recommended for production). |
     | config.resourceFilters | list | See [values.yaml](values.yaml) | Resource types to be skipped by the Kyverno policy engine. Make sure to surround each entry in quotes so that it doesn't get parsed as a nested YAML list. These are joined together without spaces, run through `tpl`, and the result is set in the config map. |
     | config.updateRequestThreshold | int | `1000` | Sets the threshold for the total number of UpdateRequests generated for mutateExisitng and generate policies. |
     | config.webhooks | object | `{"namespaceSelector":{"matchExpressions":[{"key":"kubernetes.io/metadata.name","operator":"NotIn","values":["kube-system"]}]}}` | Defines the `namespaceSelector`/`objectSelector` in the webhook configurations. The Kyverno namespace is excluded if `excludeKyvernoNamespace` is `true` (default) |
    
  • charts/kyverno/templates/config/configmap.yaml+3 0 modified
    @@ -54,4 +54,7 @@ data:
       {{- with .Values.config.matchConditions }}
       matchConditions: {{ toJson . | quote }}
       {{- end }}
    +  {{- with .Values.config.maxContextSize }}
    +  maxContextSize: {{ . | quote }}
    +  {{- end }}
     {{- end -}}
    
  • charts/kyverno/values.yaml+8 0 modified
    @@ -264,6 +264,14 @@ config:
       # -- Generate success events.
       generateSuccessEvents: false
     
    +  # -- Maximum cumulative size of context data during policy evaluation.
    +  # Supports Kubernetes quantity format (e.g., 100Mi, 2Gi) or plain bytes (e.g., 2097152).
    +  # Limits memory used by context variables to prevent unbounded growth.
    +  # Increase if policies legitimately need large context data (e.g., processing large ConfigMaps).
    +  # Set to 0 to disable the limit (not recommended for production).
    +  # @default -- 2Mi
    +  maxContextSize: ~
    +
       # -- Resource types to be skipped by the Kyverno policy engine.
       # Make sure to surround each entry in quotes so that it doesn't get parsed as a nested YAML list.
       # These are joined together without spaces, run through `tpl`, and the result is set in the config map.
    
  • pkg/config/config.go+29 0 modified
    @@ -11,6 +11,7 @@ import (
     	osutils "github.com/kyverno/kyverno/pkg/utils/os"
     	admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
     	corev1 "k8s.io/api/core/v1"
    +	"k8s.io/apimachinery/pkg/api/resource"
     	"k8s.io/apimachinery/pkg/runtime/schema"
     )
     
    @@ -113,10 +114,14 @@ const (
     	webhookLabels                 = "webhookLabels"
     	matchConditions               = "matchConditions"
     	updateRequestThreshold        = "updateRequestThreshold"
    +	maxContextSize                = "maxContextSize"
     )
     
     const UpdateRequestThreshold = 1000
     
    +// DefaultMaxContextSize is the default maximum size of context data in bytes (2MB)
    +const DefaultMaxContextSize int64 = 2 * 1024 * 1024
    +
     var (
     	// kyvernoNamespace is the Kyverno namespace
     	kyvernoNamespace = osutils.GetEnvWithFallback("KYVERNO_NAMESPACE", "kyverno")
    @@ -204,6 +209,8 @@ type Configuration interface {
     	OnChanged(func())
     	// GetUpdateRequestThreshold gets the threshold limit for the total number of updaterequests
     	GetUpdateRequestThreshold() int64
    +	// GetMaxContextSize gets the maximum context size in bytes for policy evaluation
    +	GetMaxContextSize() int64
     }
     
     // configuration stores the configuration
    @@ -222,6 +229,7 @@ type configuration struct {
     	mux                           sync.RWMutex
     	callbacks                     []func()
     	updateRequestThreshold        int64
    +	maxContextSize                int64
     }
     
     type match struct {
    @@ -356,6 +364,12 @@ func (cd *configuration) GetUpdateRequestThreshold() int64 {
     	return cd.updateRequestThreshold
     }
     
    +func (cd *configuration) GetMaxContextSize() int64 {
    +	cd.mux.RLock()
    +	defer cd.mux.RUnlock()
    +	return cd.maxContextSize
    +}
    +
     func (cd *configuration) Load(cm *corev1.ConfigMap) {
     	if cm != nil {
     		cd.load(cm)
    @@ -530,6 +544,20 @@ func (cd *configuration) load(cm *corev1.ConfigMap) {
     			logger.V(2).Info("enableDefaultRegistryMutation configured")
     		}
     	}
    +	// load maxContextSize (supports Kubernetes quantity format: 100Mi, 2Gi, etc.)
    +	cd.maxContextSize = DefaultMaxContextSize
    +	if maxCtxSizeStr, ok := data[maxContextSize]; ok {
    +		logger := logger.WithValues("maxContextSize", maxCtxSizeStr)
    +		quantity, err := resource.ParseQuantity(maxCtxSizeStr)
    +		if err != nil {
    +			logger.Error(err, "maxContextSize is not a valid quantity (use formats like 100Mi, 2Gi, or plain bytes)")
    +		} else {
    +			cd.maxContextSize = quantity.Value()
    +			logger.V(2).Info("maxContextSize configured", "bytes", cd.maxContextSize)
    +		}
    +	} else {
    +		logger.V(2).Info("maxContextSize not set, using default", "default", DefaultMaxContextSize)
    +	}
     }
     
     func (cd *configuration) unload() {
    @@ -545,6 +573,7 @@ func (cd *configuration) unload() {
     	cd.webhook = WebhookConfig{}
     	cd.webhookAnnotations = nil
     	cd.webhookLabels = nil
    +	cd.maxContextSize = DefaultMaxContextSize
     	logger.V(2).Info("configuration unloaded")
     }
     
    
  • pkg/config/config_test.go+123 0 added
    @@ -0,0 +1,123 @@
    +package config
    +
    +import (
    +	"testing"
    +
    +	"github.com/stretchr/testify/assert"
    +	corev1 "k8s.io/api/core/v1"
    +	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    +)
    +
    +func TestConfiguration_GetMaxContextSize_Default(t *testing.T) {
    +	cfg := NewDefaultConfiguration(false)
    +	// Load with nil configmap to trigger unload which sets defaults
    +	cfg.Load(nil)
    +
    +	assert.Equal(t, DefaultMaxContextSize, cfg.GetMaxContextSize())
    +}
    +
    +func TestConfiguration_GetMaxContextSize_FromConfigMap(t *testing.T) {
    +	cfg := NewDefaultConfiguration(false)
    +
    +	cm := &corev1.ConfigMap{
    +		ObjectMeta: metav1.ObjectMeta{
    +			Name:      "kyverno",
    +			Namespace: "kyverno",
    +		},
    +		Data: map[string]string{
    +			"maxContextSize": "4194304", // 4MB
    +		},
    +	}
    +
    +	cfg.Load(cm)
    +
    +	assert.Equal(t, int64(4194304), cfg.GetMaxContextSize())
    +}
    +
    +func TestConfiguration_GetMaxContextSize_InvalidValue(t *testing.T) {
    +	cfg := NewDefaultConfiguration(false)
    +
    +	cm := &corev1.ConfigMap{
    +		ObjectMeta: metav1.ObjectMeta{
    +			Name:      "kyverno",
    +			Namespace: "kyverno",
    +		},
    +		Data: map[string]string{
    +			"maxContextSize": "invalid",
    +		},
    +	}
    +
    +	cfg.Load(cm)
    +
    +	// Should fall back to default on parse error
    +	assert.Equal(t, DefaultMaxContextSize, cfg.GetMaxContextSize())
    +}
    +
    +func TestConfiguration_GetMaxContextSize_NotSet(t *testing.T) {
    +	cfg := NewDefaultConfiguration(false)
    +
    +	cm := &corev1.ConfigMap{
    +		ObjectMeta: metav1.ObjectMeta{
    +			Name:      "kyverno",
    +			Namespace: "kyverno",
    +		},
    +		Data: map[string]string{},
    +	}
    +
    +	cfg.Load(cm)
    +
    +	// Should use default when not set
    +	assert.Equal(t, DefaultMaxContextSize, cfg.GetMaxContextSize())
    +}
    +
    +func TestConfiguration_GetMaxContextSize_ZeroDisablesLimit(t *testing.T) {
    +	cfg := NewDefaultConfiguration(false)
    +
    +	cm := &corev1.ConfigMap{
    +		ObjectMeta: metav1.ObjectMeta{
    +			Name:      "kyverno",
    +			Namespace: "kyverno",
    +		},
    +		Data: map[string]string{
    +			"maxContextSize": "0",
    +		},
    +	}
    +
    +	cfg.Load(cm)
    +
    +	// Zero should be valid and disable the limit
    +	assert.Equal(t, int64(0), cfg.GetMaxContextSize())
    +}
    +
    +func TestConfiguration_GetMaxContextSize_KubernetesQuantityFormat(t *testing.T) {
    +	tests := []struct {
    +		name     string
    +		value    string
    +		expected int64
    +	}{
    +		{"100Mi", "100Mi", 100 * 1024 * 1024},
    +		{"4Mi", "4Mi", 4 * 1024 * 1024},
    +		{"1Gi", "1Gi", 1024 * 1024 * 1024},
    +		{"500Ki", "500Ki", 500 * 1024},
    +	}
    +
    +	for _, tt := range tests {
    +		t.Run(tt.name, func(t *testing.T) {
    +			cfg := NewDefaultConfiguration(false)
    +
    +			cm := &corev1.ConfigMap{
    +				ObjectMeta: metav1.ObjectMeta{
    +					Name:      "kyverno",
    +					Namespace: "kyverno",
    +				},
    +				Data: map[string]string{
    +					"maxContextSize": tt.value,
    +				},
    +			}
    +
    +			cfg.Load(cm)
    +
    +			assert.Equal(t, tt.expected, cfg.GetMaxContextSize())
    +		})
    +	}
    +}
    
  • pkg/config/mocks/mock_config.go+14 0 modified
    @@ -93,6 +93,20 @@ func (mr *MockConfigurationMockRecorder) GetMatchConditions() *gomock.Call {
     	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMatchConditions", reflect.TypeOf((*MockConfiguration)(nil).GetMatchConditions))
     }
     
    +// GetMaxContextSize mocks base method.
    +func (m *MockConfiguration) GetMaxContextSize() int64 {
    +	m.ctrl.T.Helper()
    +	ret := m.ctrl.Call(m, "GetMaxContextSize")
    +	ret0, _ := ret[0].(int64)
    +	return ret0
    +}
    +
    +// GetMaxContextSize indicates an expected call of GetMaxContextSize.
    +func (mr *MockConfigurationMockRecorder) GetMaxContextSize() *gomock.Call {
    +	mr.mock.ctrl.T.Helper()
    +	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMaxContextSize", reflect.TypeOf((*MockConfiguration)(nil).GetMaxContextSize))
    +}
    +
     // GetUpdateRequestThreshold mocks base method.
     func (m *MockConfiguration) GetUpdateRequestThreshold() int64 {
     	m.ctrl.T.Helper()
    
  • pkg/engine/context/context.go+46 0 modified
    @@ -114,6 +114,19 @@ type Interface interface {
     	addJSON(dataMap map[string]interface{}, overwriteMaps bool) error
     }
     
    +// DefaultMaxContextSize is the default maximum size of context data in bytes (2MB)
    +const DefaultMaxContextSize = 2 * 1024 * 1024
    +
    +// ContextSizeLimitExceededError is returned when context size exceeds the limit
    +type ContextSizeLimitExceededError struct {
    +	Size  int64
    +	Limit int64
    +}
    +
    +func (e ContextSizeLimitExceededError) Error() string {
    +	return fmt.Sprintf("context size limit exceeded: %d bytes exceeds limit of %d bytes", e.Size, e.Limit)
    +}
    +
     // Context stores the data resources as JSON
     type context struct {
     	jp                 jmespath.Interface
    @@ -122,6 +135,8 @@ type context struct {
     	images             map[string]map[string]apiutils.ImageInfo
     	operation          kyvernov1.AdmissionOperation
     	deferred           DeferredLoaders
    +	contextSize        int64
    +	maxContextSize     int64
     }
     
     // NewContext returns a new context
    @@ -136,6 +151,18 @@ func NewContextFromRaw(jp jmespath.Interface, raw map[string]interface{}) Interf
     		jsonRaw:            raw,
     		jsonRawCheckpoints: make([]map[string]interface{}, 0),
     		deferred:           NewDeferredLoaders(),
    +		maxContextSize:     DefaultMaxContextSize,
    +	}
    +}
    +
    +// NewContextWithMaxSize returns a new context with a specified maximum context size
    +func NewContextWithMaxSize(jp jmespath.Interface, maxSize int64) Interface {
    +	return &context{
    +		jp:                 jp,
    +		jsonRaw:            map[string]interface{}{},
    +		jsonRawCheckpoints: make([]map[string]interface{}, 0),
    +		deferred:           NewDeferredLoaders(),
    +		maxContextSize:     maxSize,
     	}
     }
     
    @@ -187,15 +214,22 @@ func (ctx *context) AddVariable(key string, value interface{}) error {
     }
     
     func (ctx *context) AddContextEntry(name string, dataRaw []byte) error {
    +	if err := ctx.checkContextSizeLimit(int64(len(dataRaw))); err != nil {
    +		return err
    +	}
     	var data interface{}
     	if err := json.Unmarshal(dataRaw, &data); err != nil {
     		logger.Error(err, "failed to unmarshal the resource")
     		return err
     	}
    +	ctx.contextSize += int64(len(dataRaw))
     	return addToContext(ctx, data, false, name)
     }
     
     func (ctx *context) ReplaceContextEntry(name string, dataRaw []byte) error {
    +	if err := ctx.checkContextSizeLimit(int64(len(dataRaw))); err != nil {
    +		return err
    +	}
     	var data interface{}
     	if err := json.Unmarshal(dataRaw, &data); err != nil {
     		logger.Error(err, "failed to unmarshal the resource")
    @@ -206,6 +240,7 @@ func (ctx *context) ReplaceContextEntry(name string, dataRaw []byte) error {
     		logger.Error(err, "unable to replace context entry", "context entry name", name)
     		return err
     	}
    +	ctx.contextSize += int64(len(dataRaw))
     	return addToContext(ctx, data, false, name)
     }
     
    @@ -464,3 +499,14 @@ func (ctx *context) AddDeferredLoader(dl DeferredLoader) error {
     	ctx.deferred.Add(dl, len(ctx.jsonRawCheckpoints))
     	return nil
     }
    +
    +// checkContextSizeLimit checks if adding additionalSize bytes would exceed the context size limit
    +func (ctx *context) checkContextSizeLimit(additionalSize int64) error {
    +	if ctx.maxContextSize > 0 && ctx.contextSize+additionalSize > ctx.maxContextSize {
    +		return ContextSizeLimitExceededError{
    +			Size:  ctx.contextSize + additionalSize,
    +			Limit: ctx.maxContextSize,
    +		}
    +	}
    +	return nil
    +}
    
  • pkg/engine/context/context_test.go+177 0 modified
    @@ -1,7 +1,10 @@
     package context
     
     import (
    +	stdjson "encoding/json"
    +	"fmt"
     	"reflect"
    +	"strings"
     	"testing"
     
     	urkyverno "github.com/kyverno/kyverno/api/kyverno/v2"
    @@ -284,3 +287,177 @@ func Test_ImageInfoLoader_OnDirectCall(t *testing.T) {
     	imageinfos := newctx.ImageInfo()
     	assert.Equal(t, imageinfos["containers"]["test_container"].Name, "nginx")
     }
    +
    +func Test_ContextSizeLimit(t *testing.T) {
    +	tests := []struct {
    +		name    string
    +		maxSize int64
    +		entries []struct {
    +			name string
    +			data string
    +		}
    +		wantErr        bool
    +		expectedErrMsg string
    +	}{
    +		{
    +			name:    "within limit",
    +			maxSize: 1024,
    +			entries: []struct {
    +				name string
    +				data string
    +			}{
    +				{name: "small", data: `"hello"`},
    +			},
    +			wantErr: false,
    +		},
    +		{
    +			name:    "exceeds limit single entry",
    +			maxSize: 10,
    +			entries: []struct {
    +				name string
    +				data string
    +			}{
    +				{name: "large", data: `"this is a string that exceeds the limit"`},
    +			},
    +			wantErr:        true,
    +			expectedErrMsg: "context size limit exceeded",
    +		},
    +		{
    +			name:    "exceeds limit cumulative",
    +			maxSize: 50,
    +			entries: []struct {
    +				name string
    +				data string
    +			}{
    +				{name: "first", data: `"first entry data"`},
    +				{name: "second", data: `"second entry data"`},
    +				{name: "third", data: `"third entry that pushes over"`},
    +			},
    +			wantErr:        true,
    +			expectedErrMsg: "context size limit exceeded",
    +		},
    +		{
    +			name:    "zero limit disables check",
    +			maxSize: 0,
    +			entries: []struct {
    +				name string
    +				data string
    +			}{
    +				{name: "large", data: `"this can be any size when limit is zero"`},
    +			},
    +			wantErr: false,
    +		},
    +	}
    +
    +	for _, tt := range tests {
    +		t.Run(tt.name, func(t *testing.T) {
    +			ctx := &context{
    +				jp:             jp,
    +				jsonRaw:        map[string]interface{}{},
    +				maxContextSize: tt.maxSize,
    +				deferred:       NewDeferredLoaders(),
    +			}
    +
    +			var lastErr error
    +			for _, entry := range tt.entries {
    +				lastErr = ctx.AddContextEntry(entry.name, []byte(entry.data))
    +				if lastErr != nil {
    +					break
    +				}
    +			}
    +
    +			if tt.wantErr {
    +				assert.Error(t, lastErr)
    +				assert.Contains(t, lastErr.Error(), tt.expectedErrMsg)
    +				// Verify it's the correct error type
    +				var sizeErr ContextSizeLimitExceededError
    +				assert.ErrorAs(t, lastErr, &sizeErr)
    +			} else {
    +				assert.NoError(t, lastErr)
    +			}
    +		})
    +	}
    +}
    +
    +func Test_ContextSizeLimitWithReplace(t *testing.T) {
    +	ctx := &context{
    +		jp:             jp,
    +		jsonRaw:        map[string]interface{}{},
    +		maxContextSize: 30,
    +		deferred:       NewDeferredLoaders(),
    +	}
    +
    +	// First entry should succeed
    +	err := ctx.ReplaceContextEntry("var1", []byte(`"a"`))
    +	assert.NoError(t, err)
    +	assert.Greater(t, ctx.contextSize, int64(0))
    +
    +	// Second entry should succeed
    +	err = ctx.ReplaceContextEntry("var2", []byte(`"b"`))
    +	assert.NoError(t, err)
    +
    +	// Large entry that exceeds limit should fail
    +	largeData := []byte(`"this string is definitely larger than 30 bytes total"`)
    +	err = ctx.ReplaceContextEntry("var3", largeData)
    +	assert.Error(t, err)
    +	var sizeErr ContextSizeLimitExceededError
    +	assert.ErrorAs(t, err, &sizeErr)
    +}
    +
    +func Test_ContextSizeLimitExceededError(t *testing.T) {
    +	err := ContextSizeLimitExceededError{Size: 3000, Limit: 2000}
    +	assert.Equal(t, "context size limit exceeded: 3000 bytes exceeds limit of 2000 bytes", err.Error())
    +}
    +
    +// Test_ContextSizeLimitBlocksExponentialAmplification simulates a case where
    +// exponential string doubling via context variables attempts to consume
    +// unbounded memory (e.g., 1KB -> 2KB -> 4KB -> ... -> 256MB).
    +// This test verifies that the context size limit blocks such attacks.
    +func Test_ContextSizeLimitBlocksExponentialAmplification(t *testing.T) {
    +	// Use a small limit to make the test fast (16KB instead of 2MB default)
    +	const testLimit = 16 * 1024
    +
    +	ctx := &context{
    +		jp:             jp,
    +		jsonRaw:        map[string]interface{}{},
    +		maxContextSize: testLimit,
    +		deferred:       NewDeferredLoaders(),
    +	}
    +
    +	// Simulate the pattern:
    +	// l0 = random('[a-zA-Z0-9]{1000}') -> ~1KB
    +	// l1 = join('', [l0, l0]) -> ~2KB
    +	// l2 = join('', [l1, l1]) -> ~4KB
    +	// ... exponential growth until blocked
    +
    +	baseString := strings.Repeat("a", 1000)
    +	currentData := baseString
    +
    +	var lastErr error
    +	level := 0
    +
    +	for level < 20 { // Would reach 1GB if unchecked
    +		jsonData, err := stdjson.Marshal(currentData)
    +		assert.NoError(t, err)
    +
    +		entryName := fmt.Sprintf("l%d", level)
    +		lastErr = ctx.AddContextEntry(entryName, jsonData)
    +
    +		if lastErr != nil {
    +			// Attack blocked by size limit
    +			break
    +		}
    +
    +		// Double the string for next iteration (simulating join('', [prev, prev]))
    +		currentData = currentData + currentData
    +		level++
    +	}
    +
    +	// Verify the attack was blocked before reaching dangerous levels
    +	assert.Error(t, lastErr, "exponential amplification should be blocked")
    +	assert.Less(t, level, 20, "attack should be blocked well before 20 doublings (1GB)")
    +
    +	var sizeErr ContextSizeLimitExceededError
    +	assert.ErrorAs(t, lastErr, &sizeErr)
    +	assert.LessOrEqual(t, sizeErr.Limit, int64(testLimit))
    +}
    
  • pkg/engine/policycontext/policy_context.go+12 11 modified
    @@ -201,37 +201,37 @@ func NewPolicyContext(
     	admissionInfo *kyvernov2.RequestInfo,
     	configuration config.Configuration,
     ) (*PolicyContext, error) {
    -	enginectx := enginectx.NewContext(jp)
    +	engineCtx := enginectx.NewContextWithMaxSize(jp, configuration.GetMaxContextSize())
     
     	if operation != kyvernov1.Delete {
    -		if err := enginectx.AddResource(resource.Object); err != nil {
    +		if err := engineCtx.AddResource(resource.Object); err != nil {
     			return nil, err
     		}
     	} else {
    -		if err := enginectx.AddOldResource(resource.Object); err != nil {
    +		if err := engineCtx.AddOldResource(resource.Object); err != nil {
     			return nil, err
     		}
     	}
    -	if err := enginectx.AddNamespace(resource.GetNamespace()); err != nil {
    +	if err := engineCtx.AddNamespace(resource.GetNamespace()); err != nil {
     		return nil, err
     	}
     
    -	if err := enginectx.AddImageInfos(&resource, configuration); err != nil {
    +	if err := engineCtx.AddImageInfos(&resource, configuration); err != nil {
     		return nil, err
     	}
     
     	if admissionInfo != nil {
    -		if err := enginectx.AddUserInfo(*admissionInfo); err != nil {
    +		if err := engineCtx.AddUserInfo(*admissionInfo); err != nil {
     			return nil, err
     		}
    -		if err := enginectx.AddServiceAccount(admissionInfo.AdmissionUserInfo.Username); err != nil {
    +		if err := engineCtx.AddServiceAccount(admissionInfo.AdmissionUserInfo.Username); err != nil {
     			return nil, err
     		}
     	}
    -	if err := enginectx.AddOperation(string(operation)); err != nil {
    +	if err := engineCtx.AddOperation(string(operation)); err != nil {
     		return nil, err
     	}
    -	policyContext := newPolicyContextWithJsonContext(operation, enginectx)
    +	policyContext := newPolicyContextWithJsonContext(operation, engineCtx)
     	if operation != kyvernov1.Delete {
     		policyContext = policyContext.WithNewResource(resource)
     	} else {
    @@ -251,7 +251,7 @@ func NewPolicyContextFromAdmissionRequest(
     	gvk schema.GroupVersionKind,
     	configuration config.Configuration,
     ) (*PolicyContext, error) {
    -	engineCtx, err := newJsonContext(jp, request, &admissionInfo)
    +	engineCtx, err := newJsonContext(jp, request, &admissionInfo, configuration.GetMaxContextSize())
     	if err != nil {
     		return nil, fmt.Errorf("failed to create policy rule context: %w", err)
     	}
    @@ -277,8 +277,9 @@ func newJsonContext(
     	jp jmespath.Interface,
     	request admissionv1.AdmissionRequest,
     	userRequestInfo *kyvernov2.RequestInfo,
    +	maxContextSize int64,
     ) (enginectx.Interface, error) {
    -	engineCtx := enginectx.NewContext(jp)
    +	engineCtx := enginectx.NewContextWithMaxSize(jp, maxContextSize)
     	if err := engineCtx.AddRequest(request); err != nil {
     		return nil, fmt.Errorf("failed to load incoming request in context: %w", err)
     	}
    
  • pkg/webhooks/handlers/filter_test.go+326 0 added
    @@ -0,0 +1,326 @@
    +package handlers
    +
    +import (
    +	"context"
    +	"testing"
    +	"time"
    +
    +	"github.com/go-logr/logr"
    +	"github.com/kyverno/kyverno/pkg/config"
    +	"github.com/stretchr/testify/assert"
    +	admissionv1 "k8s.io/api/admission/v1"
    +	admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
    +	corev1 "k8s.io/api/core/v1"
    +	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    +	"k8s.io/apimachinery/pkg/runtime/schema"
    +)
    +
    +type mockConfiguration struct {
    +	excluded bool
    +	filtered bool
    +}
    +
    +func (m *mockConfiguration) GetDefaultRegistry() string               { return "" }
    +func (m *mockConfiguration) GetEnableDefaultRegistryMutation() bool   { return false }
    +func (m *mockConfiguration) GetGenerateSuccessEvents() bool           { return false }
    +func (m *mockConfiguration) GetWebhook() config.WebhookConfig         { return config.WebhookConfig{} }
    +func (m *mockConfiguration) GetWebhookAnnotations() map[string]string { return nil }
    +func (m *mockConfiguration) GetWebhookLabels() map[string]string      { return nil }
    +func (m *mockConfiguration) GetMatchConditions() []admissionregistrationv1.MatchCondition {
    +	return nil
    +}
    +func (m *mockConfiguration) Load(*corev1.ConfigMap) {}
    +func (m *mockConfiguration) OnChanged(func())       {}
    +func (m *mockConfiguration) GetUpdateRequestThreshold() int64 {
    +	return 0
    +}
    +func (m *mockConfiguration) GetMaxContextSize() int64 {
    +	return config.DefaultMaxContextSize
    +}
    +
    +func (m *mockConfiguration) IsExcluded(username string, groups []string, roles []string, clusterroles []string) bool {
    +	return m.excluded
    +}
    +
    +func (m *mockConfiguration) ToFilter(kind schema.GroupVersionKind, subresource, namespace, name string) bool {
    +	return m.filtered
    +}
    +
    +func newTestAdmissionRequest(uid string, kind metav1.GroupVersionKind, operation admissionv1.Operation, subResource string) AdmissionRequest {
    +	return AdmissionRequest{
    +		AdmissionRequest: admissionv1.AdmissionRequest{
    +			UID:       "test-uid",
    +			Kind:      kind,
    +			Operation: operation,
    +			Resource: metav1.GroupVersionResource{
    +				Group:    kind.Group,
    +				Version:  kind.Version,
    +				Resource: "pods",
    +			},
    +			SubResource: subResource,
    +			RequestKind: &metav1.GroupVersionKind{
    +				Group:   kind.Group,
    +				Version: kind.Version,
    +				Kind:    kind.Kind,
    +			},
    +			RequestResource: &metav1.GroupVersionResource{
    +				Group:    kind.Group,
    +				Version:  kind.Version,
    +				Resource: "pods",
    +			},
    +		},
    +	}
    +}
    +
    +func Test_WithFilter(t *testing.T) {
    +	tests := []struct {
    +		name            string
    +		config          config.Configuration
    +		request         AdmissionRequest
    +		wantAllowed     bool
    +		wantInnerCalled bool
    +	}{{
    +		name: "excluded by user exclusion",
    +		config: &mockConfiguration{
    +			excluded: true,
    +		},
    +		request: newTestAdmissionRequest("test-uid", metav1.GroupVersionKind{
    +			Group:   "",
    +			Version: "v1",
    +			Kind:    "Pod",
    +		}, admissionv1.Create, ""),
    +		wantAllowed:     true,
    +		wantInnerCalled: false,
    +	}, {
    +		name: "filtered by resource filter",
    +		config: &mockConfiguration{
    +			filtered: true,
    +		},
    +		request: newTestAdmissionRequest("test-uid", metav1.GroupVersionKind{
    +			Group:   "",
    +			Version: "v1",
    +			Kind:    "Pod",
    +		}, admissionv1.Create, ""),
    +		wantAllowed:     true,
    +		wantInnerCalled: false,
    +	}, {
    +		name:   "filtered kyverno resource - AdmissionReport",
    +		config: &mockConfiguration{},
    +		request: newTestAdmissionRequest("test-uid", metav1.GroupVersionKind{
    +			Group:   "kyverno.io",
    +			Version: "v1alpha2",
    +			Kind:    "AdmissionReport",
    +		}, admissionv1.Create, ""),
    +		wantAllowed:     true,
    +		wantInnerCalled: false,
    +	}, {
    +		name:   "filtered kyverno resource - ClusterAdmissionReport",
    +		config: &mockConfiguration{},
    +		request: newTestAdmissionRequest("test-uid", metav1.GroupVersionKind{
    +			Group:   "kyverno.io",
    +			Version: "v1alpha2",
    +			Kind:    "ClusterAdmissionReport",
    +		}, admissionv1.Create, ""),
    +		wantAllowed:     true,
    +		wantInnerCalled: false,
    +	}, {
    +		name:   "filtered kyverno resource - BackgroundScanReport",
    +		config: &mockConfiguration{},
    +		request: newTestAdmissionRequest("test-uid", metav1.GroupVersionKind{
    +			Group:   "kyverno.io",
    +			Version: "v1alpha2",
    +			Kind:    "BackgroundScanReport",
    +		}, admissionv1.Create, ""),
    +		wantAllowed:     true,
    +		wantInnerCalled: false,
    +	}, {
    +		name:   "filtered kyverno resource - UpdateRequest",
    +		config: &mockConfiguration{},
    +		request: newTestAdmissionRequest("test-uid", metav1.GroupVersionKind{
    +			Group:   "kyverno.io",
    +			Version: "v1beta1",
    +			Kind:    "UpdateRequest",
    +		}, admissionv1.Create, ""),
    +		wantAllowed:     true,
    +		wantInnerCalled: false,
    +	}, {
    +		name:   "not filtered - regular Pod",
    +		config: &mockConfiguration{},
    +		request: newTestAdmissionRequest("test-uid", metav1.GroupVersionKind{
    +			Group:   "",
    +			Version: "v1",
    +			Kind:    "Pod",
    +		}, admissionv1.Create, ""),
    +		wantAllowed:     false,
    +		wantInnerCalled: true,
    +	}, {
    +		name:   "not filtered - ClusterPolicy (not excluded by ExcludeKyvernoResources)",
    +		config: &mockConfiguration{},
    +		request: newTestAdmissionRequest("test-uid", metav1.GroupVersionKind{
    +			Group:   "kyverno.io",
    +			Version: "v1",
    +			Kind:    "ClusterPolicy",
    +		}, admissionv1.Create, ""),
    +		wantAllowed:     false,
    +		wantInnerCalled: true,
    +	}}
    +	for _, tt := range tests {
    +		t.Run(tt.name, func(t *testing.T) {
    +			innerCalled := false
    +			inner := func(ctx context.Context, logger logr.Logger, request AdmissionRequest, startTime time.Time) AdmissionResponse {
    +				innerCalled = true
    +				return AdmissionResponse{Allowed: false}
    +			}
    +			handler := AdmissionHandler(inner).WithFilter(tt.config)
    +			response := handler(context.TODO(), logr.Discard(), tt.request, time.Now())
    +			assert.Equal(t, tt.wantAllowed, response.Allowed)
    +			assert.Equal(t, tt.wantInnerCalled, innerCalled)
    +		})
    +	}
    +}
    +
    +func Test_WithOperationFilter(t *testing.T) {
    +	tests := []struct {
    +		name            string
    +		operations      []admissionv1.Operation
    +		requestOp       admissionv1.Operation
    +		wantAllowed     bool
    +		wantInnerCalled bool
    +	}{{
    +		name:            "allowed operation - CREATE",
    +		operations:      []admissionv1.Operation{admissionv1.Create},
    +		requestOp:       admissionv1.Create,
    +		wantAllowed:     false,
    +		wantInnerCalled: true,
    +	}, {
    +		name:            "allowed operation - UPDATE",
    +		operations:      []admissionv1.Operation{admissionv1.Update},
    +		requestOp:       admissionv1.Update,
    +		wantAllowed:     false,
    +		wantInnerCalled: true,
    +	}, {
    +		name:            "allowed operation - DELETE",
    +		operations:      []admissionv1.Operation{admissionv1.Delete},
    +		requestOp:       admissionv1.Delete,
    +		wantAllowed:     false,
    +		wantInnerCalled: true,
    +	}, {
    +		name:            "allowed multiple operations - CREATE and UPDATE",
    +		operations:      []admissionv1.Operation{admissionv1.Create, admissionv1.Update},
    +		requestOp:       admissionv1.Update,
    +		wantAllowed:     false,
    +		wantInnerCalled: true,
    +	}, {
    +		name:            "filtered operation - UPDATE not in allowed list",
    +		operations:      []admissionv1.Operation{admissionv1.Create},
    +		requestOp:       admissionv1.Update,
    +		wantAllowed:     true,
    +		wantInnerCalled: false,
    +	}, {
    +		name:            "filtered operation - DELETE not in allowed list",
    +		operations:      []admissionv1.Operation{admissionv1.Create, admissionv1.Update},
    +		requestOp:       admissionv1.Delete,
    +		wantAllowed:     true,
    +		wantInnerCalled: false,
    +	}, {
    +		name:            "empty operations list filters all",
    +		operations:      []admissionv1.Operation{},
    +		requestOp:       admissionv1.Create,
    +		wantAllowed:     true,
    +		wantInnerCalled: false,
    +	}}
    +	for _, tt := range tests {
    +		t.Run(tt.name, func(t *testing.T) {
    +			innerCalled := false
    +			inner := func(ctx context.Context, logger logr.Logger, request AdmissionRequest, startTime time.Time) AdmissionResponse {
    +				innerCalled = true
    +				return AdmissionResponse{Allowed: false}
    +			}
    +			request := newTestAdmissionRequest("test-uid", metav1.GroupVersionKind{
    +				Group:   "",
    +				Version: "v1",
    +				Kind:    "Pod",
    +			}, tt.requestOp, "")
    +			handler := AdmissionHandler(inner).WithOperationFilter(tt.operations...)
    +			response := handler(context.TODO(), logr.Discard(), request, time.Now())
    +			assert.Equal(t, tt.wantAllowed, response.Allowed)
    +			assert.Equal(t, tt.wantInnerCalled, innerCalled)
    +		})
    +	}
    +}
    +
    +func Test_WithSubResourceFilter(t *testing.T) {
    +	tests := []struct {
    +		name            string
    +		subresources    []string
    +		requestSubRes   string
    +		wantAllowed     bool
    +		wantInnerCalled bool
    +	}{{
    +		name:            "allowed subresource - status",
    +		subresources:    []string{"status"},
    +		requestSubRes:   "status",
    +		wantAllowed:     false,
    +		wantInnerCalled: true,
    +	}, {
    +		name:            "allowed subresource - scale",
    +		subresources:    []string{"scale"},
    +		requestSubRes:   "scale",
    +		wantAllowed:     false,
    +		wantInnerCalled: true,
    +	}, {
    +		name:            "allowed multiple subresources",
    +		subresources:    []string{"status", "scale"},
    +		requestSubRes:   "scale",
    +		wantAllowed:     false,
    +		wantInnerCalled: true,
    +	}, {
    +		name:            "empty request subresource is always allowed",
    +		subresources:    []string{"status"},
    +		requestSubRes:   "",
    +		wantAllowed:     false,
    +		wantInnerCalled: true,
    +	}, {
    +		name:            "empty subresource list - empty request allowed",
    +		subresources:    []string{},
    +		requestSubRes:   "",
    +		wantAllowed:     false,
    +		wantInnerCalled: true,
    +	}, {
    +		name:            "filtered subresource - scale not in allowed list",
    +		subresources:    []string{"status"},
    +		requestSubRes:   "scale",
    +		wantAllowed:     true,
    +		wantInnerCalled: false,
    +	}, {
    +		name:            "filtered subresource - eviction not in allowed list",
    +		subresources:    []string{"status", "scale"},
    +		requestSubRes:   "eviction",
    +		wantAllowed:     true,
    +		wantInnerCalled: false,
    +	}, {
    +		name:            "empty subresource list filters all non-empty requests",
    +		subresources:    []string{},
    +		requestSubRes:   "status",
    +		wantAllowed:     true,
    +		wantInnerCalled: false,
    +	}}
    +	for _, tt := range tests {
    +		t.Run(tt.name, func(t *testing.T) {
    +			innerCalled := false
    +			inner := func(ctx context.Context, logger logr.Logger, request AdmissionRequest, startTime time.Time) AdmissionResponse {
    +				innerCalled = true
    +				return AdmissionResponse{Allowed: false}
    +			}
    +			request := newTestAdmissionRequest("test-uid", metav1.GroupVersionKind{
    +				Group:   "",
    +				Version: "v1",
    +				Kind:    "Pod",
    +			}, admissionv1.Create, tt.requestSubRes)
    +			handler := AdmissionHandler(inner).WithSubResourceFilter(tt.subresources...)
    +			response := handler(context.TODO(), logr.Discard(), request, time.Now())
    +			assert.Equal(t, tt.wantAllowed, response.Allowed)
    +			assert.Equal(t, tt.wantInnerCalled, innerCalled)
    +		})
    +	}
    +}
    
f5617f609205

Merge commit from fork (#14846)

https://github.com/kyverno/kyvernoshutingJan 26, 2026via ghsa
10 files changed · +739 11
  • charts/kyverno/README.md+1 0 modified
    @@ -300,6 +300,7 @@ The chart values are organised per component.
     | config.excludeUsernames | list | `[]` | Exclude usernames |
     | config.generateSuccessEvents | bool | `false` | Generate success events. |
     | config.matchConditions | list | `[]` | Defines match conditions to set on webhook configurations (requires Kubernetes 1.27+). |
    +| config.maxContextSize | string | 2Mi | Maximum cumulative size of context data during policy evaluation. Supports Kubernetes quantity format (e.g., 100Mi, 2Gi) or plain bytes (e.g., 2097152). Limits memory used by context variables to prevent unbounded growth. Increase if policies legitimately need large context data (e.g., processing large ConfigMaps). Set to 0 to disable the limit (not recommended for production). |
     | config.name | string | `nil` | The configmap name (required if `create` is `false`). |
     | config.preserve | bool | `true` | Preserve the configmap settings during upgrade. |
     | config.resourceFilters | list | See [values.yaml](values.yaml) | Resource types to be skipped by the Kyverno policy engine. Make sure to surround each entry in quotes so that it doesn't get parsed as a nested YAML list. These are joined together without spaces, run through `tpl`, and the result is set in the config map. |
    
  • charts/kyverno/templates/config/configmap.yaml+3 0 modified
    @@ -54,4 +54,7 @@ data:
       {{- with .Values.config.matchConditions }}
       matchConditions: {{ toJson . | quote }}
       {{- end }}
    +  {{- with .Values.config.maxContextSize }}
    +  maxContextSize: {{ . | quote }}
    +  {{- end }}
     {{- end -}}
    
  • charts/kyverno/values.yaml+8 0 modified
    @@ -252,6 +252,14 @@ config:
       # -- Generate success events.
       generateSuccessEvents: false
     
    +  # -- Maximum cumulative size of context data during policy evaluation.
    +  # Supports Kubernetes quantity format (e.g., 100Mi, 2Gi) or plain bytes (e.g., 2097152).
    +  # Limits memory used by context variables to prevent unbounded growth.
    +  # Increase if policies legitimately need large context data (e.g., processing large ConfigMaps).
    +  # Set to 0 to disable the limit (not recommended for production).
    +  # @default -- 2Mi
    +  maxContextSize: ~
    +
       # -- Resource types to be skipped by the Kyverno policy engine.
       # Make sure to surround each entry in quotes so that it doesn't get parsed as a nested YAML list.
       # These are joined together without spaces, run through `tpl`, and the result is set in the config map.
    
  • pkg/config/config.go+29 0 modified
    @@ -11,6 +11,7 @@ import (
     	osutils "github.com/kyverno/kyverno/pkg/utils/os"
     	admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
     	corev1 "k8s.io/api/core/v1"
    +	"k8s.io/apimachinery/pkg/api/resource"
     	"k8s.io/apimachinery/pkg/runtime/schema"
     )
     
    @@ -117,10 +118,14 @@ const (
     	webhookLabels                 = "webhookLabels"
     	matchConditions               = "matchConditions"
     	updateRequestThreshold        = "updateRequestThreshold"
    +	maxContextSize                = "maxContextSize"
     )
     
     const UpdateRequestThreshold = 1000
     
    +// DefaultMaxContextSize is the default maximum size of context data in bytes (2MB)
    +const DefaultMaxContextSize int64 = 2 * 1024 * 1024
    +
     var (
     	// kyvernoNamespace is the Kyverno namespace
     	kyvernoNamespace = osutils.GetEnvWithFallback("KYVERNO_NAMESPACE", "kyverno")
    @@ -208,6 +213,8 @@ type Configuration interface {
     	OnChanged(func())
     	// GetUpdateRequestThreshold gets the threshold limit for the total number of updaterequests
     	GetUpdateRequestThreshold() int64
    +	// GetMaxContextSize gets the maximum context size in bytes for policy evaluation
    +	GetMaxContextSize() int64
     }
     
     // configuration stores the configuration
    @@ -226,6 +233,7 @@ type configuration struct {
     	mux                           sync.RWMutex
     	callbacks                     []func()
     	updateRequestThreshold        int64
    +	maxContextSize                int64
     }
     
     type match struct {
    @@ -360,6 +368,12 @@ func (cd *configuration) GetUpdateRequestThreshold() int64 {
     	return cd.updateRequestThreshold
     }
     
    +func (cd *configuration) GetMaxContextSize() int64 {
    +	cd.mux.RLock()
    +	defer cd.mux.RUnlock()
    +	return cd.maxContextSize
    +}
    +
     func (cd *configuration) Load(cm *corev1.ConfigMap) {
     	if cm != nil {
     		cd.load(cm)
    @@ -534,6 +548,20 @@ func (cd *configuration) load(cm *corev1.ConfigMap) {
     			logger.V(2).Info("enableDefaultRegistryMutation configured")
     		}
     	}
    +	// load maxContextSize (supports Kubernetes quantity format: 100Mi, 2Gi, etc.)
    +	cd.maxContextSize = DefaultMaxContextSize
    +	if maxCtxSizeStr, ok := data[maxContextSize]; ok {
    +		logger := logger.WithValues("maxContextSize", maxCtxSizeStr)
    +		quantity, err := resource.ParseQuantity(maxCtxSizeStr)
    +		if err != nil {
    +			logger.Error(err, "maxContextSize is not a valid quantity (use formats like 100Mi, 2Gi, or plain bytes)")
    +		} else {
    +			cd.maxContextSize = quantity.Value()
    +			logger.V(2).Info("maxContextSize configured", "bytes", cd.maxContextSize)
    +		}
    +	} else {
    +		logger.V(2).Info("maxContextSize not set, using default", "default", DefaultMaxContextSize)
    +	}
     }
     
     func (cd *configuration) unload() {
    @@ -549,6 +577,7 @@ func (cd *configuration) unload() {
     	cd.webhook = WebhookConfig{}
     	cd.webhookAnnotations = nil
     	cd.webhookLabels = nil
    +	cd.maxContextSize = DefaultMaxContextSize
     	logger.V(2).Info("configuration unloaded")
     }
     
    
  • pkg/config/config_test.go+123 0 added
    @@ -0,0 +1,123 @@
    +package config
    +
    +import (
    +	"testing"
    +
    +	"github.com/stretchr/testify/assert"
    +	corev1 "k8s.io/api/core/v1"
    +	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    +)
    +
    +func TestConfiguration_GetMaxContextSize_Default(t *testing.T) {
    +	cfg := NewDefaultConfiguration(false)
    +	// Load with nil configmap to trigger unload which sets defaults
    +	cfg.Load(nil)
    +
    +	assert.Equal(t, DefaultMaxContextSize, cfg.GetMaxContextSize())
    +}
    +
    +func TestConfiguration_GetMaxContextSize_FromConfigMap(t *testing.T) {
    +	cfg := NewDefaultConfiguration(false)
    +
    +	cm := &corev1.ConfigMap{
    +		ObjectMeta: metav1.ObjectMeta{
    +			Name:      "kyverno",
    +			Namespace: "kyverno",
    +		},
    +		Data: map[string]string{
    +			"maxContextSize": "4194304", // 4MB
    +		},
    +	}
    +
    +	cfg.Load(cm)
    +
    +	assert.Equal(t, int64(4194304), cfg.GetMaxContextSize())
    +}
    +
    +func TestConfiguration_GetMaxContextSize_InvalidValue(t *testing.T) {
    +	cfg := NewDefaultConfiguration(false)
    +
    +	cm := &corev1.ConfigMap{
    +		ObjectMeta: metav1.ObjectMeta{
    +			Name:      "kyverno",
    +			Namespace: "kyverno",
    +		},
    +		Data: map[string]string{
    +			"maxContextSize": "invalid",
    +		},
    +	}
    +
    +	cfg.Load(cm)
    +
    +	// Should fall back to default on parse error
    +	assert.Equal(t, DefaultMaxContextSize, cfg.GetMaxContextSize())
    +}
    +
    +func TestConfiguration_GetMaxContextSize_NotSet(t *testing.T) {
    +	cfg := NewDefaultConfiguration(false)
    +
    +	cm := &corev1.ConfigMap{
    +		ObjectMeta: metav1.ObjectMeta{
    +			Name:      "kyverno",
    +			Namespace: "kyverno",
    +		},
    +		Data: map[string]string{},
    +	}
    +
    +	cfg.Load(cm)
    +
    +	// Should use default when not set
    +	assert.Equal(t, DefaultMaxContextSize, cfg.GetMaxContextSize())
    +}
    +
    +func TestConfiguration_GetMaxContextSize_ZeroDisablesLimit(t *testing.T) {
    +	cfg := NewDefaultConfiguration(false)
    +
    +	cm := &corev1.ConfigMap{
    +		ObjectMeta: metav1.ObjectMeta{
    +			Name:      "kyverno",
    +			Namespace: "kyverno",
    +		},
    +		Data: map[string]string{
    +			"maxContextSize": "0",
    +		},
    +	}
    +
    +	cfg.Load(cm)
    +
    +	// Zero should be valid and disable the limit
    +	assert.Equal(t, int64(0), cfg.GetMaxContextSize())
    +}
    +
    +func TestConfiguration_GetMaxContextSize_KubernetesQuantityFormat(t *testing.T) {
    +	tests := []struct {
    +		name     string
    +		value    string
    +		expected int64
    +	}{
    +		{"100Mi", "100Mi", 100 * 1024 * 1024},
    +		{"4Mi", "4Mi", 4 * 1024 * 1024},
    +		{"1Gi", "1Gi", 1024 * 1024 * 1024},
    +		{"500Ki", "500Ki", 500 * 1024},
    +	}
    +
    +	for _, tt := range tests {
    +		t.Run(tt.name, func(t *testing.T) {
    +			cfg := NewDefaultConfiguration(false)
    +
    +			cm := &corev1.ConfigMap{
    +				ObjectMeta: metav1.ObjectMeta{
    +					Name:      "kyverno",
    +					Namespace: "kyverno",
    +				},
    +				Data: map[string]string{
    +					"maxContextSize": tt.value,
    +				},
    +			}
    +
    +			cfg.Load(cm)
    +
    +			assert.Equal(t, tt.expected, cfg.GetMaxContextSize())
    +		})
    +	}
    +}
    
  • pkg/config/mocks/mock_config.go+14 0 modified
    @@ -93,6 +93,20 @@ func (mr *MockConfigurationMockRecorder) GetMatchConditions() *gomock.Call {
     	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMatchConditions", reflect.TypeOf((*MockConfiguration)(nil).GetMatchConditions))
     }
     
    +// GetMaxContextSize mocks base method.
    +func (m *MockConfiguration) GetMaxContextSize() int64 {
    +	m.ctrl.T.Helper()
    +	ret := m.ctrl.Call(m, "GetMaxContextSize")
    +	ret0, _ := ret[0].(int64)
    +	return ret0
    +}
    +
    +// GetMaxContextSize indicates an expected call of GetMaxContextSize.
    +func (mr *MockConfigurationMockRecorder) GetMaxContextSize() *gomock.Call {
    +	mr.mock.ctrl.T.Helper()
    +	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMaxContextSize", reflect.TypeOf((*MockConfiguration)(nil).GetMaxContextSize))
    +}
    +
     // GetUpdateRequestThreshold mocks base method.
     func (m *MockConfiguration) GetUpdateRequestThreshold() int64 {
     	m.ctrl.T.Helper()
    
  • pkg/engine/context/context.go+46 0 modified
    @@ -114,6 +114,19 @@ type Interface interface {
     	addJSON(dataMap map[string]interface{}, overwriteMaps bool) error
     }
     
    +// DefaultMaxContextSize is the default maximum size of context data in bytes (2MB)
    +const DefaultMaxContextSize = 2 * 1024 * 1024
    +
    +// ContextSizeLimitExceededError is returned when context size exceeds the limit
    +type ContextSizeLimitExceededError struct {
    +	Size  int64
    +	Limit int64
    +}
    +
    +func (e ContextSizeLimitExceededError) Error() string {
    +	return fmt.Sprintf("context size limit exceeded: %d bytes exceeds limit of %d bytes", e.Size, e.Limit)
    +}
    +
     // Context stores the data resources as JSON
     type context struct {
     	jp                 jmespath.Interface
    @@ -122,6 +135,8 @@ type context struct {
     	images             map[string]map[string]apiutils.ImageInfo
     	operation          kyvernov1.AdmissionOperation
     	deferred           DeferredLoaders
    +	contextSize        int64
    +	maxContextSize     int64
     }
     
     // NewContext returns a new context
    @@ -136,6 +151,18 @@ func NewContextFromRaw(jp jmespath.Interface, raw map[string]interface{}) Interf
     		jsonRaw:            raw,
     		jsonRawCheckpoints: make([]map[string]interface{}, 0),
     		deferred:           NewDeferredLoaders(),
    +		maxContextSize:     DefaultMaxContextSize,
    +	}
    +}
    +
    +// NewContextWithMaxSize returns a new context with a specified maximum context size
    +func NewContextWithMaxSize(jp jmespath.Interface, maxSize int64) Interface {
    +	return &context{
    +		jp:                 jp,
    +		jsonRaw:            map[string]interface{}{},
    +		jsonRawCheckpoints: make([]map[string]interface{}, 0),
    +		deferred:           NewDeferredLoaders(),
    +		maxContextSize:     maxSize,
     	}
     }
     
    @@ -187,15 +214,22 @@ func (ctx *context) AddVariable(key string, value interface{}) error {
     }
     
     func (ctx *context) AddContextEntry(name string, dataRaw []byte) error {
    +	if err := ctx.checkContextSizeLimit(int64(len(dataRaw))); err != nil {
    +		return err
    +	}
     	var data interface{}
     	if err := json.Unmarshal(dataRaw, &data); err != nil {
     		logger.Error(err, "failed to unmarshal the resource")
     		return err
     	}
    +	ctx.contextSize += int64(len(dataRaw))
     	return addToContext(ctx, data, false, name)
     }
     
     func (ctx *context) ReplaceContextEntry(name string, dataRaw []byte) error {
    +	if err := ctx.checkContextSizeLimit(int64(len(dataRaw))); err != nil {
    +		return err
    +	}
     	var data interface{}
     	if err := json.Unmarshal(dataRaw, &data); err != nil {
     		logger.Error(err, "failed to unmarshal the resource")
    @@ -206,6 +240,7 @@ func (ctx *context) ReplaceContextEntry(name string, dataRaw []byte) error {
     		logger.Error(err, "unable to replace context entry", "context entry name", name)
     		return err
     	}
    +	ctx.contextSize += int64(len(dataRaw))
     	return addToContext(ctx, data, false, name)
     }
     
    @@ -464,3 +499,14 @@ func (ctx *context) AddDeferredLoader(dl DeferredLoader) error {
     	ctx.deferred.Add(dl, len(ctx.jsonRawCheckpoints))
     	return nil
     }
    +
    +// checkContextSizeLimit checks if adding additionalSize bytes would exceed the context size limit
    +func (ctx *context) checkContextSizeLimit(additionalSize int64) error {
    +	if ctx.maxContextSize > 0 && ctx.contextSize+additionalSize > ctx.maxContextSize {
    +		return ContextSizeLimitExceededError{
    +			Size:  ctx.contextSize + additionalSize,
    +			Limit: ctx.maxContextSize,
    +		}
    +	}
    +	return nil
    +}
    
  • pkg/engine/context/context_test.go+177 0 modified
    @@ -1,7 +1,10 @@
     package context
     
     import (
    +	stdjson "encoding/json"
    +	"fmt"
     	"reflect"
    +	"strings"
     	"testing"
     
     	urkyverno "github.com/kyverno/kyverno/api/kyverno/v2"
    @@ -284,3 +287,177 @@ func Test_ImageInfoLoader_OnDirectCall(t *testing.T) {
     	imageinfos := newctx.ImageInfo()
     	assert.Equal(t, imageinfos["containers"]["test_container"].Name, "nginx")
     }
    +
    +func Test_ContextSizeLimit(t *testing.T) {
    +	tests := []struct {
    +		name    string
    +		maxSize int64
    +		entries []struct {
    +			name string
    +			data string
    +		}
    +		wantErr        bool
    +		expectedErrMsg string
    +	}{
    +		{
    +			name:    "within limit",
    +			maxSize: 1024,
    +			entries: []struct {
    +				name string
    +				data string
    +			}{
    +				{name: "small", data: `"hello"`},
    +			},
    +			wantErr: false,
    +		},
    +		{
    +			name:    "exceeds limit single entry",
    +			maxSize: 10,
    +			entries: []struct {
    +				name string
    +				data string
    +			}{
    +				{name: "large", data: `"this is a string that exceeds the limit"`},
    +			},
    +			wantErr:        true,
    +			expectedErrMsg: "context size limit exceeded",
    +		},
    +		{
    +			name:    "exceeds limit cumulative",
    +			maxSize: 50,
    +			entries: []struct {
    +				name string
    +				data string
    +			}{
    +				{name: "first", data: `"first entry data"`},
    +				{name: "second", data: `"second entry data"`},
    +				{name: "third", data: `"third entry that pushes over"`},
    +			},
    +			wantErr:        true,
    +			expectedErrMsg: "context size limit exceeded",
    +		},
    +		{
    +			name:    "zero limit disables check",
    +			maxSize: 0,
    +			entries: []struct {
    +				name string
    +				data string
    +			}{
    +				{name: "large", data: `"this can be any size when limit is zero"`},
    +			},
    +			wantErr: false,
    +		},
    +	}
    +
    +	for _, tt := range tests {
    +		t.Run(tt.name, func(t *testing.T) {
    +			ctx := &context{
    +				jp:             jp,
    +				jsonRaw:        map[string]interface{}{},
    +				maxContextSize: tt.maxSize,
    +				deferred:       NewDeferredLoaders(),
    +			}
    +
    +			var lastErr error
    +			for _, entry := range tt.entries {
    +				lastErr = ctx.AddContextEntry(entry.name, []byte(entry.data))
    +				if lastErr != nil {
    +					break
    +				}
    +			}
    +
    +			if tt.wantErr {
    +				assert.Error(t, lastErr)
    +				assert.Contains(t, lastErr.Error(), tt.expectedErrMsg)
    +				// Verify it's the correct error type
    +				var sizeErr ContextSizeLimitExceededError
    +				assert.ErrorAs(t, lastErr, &sizeErr)
    +			} else {
    +				assert.NoError(t, lastErr)
    +			}
    +		})
    +	}
    +}
    +
    +func Test_ContextSizeLimitWithReplace(t *testing.T) {
    +	ctx := &context{
    +		jp:             jp,
    +		jsonRaw:        map[string]interface{}{},
    +		maxContextSize: 30,
    +		deferred:       NewDeferredLoaders(),
    +	}
    +
    +	// First entry should succeed
    +	err := ctx.ReplaceContextEntry("var1", []byte(`"a"`))
    +	assert.NoError(t, err)
    +	assert.Greater(t, ctx.contextSize, int64(0))
    +
    +	// Second entry should succeed
    +	err = ctx.ReplaceContextEntry("var2", []byte(`"b"`))
    +	assert.NoError(t, err)
    +
    +	// Large entry that exceeds limit should fail
    +	largeData := []byte(`"this string is definitely larger than 30 bytes total"`)
    +	err = ctx.ReplaceContextEntry("var3", largeData)
    +	assert.Error(t, err)
    +	var sizeErr ContextSizeLimitExceededError
    +	assert.ErrorAs(t, err, &sizeErr)
    +}
    +
    +func Test_ContextSizeLimitExceededError(t *testing.T) {
    +	err := ContextSizeLimitExceededError{Size: 3000, Limit: 2000}
    +	assert.Equal(t, "context size limit exceeded: 3000 bytes exceeds limit of 2000 bytes", err.Error())
    +}
    +
    +// Test_ContextSizeLimitBlocksExponentialAmplification simulates a case where
    +// exponential string doubling via context variables attempts to consume
    +// unbounded memory (e.g., 1KB -> 2KB -> 4KB -> ... -> 256MB).
    +// This test verifies that the context size limit blocks such attacks.
    +func Test_ContextSizeLimitBlocksExponentialAmplification(t *testing.T) {
    +	// Use a small limit to make the test fast (16KB instead of 2MB default)
    +	const testLimit = 16 * 1024
    +
    +	ctx := &context{
    +		jp:             jp,
    +		jsonRaw:        map[string]interface{}{},
    +		maxContextSize: testLimit,
    +		deferred:       NewDeferredLoaders(),
    +	}
    +
    +	// Simulate the pattern:
    +	// l0 = random('[a-zA-Z0-9]{1000}') -> ~1KB
    +	// l1 = join('', [l0, l0]) -> ~2KB
    +	// l2 = join('', [l1, l1]) -> ~4KB
    +	// ... exponential growth until blocked
    +
    +	baseString := strings.Repeat("a", 1000)
    +	currentData := baseString
    +
    +	var lastErr error
    +	level := 0
    +
    +	for level < 20 { // Would reach 1GB if unchecked
    +		jsonData, err := stdjson.Marshal(currentData)
    +		assert.NoError(t, err)
    +
    +		entryName := fmt.Sprintf("l%d", level)
    +		lastErr = ctx.AddContextEntry(entryName, jsonData)
    +
    +		if lastErr != nil {
    +			// Attack blocked by size limit
    +			break
    +		}
    +
    +		// Double the string for next iteration (simulating join('', [prev, prev]))
    +		currentData = currentData + currentData
    +		level++
    +	}
    +
    +	// Verify the attack was blocked before reaching dangerous levels
    +	assert.Error(t, lastErr, "exponential amplification should be blocked")
    +	assert.Less(t, level, 20, "attack should be blocked well before 20 doublings (1GB)")
    +
    +	var sizeErr ContextSizeLimitExceededError
    +	assert.ErrorAs(t, lastErr, &sizeErr)
    +	assert.LessOrEqual(t, sizeErr.Limit, int64(testLimit))
    +}
    
  • pkg/engine/policycontext/policy_context.go+12 11 modified
    @@ -201,37 +201,37 @@ func NewPolicyContext(
     	admissionInfo *kyvernov2.RequestInfo,
     	configuration config.Configuration,
     ) (*PolicyContext, error) {
    -	enginectx := enginectx.NewContext(jp)
    +	engineCtx := enginectx.NewContextWithMaxSize(jp, configuration.GetMaxContextSize())
     
     	if operation != kyvernov1.Delete {
    -		if err := enginectx.AddResource(resource.Object); err != nil {
    +		if err := engineCtx.AddResource(resource.Object); err != nil {
     			return nil, err
     		}
     	} else {
    -		if err := enginectx.AddOldResource(resource.Object); err != nil {
    +		if err := engineCtx.AddOldResource(resource.Object); err != nil {
     			return nil, err
     		}
     	}
    -	if err := enginectx.AddNamespace(resource.GetNamespace()); err != nil {
    +	if err := engineCtx.AddNamespace(resource.GetNamespace()); err != nil {
     		return nil, err
     	}
     
    -	if err := enginectx.AddImageInfos(&resource, configuration); err != nil {
    +	if err := engineCtx.AddImageInfos(&resource, configuration); err != nil {
     		return nil, err
     	}
     
     	if admissionInfo != nil {
    -		if err := enginectx.AddUserInfo(*admissionInfo); err != nil {
    +		if err := engineCtx.AddUserInfo(*admissionInfo); err != nil {
     			return nil, err
     		}
    -		if err := enginectx.AddServiceAccount(admissionInfo.AdmissionUserInfo.Username); err != nil {
    +		if err := engineCtx.AddServiceAccount(admissionInfo.AdmissionUserInfo.Username); err != nil {
     			return nil, err
     		}
     	}
    -	if err := enginectx.AddOperation(string(operation)); err != nil {
    +	if err := engineCtx.AddOperation(string(operation)); err != nil {
     		return nil, err
     	}
    -	policyContext := newPolicyContextWithJsonContext(operation, enginectx)
    +	policyContext := newPolicyContextWithJsonContext(operation, engineCtx)
     	if operation != kyvernov1.Delete {
     		policyContext = policyContext.WithNewResource(resource)
     	} else {
    @@ -251,7 +251,7 @@ func NewPolicyContextFromAdmissionRequest(
     	gvk schema.GroupVersionKind,
     	configuration config.Configuration,
     ) (*PolicyContext, error) {
    -	engineCtx, err := newJsonContext(jp, request, &admissionInfo)
    +	engineCtx, err := newJsonContext(jp, request, &admissionInfo, configuration.GetMaxContextSize())
     	if err != nil {
     		return nil, fmt.Errorf("failed to create policy rule context: %w", err)
     	}
    @@ -277,8 +277,9 @@ func newJsonContext(
     	jp jmespath.Interface,
     	request admissionv1.AdmissionRequest,
     	userRequestInfo *kyvernov2.RequestInfo,
    +	maxContextSize int64,
     ) (enginectx.Interface, error) {
    -	engineCtx := enginectx.NewContext(jp)
    +	engineCtx := enginectx.NewContextWithMaxSize(jp, maxContextSize)
     	if err := engineCtx.AddRequest(request); err != nil {
     		return nil, fmt.Errorf("failed to load incoming request in context: %w", err)
     	}
    
  • pkg/webhooks/handlers/filter_test.go+326 0 added
    @@ -0,0 +1,326 @@
    +package handlers
    +
    +import (
    +	"context"
    +	"testing"
    +	"time"
    +
    +	"github.com/go-logr/logr"
    +	"github.com/kyverno/kyverno/pkg/config"
    +	"github.com/stretchr/testify/assert"
    +	admissionv1 "k8s.io/api/admission/v1"
    +	admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
    +	corev1 "k8s.io/api/core/v1"
    +	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    +	"k8s.io/apimachinery/pkg/runtime/schema"
    +)
    +
    +type mockConfiguration struct {
    +	excluded bool
    +	filtered bool
    +}
    +
    +func (m *mockConfiguration) GetDefaultRegistry() string               { return "" }
    +func (m *mockConfiguration) GetEnableDefaultRegistryMutation() bool   { return false }
    +func (m *mockConfiguration) GetGenerateSuccessEvents() bool           { return false }
    +func (m *mockConfiguration) GetWebhook() config.WebhookConfig         { return config.WebhookConfig{} }
    +func (m *mockConfiguration) GetWebhookAnnotations() map[string]string { return nil }
    +func (m *mockConfiguration) GetWebhookLabels() map[string]string      { return nil }
    +func (m *mockConfiguration) GetMatchConditions() []admissionregistrationv1.MatchCondition {
    +	return nil
    +}
    +func (m *mockConfiguration) Load(*corev1.ConfigMap) {}
    +func (m *mockConfiguration) OnChanged(func())       {}
    +func (m *mockConfiguration) GetUpdateRequestThreshold() int64 {
    +	return 0
    +}
    +func (m *mockConfiguration) GetMaxContextSize() int64 {
    +	return config.DefaultMaxContextSize
    +}
    +
    +func (m *mockConfiguration) IsExcluded(username string, groups []string, roles []string, clusterroles []string) bool {
    +	return m.excluded
    +}
    +
    +func (m *mockConfiguration) ToFilter(kind schema.GroupVersionKind, subresource, namespace, name string) bool {
    +	return m.filtered
    +}
    +
    +func newTestAdmissionRequest(uid string, kind metav1.GroupVersionKind, operation admissionv1.Operation, subResource string) AdmissionRequest {
    +	return AdmissionRequest{
    +		AdmissionRequest: admissionv1.AdmissionRequest{
    +			UID:       "test-uid",
    +			Kind:      kind,
    +			Operation: operation,
    +			Resource: metav1.GroupVersionResource{
    +				Group:    kind.Group,
    +				Version:  kind.Version,
    +				Resource: "pods",
    +			},
    +			SubResource: subResource,
    +			RequestKind: &metav1.GroupVersionKind{
    +				Group:   kind.Group,
    +				Version: kind.Version,
    +				Kind:    kind.Kind,
    +			},
    +			RequestResource: &metav1.GroupVersionResource{
    +				Group:    kind.Group,
    +				Version:  kind.Version,
    +				Resource: "pods",
    +			},
    +		},
    +	}
    +}
    +
    +func Test_WithFilter(t *testing.T) {
    +	tests := []struct {
    +		name            string
    +		config          config.Configuration
    +		request         AdmissionRequest
    +		wantAllowed     bool
    +		wantInnerCalled bool
    +	}{{
    +		name: "excluded by user exclusion",
    +		config: &mockConfiguration{
    +			excluded: true,
    +		},
    +		request: newTestAdmissionRequest("test-uid", metav1.GroupVersionKind{
    +			Group:   "",
    +			Version: "v1",
    +			Kind:    "Pod",
    +		}, admissionv1.Create, ""),
    +		wantAllowed:     true,
    +		wantInnerCalled: false,
    +	}, {
    +		name: "filtered by resource filter",
    +		config: &mockConfiguration{
    +			filtered: true,
    +		},
    +		request: newTestAdmissionRequest("test-uid", metav1.GroupVersionKind{
    +			Group:   "",
    +			Version: "v1",
    +			Kind:    "Pod",
    +		}, admissionv1.Create, ""),
    +		wantAllowed:     true,
    +		wantInnerCalled: false,
    +	}, {
    +		name:   "filtered kyverno resource - AdmissionReport",
    +		config: &mockConfiguration{},
    +		request: newTestAdmissionRequest("test-uid", metav1.GroupVersionKind{
    +			Group:   "kyverno.io",
    +			Version: "v1alpha2",
    +			Kind:    "AdmissionReport",
    +		}, admissionv1.Create, ""),
    +		wantAllowed:     true,
    +		wantInnerCalled: false,
    +	}, {
    +		name:   "filtered kyverno resource - ClusterAdmissionReport",
    +		config: &mockConfiguration{},
    +		request: newTestAdmissionRequest("test-uid", metav1.GroupVersionKind{
    +			Group:   "kyverno.io",
    +			Version: "v1alpha2",
    +			Kind:    "ClusterAdmissionReport",
    +		}, admissionv1.Create, ""),
    +		wantAllowed:     true,
    +		wantInnerCalled: false,
    +	}, {
    +		name:   "filtered kyverno resource - BackgroundScanReport",
    +		config: &mockConfiguration{},
    +		request: newTestAdmissionRequest("test-uid", metav1.GroupVersionKind{
    +			Group:   "kyverno.io",
    +			Version: "v1alpha2",
    +			Kind:    "BackgroundScanReport",
    +		}, admissionv1.Create, ""),
    +		wantAllowed:     true,
    +		wantInnerCalled: false,
    +	}, {
    +		name:   "filtered kyverno resource - UpdateRequest",
    +		config: &mockConfiguration{},
    +		request: newTestAdmissionRequest("test-uid", metav1.GroupVersionKind{
    +			Group:   "kyverno.io",
    +			Version: "v1beta1",
    +			Kind:    "UpdateRequest",
    +		}, admissionv1.Create, ""),
    +		wantAllowed:     true,
    +		wantInnerCalled: false,
    +	}, {
    +		name:   "not filtered - regular Pod",
    +		config: &mockConfiguration{},
    +		request: newTestAdmissionRequest("test-uid", metav1.GroupVersionKind{
    +			Group:   "",
    +			Version: "v1",
    +			Kind:    "Pod",
    +		}, admissionv1.Create, ""),
    +		wantAllowed:     false,
    +		wantInnerCalled: true,
    +	}, {
    +		name:   "not filtered - ClusterPolicy (not excluded by ExcludeKyvernoResources)",
    +		config: &mockConfiguration{},
    +		request: newTestAdmissionRequest("test-uid", metav1.GroupVersionKind{
    +			Group:   "kyverno.io",
    +			Version: "v1",
    +			Kind:    "ClusterPolicy",
    +		}, admissionv1.Create, ""),
    +		wantAllowed:     false,
    +		wantInnerCalled: true,
    +	}}
    +	for _, tt := range tests {
    +		t.Run(tt.name, func(t *testing.T) {
    +			innerCalled := false
    +			inner := func(ctx context.Context, logger logr.Logger, request AdmissionRequest, startTime time.Time) AdmissionResponse {
    +				innerCalled = true
    +				return AdmissionResponse{Allowed: false}
    +			}
    +			handler := AdmissionHandler(inner).WithFilter(tt.config)
    +			response := handler(context.TODO(), logr.Discard(), tt.request, time.Now())
    +			assert.Equal(t, tt.wantAllowed, response.Allowed)
    +			assert.Equal(t, tt.wantInnerCalled, innerCalled)
    +		})
    +	}
    +}
    +
    +func Test_WithOperationFilter(t *testing.T) {
    +	tests := []struct {
    +		name            string
    +		operations      []admissionv1.Operation
    +		requestOp       admissionv1.Operation
    +		wantAllowed     bool
    +		wantInnerCalled bool
    +	}{{
    +		name:            "allowed operation - CREATE",
    +		operations:      []admissionv1.Operation{admissionv1.Create},
    +		requestOp:       admissionv1.Create,
    +		wantAllowed:     false,
    +		wantInnerCalled: true,
    +	}, {
    +		name:            "allowed operation - UPDATE",
    +		operations:      []admissionv1.Operation{admissionv1.Update},
    +		requestOp:       admissionv1.Update,
    +		wantAllowed:     false,
    +		wantInnerCalled: true,
    +	}, {
    +		name:            "allowed operation - DELETE",
    +		operations:      []admissionv1.Operation{admissionv1.Delete},
    +		requestOp:       admissionv1.Delete,
    +		wantAllowed:     false,
    +		wantInnerCalled: true,
    +	}, {
    +		name:            "allowed multiple operations - CREATE and UPDATE",
    +		operations:      []admissionv1.Operation{admissionv1.Create, admissionv1.Update},
    +		requestOp:       admissionv1.Update,
    +		wantAllowed:     false,
    +		wantInnerCalled: true,
    +	}, {
    +		name:            "filtered operation - UPDATE not in allowed list",
    +		operations:      []admissionv1.Operation{admissionv1.Create},
    +		requestOp:       admissionv1.Update,
    +		wantAllowed:     true,
    +		wantInnerCalled: false,
    +	}, {
    +		name:            "filtered operation - DELETE not in allowed list",
    +		operations:      []admissionv1.Operation{admissionv1.Create, admissionv1.Update},
    +		requestOp:       admissionv1.Delete,
    +		wantAllowed:     true,
    +		wantInnerCalled: false,
    +	}, {
    +		name:            "empty operations list filters all",
    +		operations:      []admissionv1.Operation{},
    +		requestOp:       admissionv1.Create,
    +		wantAllowed:     true,
    +		wantInnerCalled: false,
    +	}}
    +	for _, tt := range tests {
    +		t.Run(tt.name, func(t *testing.T) {
    +			innerCalled := false
    +			inner := func(ctx context.Context, logger logr.Logger, request AdmissionRequest, startTime time.Time) AdmissionResponse {
    +				innerCalled = true
    +				return AdmissionResponse{Allowed: false}
    +			}
    +			request := newTestAdmissionRequest("test-uid", metav1.GroupVersionKind{
    +				Group:   "",
    +				Version: "v1",
    +				Kind:    "Pod",
    +			}, tt.requestOp, "")
    +			handler := AdmissionHandler(inner).WithOperationFilter(tt.operations...)
    +			response := handler(context.TODO(), logr.Discard(), request, time.Now())
    +			assert.Equal(t, tt.wantAllowed, response.Allowed)
    +			assert.Equal(t, tt.wantInnerCalled, innerCalled)
    +		})
    +	}
    +}
    +
    +func Test_WithSubResourceFilter(t *testing.T) {
    +	tests := []struct {
    +		name            string
    +		subresources    []string
    +		requestSubRes   string
    +		wantAllowed     bool
    +		wantInnerCalled bool
    +	}{{
    +		name:            "allowed subresource - status",
    +		subresources:    []string{"status"},
    +		requestSubRes:   "status",
    +		wantAllowed:     false,
    +		wantInnerCalled: true,
    +	}, {
    +		name:            "allowed subresource - scale",
    +		subresources:    []string{"scale"},
    +		requestSubRes:   "scale",
    +		wantAllowed:     false,
    +		wantInnerCalled: true,
    +	}, {
    +		name:            "allowed multiple subresources",
    +		subresources:    []string{"status", "scale"},
    +		requestSubRes:   "scale",
    +		wantAllowed:     false,
    +		wantInnerCalled: true,
    +	}, {
    +		name:            "empty request subresource is always allowed",
    +		subresources:    []string{"status"},
    +		requestSubRes:   "",
    +		wantAllowed:     false,
    +		wantInnerCalled: true,
    +	}, {
    +		name:            "empty subresource list - empty request allowed",
    +		subresources:    []string{},
    +		requestSubRes:   "",
    +		wantAllowed:     false,
    +		wantInnerCalled: true,
    +	}, {
    +		name:            "filtered subresource - scale not in allowed list",
    +		subresources:    []string{"status"},
    +		requestSubRes:   "scale",
    +		wantAllowed:     true,
    +		wantInnerCalled: false,
    +	}, {
    +		name:            "filtered subresource - eviction not in allowed list",
    +		subresources:    []string{"status", "scale"},
    +		requestSubRes:   "eviction",
    +		wantAllowed:     true,
    +		wantInnerCalled: false,
    +	}, {
    +		name:            "empty subresource list filters all non-empty requests",
    +		subresources:    []string{},
    +		requestSubRes:   "status",
    +		wantAllowed:     true,
    +		wantInnerCalled: false,
    +	}}
    +	for _, tt := range tests {
    +		t.Run(tt.name, func(t *testing.T) {
    +			innerCalled := false
    +			inner := func(ctx context.Context, logger logr.Logger, request AdmissionRequest, startTime time.Time) AdmissionResponse {
    +				innerCalled = true
    +				return AdmissionResponse{Allowed: false}
    +			}
    +			request := newTestAdmissionRequest("test-uid", metav1.GroupVersionKind{
    +				Group:   "",
    +				Version: "v1",
    +				Kind:    "Pod",
    +			}, admissionv1.Create, tt.requestSubRes)
    +			handler := AdmissionHandler(inner).WithSubResourceFilter(tt.subresources...)
    +			response := handler(context.TODO(), logr.Discard(), request, time.Now())
    +			assert.Equal(t, tt.wantAllowed, response.Allowed)
    +			assert.Equal(t, tt.wantInnerCalled, innerCalled)
    +		})
    +	}
    +}
    

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

News mentions

0

No linked articles in our index yet.