Memory safety / panic (crash) vulnerability in KV parsing
Description
The commit adds a validation guard in ParseDataKeyParts to ensure the resource version metadata (rvMeta) contains at least 3 tilde-separated parts before accessing rvParts[1] and rvParts[2]. Previously, crafted or malformed rvMeta values with only 1 or 2 segments could lead to an index-out-of-range panic when parsing storage KV keys, potentially crashing Unistore and causing a denial-of-service condition or information leakage. The change is accompanied by tests covering edge cases and panic avoidance.
Proof of Concept
Go PoC code snippet:
package main
import (
"fmt"
"strings"
kv "github.com/grafana/grafana/pkg/storage/unified/resource/kv"
)
func main() {
// Craft a key with rvMeta that has only a single tilde-separated segment (would panic pre-fix)
parts := strings.Split("g/res/n/99", "/")
dk, rvParts, err := kv.ParseDataKeyParts(parts)
fmt.Printf("dk=%+v rvParts=%v err=%v\n", dk, rvParts, err)
}
Commit Details
Author: maicon
Date: 2026-04-09 12:49 UTC
Message:
KV: validate rvMeta in ParseDataKeyParts and add tests (avoid panic) (#122193)
* Unistore: Avoid panic when parsing datakey parts (KVSQL)
Signed-off-by: Maicon Costa <maiconscosta@gmail.com>
* test(kv): cover ParseDataKeyParts rvMeta and ParseKeyWithGUID
Add tests for short or invalid tilde-separated resource metadata (no panic),
valid cluster-scoped and namespaced keys, four-segment GUID keys, and
ParseKeyWithGUID error cases.
Made-with: Cursor
---------
Signed-off-by: Maicon Costa <maiconscosta@gmail.com>
Triage Assessment
Vulnerability Type: Memory safety / crash (panic prevention) vulnerability
Confidence: MEDIUM
Reasoning:
The commit adds input validation to parsing of resource version metadata to avoid panics when encountering invalid or short rvMeta parts. This prevents potential crashes and related denial-of-service or information leakage scenarios from malformed keys. Tests cover these edge cases, indicating a defensive security improvement.
Verification Assessment
Vulnerability Type: Memory safety / panic (crash) vulnerability in KV parsing
Confidence: MEDIUM
Affected Versions: <= 12.4.0
Code Diff
diff --git a/pkg/storage/unified/resource/kv/datastoretypes.go b/pkg/storage/unified/resource/kv/datastoretypes.go
index 905c2d7cd42f..ed9651ee0a4d 100644
--- a/pkg/storage/unified/resource/kv/datastoretypes.go
+++ b/pkg/storage/unified/resource/kv/datastoretypes.go
@@ -68,6 +68,9 @@ func ParseDataKeyParts(parts []string) (DataKey, []string, error) {
return DataKey{}, nil, fmt.Errorf("invalid key: expected 4 or 5 parts, got %d", len(parts))
}
rvParts := strings.Split(rvMeta, "~")
+ if len(rvParts) < 3 {
+ return DataKey{}, nil, fmt.Errorf("invalid resource version metadata: expected at least 3 parts, got %d", len(rvParts))
+ }
rv, err := strconv.ParseInt(rvParts[0], 10, 64)
if err != nil {
return DataKey{}, nil, fmt.Errorf("invalid resource version '%s': %w", rvParts[0], err)
diff --git a/pkg/storage/unified/resource/kv/datastoretypes_test.go b/pkg/storage/unified/resource/kv/datastoretypes_test.go
new file mode 100644
index 000000000000..139ae0f6bcfe
--- /dev/null
+++ b/pkg/storage/unified/resource/kv/datastoretypes_test.go
@@ -0,0 +1,102 @@
+package kv
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestParseDataKeyParts_ResourceVersionMetadata(t *testing.T) {
+ t.Parallel()
+
+ t.Run("cluster-scoped valid three-part rvMeta", func(t *testing.T) {
+ t.Parallel()
+ parts := strings.Split("g/res/n/42~updated~fld", "/")
+ dk, rvParts, err := ParseDataKeyParts(parts)
+ require.NoError(t, err)
+ require.Equal(t, "g", dk.Group)
+ require.Equal(t, "res", dk.Resource)
+ require.Equal(t, "n", dk.Name)
+ require.Equal(t, int64(42), dk.ResourceVersion)
+ require.Equal(t, DataActionUpdated, dk.Action)
+ require.Equal(t, "fld", dk.Folder)
+ require.Equal(t, []string{"42", "updated", "fld"}, rvParts)
+ })
+
+ t.Run("namespaced valid three-part rvMeta", func(t *testing.T) {
+ t.Parallel()
+ parts := strings.Split("g/res/ns/n/1~created~", "/")
+ dk, rvParts, err := ParseDataKeyParts(parts)
+ require.NoError(t, err)
+ require.Equal(t, "ns", dk.Namespace)
+ require.Equal(t, "n", dk.Name)
+ require.Equal(t, int64(1), dk.ResourceVersion)
+ require.Equal(t, DataActionCreated, dk.Action)
+ require.Equal(t, "", dk.Folder)
+ require.Equal(t, []string{"1", "created", ""}, rvParts)
+ })
+
+ t.Run("rvMeta single segment returns clear error", func(t *testing.T) {
+ t.Parallel()
+ // Previously this could panic indexing rvParts[1].
+ parts := strings.Split("g/res/n/99", "/")
+ _, _, err := ParseDataKeyParts(parts)
+ require.Error(t, err)
+ require.Contains(t, err.Error(), "invalid resource version metadata")
+ require.Contains(t, err.Error(), "expected at least 3 parts, got 1")
+ })
+
+ t.Run("rvMeta two segments returns clear error", func(t *testing.T) {
+ t.Parallel()
+ parts := strings.Split("g/res/n/1~created", "/")
+ _, _, err := ParseDataKeyParts(parts)
+ require.Error(t, err)
+ require.Contains(t, err.Error(), "expected at least 3 parts, got 2")
+ })
+
+ t.Run("rvMeta four tilde segments parses base fields and preserves full rvParts", func(t *testing.T) {
+ t.Parallel()
+ // Fourth segment is consumed by ParseKeyWithGUID as GUID, not as folder.
+ parts := strings.Split("g/res/n/1~created~f~guid-xyz", "/")
+ dk, rvParts, err := ParseDataKeyParts(parts)
+ require.NoError(t, err)
+ require.Equal(t, int64(1), dk.ResourceVersion)
+ require.Equal(t, DataActionCreated, dk.Action)
+ require.Equal(t, "f", dk.Folder)
+ require.Equal(t, []string{"1", "created", "f", "guid-xyz"}, rvParts)
+ })
+}
+
+func TestParseKeyWithGUID(t *testing.T) {
+ t.Parallel()
+
+ t.Run("round trip StringWithGUID", func(t *testing.T) {
+ t.Parallel()
+ // Namespaced key: group/resource/namespace/name/rv~action~folder~guid (5 slash segments).
+ key := "apps/deployments/default/nginx/5~updated~fld~guid-abc"
+ dk, err := ParseKeyWithGUID(key)
+ require.NoError(t, err)
+ require.Equal(t, "guid-abc", dk.GUID)
+ require.Equal(t, int64(5), dk.ResourceVersion)
+ require.Equal(t, DataActionUpdated, dk.Action)
+ require.Equal(t, "fld", dk.Folder)
+ require.Equal(t, key, dk.StringWithGUID())
+ })
+
+ t.Run("rejects key with only three tilde parts in rvMeta", func(t *testing.T) {
+ t.Parallel()
+ // ParseDataKeyParts succeeds but GUID requires fourth segment.
+ _, err := ParseKeyWithGUID("g/res/n/1~created~f")
+ require.Error(t, err)
+ require.Contains(t, err.Error(), "invalid key metadata")
+ require.Contains(t, err.Error(), "expected 4 tilde-separated parts, got 3")
+ })
+
+ t.Run("invalid rvMeta surfaces from ParseDataKeyParts", func(t *testing.T) {
+ t.Parallel()
+ _, err := ParseKeyWithGUID("g/res/n/badrv")
+ require.Error(t, err)
+ require.Contains(t, err.Error(), "invalid resource version metadata")
+ })
+}