Skip to content

refactor: introduce CompositeKeychain and deprecate Keychain.root (OWASP MCP10)#1210

Open
manjunathshiva wants to merge 1 commit into
mongodb-js:mainfrom
manjunathshiva:refactor/mcp10-remove-keychain-root
Open

refactor: introduce CompositeKeychain and deprecate Keychain.root (OWASP MCP10)#1210
manjunathshiva wants to merge 1 commit into
mongodb-js:mainfrom
manjunathshiva:refactor/mcp10-remove-keychain-root

Conversation

@manjunathshiva

Copy link
Copy Markdown

Per OWASP MCP Top 10 (2025) item MCP10 - Context Injection & Over-Sharing. The process-wide Keychain.root static encouraged implicit cross-session secret sharing in a server that can serve many MCP sessions concurrently. This change adds explicit-ownership primitives and a non-breaking deprecation path off the static.

Why this matters: the previous design let any code path register a secret on a singleton readable by every other session in the same process. Under the HTTP transport (many MCP sessions per process), this widened the blast radius of any leak. The existing JSDoc on Keychain itself flagged the static as "a bigger refactor we should do later". This PR is that refactor, staged for one-release deprecation so downstream embedders can migrate at their own pace.

src/common/keychain.ts

  • New CompositeKeychain class: read-through union of N delegates for cases where one logger needs to redact across multiple ownership scopes at once (typically a bootstrap keychain composed with a per-session keychain). register writes to the FIRST delegate only; the rest are read-only sources. This keeps ownership unambiguous: each composite has exactly one writable backing keychain.
  • Keychain.root and registerGlobalSecretToRedact are kept but marked @deprecated. They now write to a module-local fallback keychain (not a class static), making the transitional nature of the shim clear to readers. The contract for callers is unchanged
    • the old API continues to work for one release.

src/common/config/parseUserConfig.ts

  • parseUserConfig return gains a secrets: Keychain field. The returned keychain is pre-populated with every secret discovered in the parsed config (Atlas credentials, connection string, TLS file paths, etc.). The caller owns it and is responsible for threading it into loggers / sessions.
  • For one-release backward compat, every secret is ALSO registered into the deprecated Keychain.root so downstream code that still reads from the global keeps working unchanged.
  • The previous private helper registerKnownSecretsInRootKeychain is renamed registerKnownSecrets and accepts the target keychain as a parameter.

src/lib.ts

  • CompositeKeychain is added to the public exports alongside the existing Keychain and registerGlobalSecretToRedact. Nothing is removed.

Tests:

  • tests/unit/common/keychain.test.ts: new CompositeKeychain test suite covers read-union, write-target-is-first-delegate, full fan-out clear, and refusal of zero-delegate constructions. A new per-instance-isolation test guards against accidental reintroduction of cross-Keychain sharing. The existing Keychain.root + registerGlobalSecretToRedact deprecated-shim tests are preserved (with cleanup before/after) to document the back-compat contract.
  • tests/unit/common/config.test.ts: the two toStrictEqual assertions on parseUserConfig return values now destructure secrets before comparing the rest (the new field is additive but breaks deep-equality), plus assertions that the returned keychain is a real Keychain (and is empty when no secrets were present). One failure-path assertion gains the same destructure
    • an empty-keychain check.

api-extractor reports regenerated.

NOT breaking changes:

  • Keychain.root and registerGlobalSecretToRedact keep working.
  • parseUserConfig({...}) return type gains a new secrets field; existing destructures that pull only { parsed, warnings, error } keep working. Full-equality assertions (toStrictEqual) need to account for the new field — the most common case in test code, flagged here for downstream test maintenance.

A subsequent release can complete the migration by removing Keychain.root and registerGlobalSecretToRedact.

Proposed changes

Checklist

…ASP MCP10)

Per OWASP MCP Top 10 (2025) item MCP10 - Context Injection &
Over-Sharing. The process-wide `Keychain.root` static encouraged
implicit cross-session secret sharing in a server that can serve
many MCP sessions concurrently. This change adds explicit-ownership
primitives and a non-breaking deprecation path off the static.

