From 3d70bd36601424c8d2505d79e14e9999a0ec2805 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 9 Nov 2026 21:18:43 +0100 Subject: [PATCH] nats_stream_messages refuses a module's kept state (KV_*, OBJ_*), which can hold secrets (issue 1966, review 1) Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ --- modules/nats/cmd/nats-tools/consumer.go | 5 ++++- modules/nats/cmd/nats-tools/consumer_test.go | 5 +++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/modules/nats/cmd/nats-tools/consumer.go b/modules/nats/cmd/nats-tools/consumer.go index 5bb9f8ea0..c0335ca0f 100644 --- a/modules/nats/cmd/nats-tools/consumer.go +++ b/modules/nats/cmd/nats-tools/consumer.go @@ -266,7 +266,7 @@ func readTools(jsz func() ([]byte, error), open MsgOpen) []stdio.Tool { return Consumer(doc, arg(a, "stream"), arg(a, "consumer")) }}, {Name: "nats_stream_messages", - Description: "The messages a stream keeps for one subject (a filter with * or > is allowed), newest first: sequence, subject, time and size of each, and the body only when asked for and no larger than 4 KiB. Bounded by count (default 20, at most 200) and bytes (default 1 MiB); says when it stopped before the oldest the stream keeps. Read on the bus module's own credential.", + Description: "The messages a stream keeps for one subject (a filter with * or > is allowed), newest first: sequence, subject, time and size of each, and the body only when asked for and no larger than 4 KiB. Bounded by count (default 20, at most 200) and bytes (default 1 MiB); says when it stopped before the oldest the stream keeps. Read on the bus module's own credential. Refuses the streams of a module's kept state (KV_*, OBJ_*): their values are read through their owners' tools; this tool is for lost events.", Input: map[string]any{"stream": str("the stream, e.g. EVENTS"), "subject": str("a subject or filter, e.g. mesh.mod.mesh-delivery-indexer.event.view-changed"), "count": num("at most this many (default 20, at most 200)"), "bytes": num("at most this many bytes of messages (default 1048576)"), "body": map[string]any{"type": "boolean", "description": "carry the body of each message of at most 4 KiB"}}, @@ -275,6 +275,9 @@ func readTools(jsz func() ([]byte, error), open MsgOpen) []stdio.Tool { if !name.MatchString(stream) || subject == "" || strings.ContainsAny(subject, " \t") { return nil, errors.New("stream (one name) and subject (a subject or filter, no space) are required") } + if strings.HasPrefix(stream, "KV_") || strings.HasPrefix(stream, "OBJ_") { + return nil, fmt.Errorf("%s is a module's kept state (a bucket or an object store), which can hold secrets: it is read through its owner's tools, never here; this tool reads the events of a stream", stream) + } doc, err := jsz() if err != nil { return nil, err diff --git a/modules/nats/cmd/nats-tools/consumer_test.go b/modules/nats/cmd/nats-tools/consumer_test.go index 858efd80c..c74d14df8 100644 --- a/modules/nats/cmd/nats-tools/consumer_test.go +++ b/modules/nats/cmd/nats-tools/consumer_test.go @@ -123,6 +123,11 @@ func TestTheReadToolsAreServedAndRefuseBadArguments(t *testing.T) { if err != nil || out.(map[string]any)["returned"].(int) != 0 && out.(map[string]any)["returned"].(int) != 1 { t.Fatalf("%v %v", out, err) } + for _, st := range []string{"KV_mesh-vault_signers", "OBJ_mesh-knowledge_sections"} { + if _, err := by["nats_stream_messages"](map[string]any{"stream": st, "subject": ">", "body": true}); err == nil || !strings.Contains(err.Error(), "kept state") { + t.Fatalf("%s was read: %v", st, err) + } + } if _, err := by["nats_stream_messages"](map[string]any{"stream": "EVENTS"}); err == nil { t.Fatal("no subject answered") }