Memory safety / panic (crash) vulnerability in KV parsing

MEDIUM
grafana/grafana
Commit: 3361f0db8bc0
Affected: <= 12.4.0
2026-04-09 13:50 UTC

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") + }) +}
← Back to Alerts View on GitHub →