Why this matters: the previous design let any code path register a
secret on a singleton readable by every other session in the same
process. Under the HTTP transport (many MCP sessions per process),
this widened the blast radius of any leak. The existing JSDoc on
Keychain itself flagged the static as "a bigger refactor we should
do later". This PR is that refactor, staged for one-release
deprecation so downstream embedders can migrate at their own pace.

src/common/keychain.ts
- New `CompositeKeychain` class: read-through union of N delegates
  for cases where one logger needs to redact across multiple
  ownership scopes at once (typically a bootstrap keychain composed
  with a per-session keychain). `register` writes to the FIRST
  delegate only; the rest are read-only sources. This keeps
  ownership unambiguous: each composite has exactly one writable
  backing keychain.
- `Keychain.root` and `registerGlobalSecretToRedact` are kept but
  marked `@deprecated`. They now write to a module-local fallback
  keychain (not a class static), making the transitional nature of
  the shim clear to readers. The contract for callers is unchanged
  - the old API continues to work for one release.

src/common/config/parseUserConfig.ts
- `parseUserConfig` return gains a `secrets: Keychain` field. The
  returned keychain is pre-populated with every secret discovered
  in the parsed config (Atlas credentials, connection string, TLS
  file paths, etc.). The caller owns it and is responsible for
  threading it into loggers / sessions.
- For one-release backward compat, every secret is ALSO registered
  into the deprecated `Keychain.root` so downstream code that still
  reads from the global keeps working unchanged.
- The previous private helper `registerKnownSecretsInRootKeychain`
  is renamed `registerKnownSecrets` and accepts the target keychain
  as a parameter.

src/lib.ts
- `CompositeKeychain` is added to the public exports alongside the
  existing `Keychain` and `registerGlobalSecretToRedact`. Nothing is
  removed.

Tests:
- tests/unit/common/keychain.test.ts: new CompositeKeychain test
  suite covers read-union, write-target-is-first-delegate, full
  fan-out clear, and refusal of zero-delegate constructions. A new
  per-instance-isolation test guards against accidental reintroduction
  of cross-Keychain sharing. The existing `Keychain.root` +
  `registerGlobalSecretToRedact` deprecated-shim tests are preserved
  (with cleanup before/after) to document the back-compat contract.
- tests/unit/common/config.test.ts: the two `toStrictEqual`
  assertions on `parseUserConfig` return values now destructure
  `secrets` before comparing the rest (the new field is additive
  but breaks deep-equality), plus assertions that the returned
  keychain is a real `Keychain` (and is empty when no secrets were
  present). One failure-path assertion gains the same destructure
  + an empty-keychain check.

api-extractor reports regenerated.

NOT breaking changes:
- `Keychain.root` and `registerGlobalSecretToRedact` keep working.
- `parseUserConfig({...})` return type gains a new `secrets` field;
  existing destructures that pull only `{ parsed, warnings, error }`
  keep working. Full-equality assertions (`toStrictEqual`) need to
  account for the new field — the most common case in test code,
  flagged here for downstream test maintenance.

A subsequent release can complete the migration by removing
`Keychain.root` and `registerGlobalSecretToRedact`.
@manjunathshiva manjunathshiva requested a review from a team as a code owner May 27, 2026 10:18
@manjunathshiva manjunathshiva requested review from blva and removed request for a team May 27, 2026 10:18

@nirinchev nirinchev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure what this achieves. Instead of storing the secrets in a single place, we store it in multiple ones, but what is the benefit for users? My intuition would be that if something was registered as a secret in session 1, we shouldn't log it as plaintext in session 2.

@blva blva removed their request for review June 3, 2026 10:15
@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

This PR has gone 30 days without any activity and meets the project's definition of "stale". This will be auto-closed if there is no new activity over the next 30 days. If the issue is still relevant and active, you can simply comment with a "bump" to keep it open, or add the label "not_stale". Thanks for keeping our repository healthy!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants