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
This commit is contained in:
2026-11-09 21:18:43 +01:00
parent a4e5a8ffef
commit 3d70bd3660
2 changed files with 9 additions and 1 deletions
+4 -1
View File
@@ -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
@@ -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")
}