From 23f5babe33c6b1d024e9118859445370a46dbbc7 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 28 Oct 2026 21:46:35 +0100 Subject: [PATCH 1/3] Runtime state guard: a name holding an object is data, not a credential, so a placement's binding of the provision secret is kept (issue 1126) --- node-tools/internal/bus/state.go | 7 +++++-- node-tools/internal/bus/state_test.go | 5 +++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/node-tools/internal/bus/state.go b/node-tools/internal/bus/state.go index e2db7b3..f4f6adb 100644 --- a/node-tools/internal/bus/state.go +++ b/node-tools/internal/bus/state.go @@ -391,7 +391,9 @@ func valueOf(raw []byte) json.RawMessage { var credentialEndings = []string{"password", "passwd", "secret", "token", "credential", "credentials", "authorization", "apikey", "privatekey", "accesskey", "cookie"} -// credentialField is the first field anywhere in a JSON value whose name says it is a credential, or "". +// credentialField is the first field anywhere in a JSON value whose name says it is a credential, or "". A credential is +// a value, so a name counts only where it holds one or a list of them: a name holding an object is data naming a structure (a +// placement's binding of the provision "secret" to the vault, issue 1126), whose own fields are looked at in turn. func credentialField(value json.RawMessage) string { var v any if json.Unmarshal(value, &v) != nil { @@ -408,8 +410,9 @@ func credentialField(value json.RawMessage) string { sort.Strings(keys) for _, k := range keys { norm := strings.NewReplacer("-", "", "_", "", " ", "").Replace(strings.ToLower(k)) + _, object := t[k].(map[string]any) for _, end := range credentialEndings { - if strings.HasSuffix(norm, end) { + if strings.HasSuffix(norm, end) && !object { return k } } diff --git a/node-tools/internal/bus/state_test.go b/node-tools/internal/bus/state_test.go index 9f191c4..f157be7 100644 --- a/node-tools/internal/bus/state_test.go +++ b/node-tools/internal/bus/state_test.go @@ -15,6 +15,11 @@ func TestACredentialNamedFieldIsFoundAnywhereInAValue(t *testing.T) { `{"clientSecret":"s"}`: "clientSecret", `{"maxTokens":4096,"name":"a","generation":3}`: "", `"just a string"`: "", + // Issue 1126: a placement binds the provision "secret" to the vault; the name holds a structure, not a value. + `{"bindings":{"secret":{"node":"anchor","module":"mesh-vault"}}}`: "", + `{"secret":{"value":"s"}}`: "", + `{"secret":["s"]}`: "secret", + `{"bindings":{"secret":{"token":"s"}}}`: "token", } { if got := credentialField(json.RawMessage(value)); got != want { t.Errorf("%s: found %q, want %q", value, got, want) From 1a0424117a29549da0e037fd9b5692a0d27d2330 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 28 Oct 2026 21:48:27 +0100 Subject: [PATCH 2/3] Runtime state guard: a credential-shaped name passes only holding an exact provider reference, node and module, so a placement's binding is kept and nothing else gets through (issue 1126) --- node-tools/internal/bus/state.go | 21 ++++++++++++++++----- node-tools/internal/bus/state_test.go | 10 ++++++---- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/node-tools/internal/bus/state.go b/node-tools/internal/bus/state.go index f4f6adb..d4dee76 100644 --- a/node-tools/internal/bus/state.go +++ b/node-tools/internal/bus/state.go @@ -391,9 +391,10 @@ func valueOf(raw []byte) json.RawMessage { var credentialEndings = []string{"password", "passwd", "secret", "token", "credential", "credentials", "authorization", "apikey", "privatekey", "accesskey", "cookie"} -// credentialField is the first field anywhere in a JSON value whose name says it is a credential, or "". A credential is -// a value, so a name counts only where it holds one or a list of them: a name holding an object is data naming a structure (a -// placement's binding of the provision "secret" to the vault, issue 1126), whose own fields are looked at in turn. +// credentialField is the first field anywhere in a JSON value whose name says it is a credential, or "". The one shape a +// credential-shaped name may hold is a provider reference, an object of exactly `node` and `module`, both strings: a +// placement's binding of the provision "secret" to the vault (issue 1126) names who provides it, never its value. Any +// other shape under such a name is refused. func credentialField(value json.RawMessage) string { var v any if json.Unmarshal(value, &v) != nil { @@ -410,9 +411,8 @@ func credentialField(value json.RawMessage) string { sort.Strings(keys) for _, k := range keys { norm := strings.NewReplacer("-", "", "_", "", " ", "").Replace(strings.ToLower(k)) - _, object := t[k].(map[string]any) for _, end := range credentialEndings { - if strings.HasSuffix(norm, end) && !object { + if strings.HasSuffix(norm, end) && !providerReference(t[k]) { return k } } @@ -431,3 +431,14 @@ func credentialField(value json.RawMessage) string { } return walk(v) } + +// providerReference says whether v is exactly {"node": , "module": }. +func providerReference(v any) bool { + m, ok := v.(map[string]any) + if !ok || len(m) != 2 { + return false + } + node, okN := m["node"].(string) + module, okM := m["module"].(string) + return okN && okM && node != "" && module != "" +} diff --git a/node-tools/internal/bus/state_test.go b/node-tools/internal/bus/state_test.go index f157be7..cc85f48 100644 --- a/node-tools/internal/bus/state_test.go +++ b/node-tools/internal/bus/state_test.go @@ -15,11 +15,13 @@ func TestACredentialNamedFieldIsFoundAnywhereInAValue(t *testing.T) { `{"clientSecret":"s"}`: "clientSecret", `{"maxTokens":4096,"name":"a","generation":3}`: "", `"just a string"`: "", - // Issue 1126: a placement binds the provision "secret" to the vault; the name holds a structure, not a value. + // Issue 1126: a placement binds the provision "secret" to the vault; only an exact provider reference passes. `{"bindings":{"secret":{"node":"anchor","module":"mesh-vault"}}}`: "", - `{"secret":{"value":"s"}}`: "", - `{"secret":["s"]}`: "secret", - `{"bindings":{"secret":{"token":"s"}}}`: "token", + `{"secret":{"value":"s"}}`: "secret", + `{"secret":{"node":"anchor","module":"mesh-vault","value":"s"}}`: "secret", + `{"secret":{"node":"anchor","module":7}}`: "secret", + `{"secret":["s"]}`: "secret", + `{"bindings":{"secret":{"token":"s"}}}`: "secret", } { if got := credentialField(json.RawMessage(value)); got != want { t.Errorf("%s: found %q, want %q", value, got, want) From 2966c1e32c3dc80fc57880b6204e32fd393061fe Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 28 Oct 2026 21:49:52 +0100 Subject: [PATCH 3/3] Runtime state guard: hold a provider reference's node and module to the mesh's name pattern, so no credential rides in one (issue 1126) --- node-tools/internal/bus/state.go | 7 +++++-- node-tools/internal/bus/state_test.go | 5 +++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/node-tools/internal/bus/state.go b/node-tools/internal/bus/state.go index d4dee76..a673168 100644 --- a/node-tools/internal/bus/state.go +++ b/node-tools/internal/bus/state.go @@ -432,7 +432,7 @@ func credentialField(value json.RawMessage) string { return walk(v) } -// providerReference says whether v is exactly {"node": , "module": }. +// providerReference says whether v is exactly {"node": , "module": }, each a name as mesh-sdk names them. func providerReference(v any) bool { m, ok := v.(map[string]any) if !ok || len(m) != 2 { @@ -440,5 +440,8 @@ func providerReference(v any) bool { } node, okN := m["node"].(string) module, okM := m["module"].(string) - return okN && okM && node != "" && module != "" + return okN && okM && referenceName.MatchString(node) && referenceName.MatchString(module) } + +// referenceName is a node's or a module's name as mesh-sdk names them: lower case, digits and dashes. +var referenceName = regexp.MustCompile(`^[a-z0-9][a-z0-9-]*$`) diff --git a/node-tools/internal/bus/state_test.go b/node-tools/internal/bus/state_test.go index cc85f48..6ce8393 100644 --- a/node-tools/internal/bus/state_test.go +++ b/node-tools/internal/bus/state_test.go @@ -20,8 +20,9 @@ func TestACredentialNamedFieldIsFoundAnywhereInAValue(t *testing.T) { `{"secret":{"value":"s"}}`: "secret", `{"secret":{"node":"anchor","module":"mesh-vault","value":"s"}}`: "secret", `{"secret":{"node":"anchor","module":7}}`: "secret", - `{"secret":["s"]}`: "secret", - `{"bindings":{"secret":{"token":"s"}}}`: "secret", + `{"secret":{"node":"anchor","module":"hunter2 Bearer s3cr3t"}}`: "secret", + `{"secret":["s"]}`: "secret", + `{"bindings":{"secret":{"token":"s"}}}`: "secret", } { if got := credentialField(json.RawMessage(value)); got != want { t.Errorf("%s: found %q, want %q", value, got, want)