From 41a6c03010cfdc0a429009f5504e395db7af6fa5 Mon Sep 17 00:00:00 2001 From: lyon Date: Mon, 22 Jun 2026 13:57:29 +0800 Subject: [PATCH] fix: sanitize workbench facts cache payloads --- internal/workbenchruntime/cache_test.go | 10 ++++++++- internal/workbenchruntime/service.go | 30 ++++++++++++++++++++++++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/internal/workbenchruntime/cache_test.go b/internal/workbenchruntime/cache_test.go index 77b0bf5b..be33588a 100644 --- a/internal/workbenchruntime/cache_test.go +++ b/internal/workbenchruntime/cache_test.go @@ -193,7 +193,7 @@ func TestFactsCacheStorablePayloadRemovesRuntimeOnlySecretKeys(t *testing.T) { "secretMaterialStored": false, "persistence": map[string]any{"cache": map[string]any{"secretMaterialStored": false}}, "facts": map[string]any{ - "traceEvents": []any{map[string]any{"traceId": "trc_1", "projectedSeq": 1, "valuesRedacted": true}}, + "traceEvents": []any{map[string]any{"traceId": "trc_1", "projectedSeq": 1, "providerPayload": map[string]any{"raw": true}, "stdout": "internal", "valuesRedacted": true}}, }, "valuesRedacted": true, } @@ -204,6 +204,14 @@ func TestFactsCacheStorablePayloadRemovesRuntimeOnlySecretKeys(t *testing.T) { if _, ok := stored["persistence"]; ok { t.Fatalf("facts storable payload kept persistence: %#v", stored) } + traceEvents := objectMap(stored["facts"])["traceEvents"].([]any) + traceEvent := objectMap(traceEvents[0]) + if _, ok := traceEvent["providerPayload"]; ok { + t.Fatalf("facts storable payload kept providerPayload: %#v", traceEvent) + } + if _, ok := traceEvent["stdout"]; ok { + t.Fatalf("facts storable payload kept stdout: %#v", traceEvent) + } entry := factsCacheEntry{CachedAt: time.Now().UTC().Format(time.RFC3339Nano), Payload: stored, PayloadBytes: 128, TTLMillis: 300000, ValuesRedacted: true} if _, err := cachePayloadBytes(entry, 4096); err != nil { t.Fatalf("storable facts payload rejected: %v", err) diff --git a/internal/workbenchruntime/service.go b/internal/workbenchruntime/service.go index 47c2e3ff..bb35c509 100644 --- a/internal/workbenchruntime/service.go +++ b/internal/workbenchruntime/service.go @@ -543,7 +543,35 @@ func factsCacheStorablePayload(payload map[string]any) map[string]any { delete(clone, "cache") delete(clone, "persistence") delete(clone, "secretMaterialStored") - return clone + return objectMap(sanitizeFactsCachePayload(clone)) +} + +func sanitizeFactsCachePayload(value any) any { + switch item := value.(type) { + case map[string]any: + cleaned := map[string]any{} + for key, child := range item { + if cachePayloadKeyRejected(key) { + continue + } + cleaned[key] = sanitizeFactsCachePayload(child) + } + return cleaned + case []any: + cleaned := make([]any, 0, len(item)) + for _, child := range item { + cleaned = append(cleaned, sanitizeFactsCachePayload(child)) + } + return cleaned + case []map[string]any: + cleaned := make([]any, 0, len(item)) + for _, child := range item { + cleaned = append(cleaned, sanitizeFactsCachePayload(child)) + } + return cleaned + default: + return value + } } func factsCacheResponsePayload(payload map[string]any, persistence map[string]any) map[string]any {