Kyverno's Improper JMESPath Variable Evaluation Leads to Denial of Service
Description
Kyverno is a policy engine designed for cloud native platform engineering teams. In versions 1.14.1 and below, a Denial of Service (DoS) vulnerability exists due to improper handling of JMESPath variable substitutions. Attackers with permissions to create or update Kyverno policies can craft expressions using the {{@}} variable combined with a pipe and an invalid JMESPath function (e.g., {{@ | non_existent_function }}). This leads to a nil value being substituted into the policy structure. Subsequent processing by internal functions, specifically getValueAsStringMap, which expect string values, results in a panic due to a type assertion failure (interface {} is nil, not string). This crashes Kyverno worker threads in the admission controller and causes continuous crashes of the reports controller pod. This is fixed in version 1.14.2.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/kyverno/kyvernoGo | < 1.14.2 | 1.14.2 |
Affected products
1Patches
1cbd7d4ca24deMerge commit from fork (#13138)
2 files changed · +250 −1
pkg/engine/wildcards/wildcards.go+10 −1 modified@@ -135,7 +135,16 @@ func getValueAsStringMap(key string, data interface{}) (string, map[string]strin } for k, v := range valMap { - result[k] = v.(string) + if v == nil { + continue + } + + switch typedVal := v.(type) { + case string: + result[k] = typedVal + default: + continue + } } return patternKey, result
pkg/engine/wildcards/wildcards_test.go+240 −0 modified@@ -3,6 +3,8 @@ package wildcards import ( "reflect" "testing" + + "github.com/stretchr/testify/assert" ) func TestExpandInMetadata(t *testing.T) { @@ -25,3 +27,241 @@ func testExpand(t *testing.T, patternMap, resourceMap map[string]string, expecte t.Errorf("expected %v but received %v", expectedMap, result) } } + +func TestGetValueAsStringMap_NilHandling(t *testing.T) { + tests := []struct { + name string + key string + data interface{} + expectedKey string + expectedResult map[string]string + }{ + { + name: "nil data", + key: "test", + data: nil, + expectedKey: "", + expectedResult: nil, + }, + { + name: "data is not a map", + key: "test", + data: "not a map", + expectedKey: "", + expectedResult: nil, + }, + { + name: "key not found", + key: "nonexistent", + data: map[string]interface{}{"otherKey": "value"}, + expectedKey: "", + expectedResult: nil, + }, + { + name: "value is nil", + key: "test", + data: map[string]interface{}{"test": nil}, + expectedKey: "", + expectedResult: nil, + }, + { + name: "value is not a map", + key: "test", + data: map[string]interface{}{"test": "not a map"}, + expectedKey: "", + expectedResult: nil, + }, + { + name: "handles nil value in map", + key: "test", + data: map[string]interface{}{ + "test": map[string]interface{}{ + "key1": "value1", + "key2": nil, + "key3": "value3", + }, + }, + expectedKey: "test", + expectedResult: map[string]string{ + "key1": "value1", + // key2 should be skipped + "key3": "value3", + }, + }, + { + name: "handles non-string value in map", + key: "test", + data: map[string]interface{}{ + "test": map[string]interface{}{ + "key1": "value1", + "key2": 123, + "key3": map[string]string{ + "nested": "value", + }, + "key4": "value4", + }, + }, + expectedKey: "test", + expectedResult: map[string]string{ + "key1": "value1", + // key2 should be skipped (non-string) + // key3 should be skipped (complex) + "key4": "value4", + }, + }, + { + name: "normal case - all strings", + key: "test", + data: map[string]interface{}{ + "test": map[string]interface{}{ + "key1": "value1", + "key2": "value2", + }, + }, + expectedKey: "test", + expectedResult: map[string]string{ + "key1": "value1", + "key2": "value2", + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + key, result := getValueAsStringMap(test.key, test.data) + assert.Equal(t, test.expectedKey, key) + assert.Equal(t, test.expectedResult, result) + }) + } +} + +func TestExpandInMetadata_NilSafety(t *testing.T) { + testCases := []struct { + name string + patternMap map[string]interface{} + resourceMap map[string]interface{} + shouldPanic bool + }{ + { + name: "nil value in annotation should not panic", + patternMap: map[string]interface{}{ + "metadata": map[string]interface{}{ + "annotations": map[string]interface{}{ + "some-key": nil, + }, + }, + }, + resourceMap: map[string]interface{}{ + "metadata": map[string]interface{}{ + "annotations": map[string]interface{}{ + "real-key": "real-value", + }, + }, + }, + shouldPanic: false, + }, + { + name: "complex value in annotation should not panic", + patternMap: map[string]interface{}{ + "metadata": map[string]interface{}{ + "annotations": map[string]interface{}{ + "some-key": map[string]interface{}{ + "nested": "value", + }, + }, + }, + }, + resourceMap: map[string]interface{}{ + "metadata": map[string]interface{}{ + "annotations": map[string]interface{}{ + "real-key": "real-value", + }, + }, + }, + shouldPanic: false, + }, + { + name: "simulated jmespath nil result", + patternMap: map[string]interface{}{ + "metadata": map[string]interface{}{ + "annotations": map[string]interface{}{ + "test": nil, // Simulating what happens when {{@ | foo}} evaluates with undefined 'foo' + }, + }, + }, + resourceMap: map[string]interface{}{ + "metadata": map[string]interface{}{ + "annotations": map[string]interface{}{ + "real-key": "real-value", + }, + }, + }, + shouldPanic: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + defer func() { + r := recover() + if tc.shouldPanic { + assert.NotNil(t, r, "Expected function to panic, but it didn't") + } else { + assert.Nil(t, r, "Function panicked unexpectedly") + } + }() + + ExpandInMetadata(tc.patternMap, tc.resourceMap) + }) + } +} + +func TestJMESPathNil(t *testing.T) { + // Create a pattern with a nil value in labels or annotations + // to simulate what happens after a JMESPath expression like {{@ | foo}} + // (where 'foo' is not a defined function) is evaluated and results in nil. + patternMap := map[string]interface{}{ + "metadata": map[string]interface{}{ + "labels": map[string]interface{}{ + "normal": "value", + "nil-value": nil, // This represents the result of {{@ | foo}} substitution + }, + "annotations": map[string]interface{}{ + "another-normal": "value", + "complex-value": map[string]interface{}{ // And this represents a complex structure + "nested": "value", + }, + }, + }, + } + + resourceMap := map[string]interface{}{ + "metadata": map[string]interface{}{ + "labels": map[string]interface{}{ + "app": "test", + }, + "annotations": map[string]interface{}{ + "test": "value", + }, + }, + } + + defer func() { + r := recover() + assert.Nil(t, r, "ExpandInMetadata should not panic with nil values, but it did: %v", r) + }() + + result := ExpandInMetadata(patternMap, resourceMap) + + // Additional verification that the function works correctly + metadataResult := result["metadata"].(map[string]interface{}) + labelsResult, ok := metadataResult["labels"].(map[string]interface{}) + + assert.True(t, ok, "Expected labels to be a map[string]interface{}") + assert.Contains(t, labelsResult, "normal") + + // Annotations with complex values should also be handled properly + annotationsResult, ok := metadataResult["annotations"].(map[string]interface{}) + assert.True(t, ok, "Expected annotations to be a map[string]interface{}") + assert.Contains(t, annotationsResult, "another-normal") +}
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4- github.com/advisories/GHSA-r5p3-955p-5ggqghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-47281ghsaADVISORY
- github.com/kyverno/kyverno/commit/cbd7d4ca24de1c55396fc3295e9fc3215832be7cghsax_refsource_MISCWEB
- github.com/kyverno/kyverno/security/advisories/GHSA-r5p3-955p-5ggqghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.