Skip to content

fix(store): descend through non-string-keyed nodes in models.yml credential denylist (RIG-3116) - #826

Open
rigel-mintaka wants to merge 2 commits into
mainfrom
compass-server/rig-3116-models-cred-fail-open
Open

fix(store): descend through non-string-keyed nodes in models.yml credential denylist (RIG-3116)#826
rigel-mintaka wants to merge 2 commits into
mainfrom
compass-server/rig-3116-models-cred-fail-open

Conversation

@rigel-mintaka

Copy link
Copy Markdown
Contributor

rejectCredentialModels — the store-door CP-4 guard keeping provider
credentials off the config bundle — descended into providers, each
provider, and headers via direct .(map[string]any) assertions. yaml.v3
decodes a mapping to map[any]any the moment any key in it is non-string, so
a non-string sibling key flipped the enclosing node's Go type and every
assertion on it failed OPEN: the walk skipped the credential-bearing sibling
and the secret rode the bundle un-rejected.

This is the same fail-open bug class RIG-2968's round-2 fix closed for the
settings/profile-settings credential walk (via yamlMapIndex), left unclosed
here because this door ENUMERATES arbitrary provider/header names rather than
doing keyed lookups over known paths. Surfaced by the RIG-2968 round-3
adversarial review's cross-member parity check; three shapes proven accepted
end-to-end through validateAndHashConfigBundle:

  • provider-level shield: providers.openai.{0: junk, apiKey: …} hides the apiKey.
  • providers-level shield: providers.{0: junk, openai: {apiKey: …}} skips ALL providers.
  • headers-level shield: providers.x.headers.{0: junk, Authorization: …} skips the literal-secret header.

Fix: add yamlMapEntries, the enumeration analog of yamlMapIndex — it
iterates both map shapes and returns a collision-safe slice (a non-string key
that stringifies to a sibling's name cannot overwrite it). Route
rejectCredentialModels' descent through yamlMapEntries (providers, headers)
and yamlMapIndex (per-provider apiKey/headers lookup), so no sibling can flip
a node and shield a credential leaf. Both credential doors now share one
convention: detect credentials regardless of map shape, tolerate benign
non-string keys, reject only credentials.

parseYAMLMapping already fails CLOSED on a top-level non-string key, so the
three inner descents were the only fail-open sites.

Tests: three red-first reject cases (one per shield site, each verified to
redden when its defending helper's map[any]any branch is neutered) plus an
accept-symmetry case (a nested non-string key with no credential leaf must
still be ACCEPTED).

Refs RIG-3116

Co-authored-by: Matt Wilkinson matt@rigel.build

…ential denylist (RIG-3116)

`rejectCredentialModels` — the store-door CP-4 guard keeping provider
credentials off the config bundle — descended into `providers`, each
provider, and `headers` via direct `.(map[string]any)` assertions. yaml.v3
decodes a mapping to `map[any]any` the moment any key in it is non-string, so
a non-string sibling key flipped the enclosing node's Go type and every
assertion on it failed OPEN: the walk skipped the credential-bearing sibling
and the secret rode the bundle un-rejected.

This is the same fail-open bug class RIG-2968's round-2 fix closed for the
settings/profile-settings credential walk (via `yamlMapIndex`), left unclosed
here because this door ENUMERATES arbitrary provider/header names rather than
doing keyed lookups over known paths. Surfaced by the RIG-2968 round-3
adversarial review's cross-member parity check; three shapes proven accepted
end-to-end through `validateAndHashConfigBundle`:

- provider-level shield: `providers.openai.{0: junk, apiKey: …}` hides the apiKey.
- providers-level shield: `providers.{0: junk, openai: {apiKey: …}}` skips ALL providers.
- headers-level shield: `providers.x.headers.{0: junk, Authorization: …}` skips the literal-secret header.

Fix: add `yamlMapEntries`, the enumeration analog of `yamlMapIndex` — it
iterates both map shapes and returns a collision-safe slice (a non-string key
that stringifies to a sibling's name cannot overwrite it). Route
`rejectCredentialModels`' descent through `yamlMapEntries` (providers, headers)
and `yamlMapIndex` (per-provider apiKey/headers lookup), so no sibling can flip
a node and shield a credential leaf. Both credential doors now share one
convention: detect credentials regardless of map shape, tolerate benign
non-string keys, reject only credentials.

`parseYAMLMapping` already fails CLOSED on a top-level non-string key, so the
three inner descents were the only fail-open sites.

Tests: three red-first reject cases (one per shield site, each verified to
redden when its defending helper's `map[any]any` branch is neutered) plus an
accept-symmetry case (a nested non-string key with no credential leaf must
still be ACCEPTED).

Refs RIG-3116

Co-authored-by: Matt Wilkinson <matt@rigel.build>
@linear-code

linear-code Bot commented Sep 1, 2026

Copy link
Copy Markdown

RIG-3116

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown

Compass engineering docs preview: https://compass-server-rig-3116-mode.compass-eng-docs.pages.dev

Deployed from compass-server/rig-3116-models-cred-fail-open at f3d0f8d.

…dential denylist (RIG-3116)

Adds the combined-shield reject case from the round-1 review (Low #1): a
non-string sibling at BOTH the `providers` node AND the provider node
simultaneously. Cases (a)/(b) already cover each flip point in isolation via
`yamlMapIndex` / `yamlMapEntries`; the two helpers descend linearly with no
shared state, so the combined case cannot regress unless a singleton does. Pinned
explicitly to defend the composition against a future refactor that couples the
descents.

Review-loop disposition for PR #826 (round 1, clean pass at floor — 0 high, 0
medium): Low #1 fixed here; Low #2 (non-string header VALUE skip, a pre-existing
byte-for-byte-unchanged value-shape axis) deferred to RIG-3134.

Spec-impact: none (test-only; production behavior unchanged).
Refs RIG-3116
Refs RIG-3134

Co-authored-by: Matt Wilkinson <matt@rigel.build>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant