Skip to content

v0.1.30

Choose a tag to compare

@memtomem memtomem released this 30 Jun 04:19
3e585b5

A security-hardening release. Three coordinated fixes close an ungated
project-local config adoption path during import / host-sync, env / headers
leakage in --json output, and a non-loopback bind of the surfacing daemon.
Each ships behind an explicit opt-in so an existing trusted workflow can
re-permit the prior behavior, and none changes the proxy runtime.

These fixes were developed and merged through GitHub's private security-fork
flow, so they carry no public PR number; citations below are the GitHub
Security Advisory IDs (GHSA-…).

Security

  • Project-local MCP configs are gated before registry adoption during import / host-sync (GHSA-9hjq-vxq2-36px) — mms import --apply and mms host sync --apply fed discover("all", cwd) into the global registry, which reads project-local config files under the current directory (<cwd>/.mcp.json, <cwd>/.cursor/mcp.json). An untrusted repository checkout can ship those files, so adopting them without acknowledgement let a checkout register a command that later runs with the user's privileges — and mms host sync's confirmation gate covered only the REMOVE/RESTAMP buckets, leaving ADD entirely ungated. A new is_repo_local marker on ImportCandidate is set only at the two scanner sites that read a file under cwd; both registry-writing buckets now fail closed before any write when they adopt a repo-local candidate — ADD (new) in both commands, and (under --force) RESTAMP in mms host sync — unless the new --allow-project-configs flag is passed. The marker keys on the source file location, not the source label: Claude Code (project) entries live in the user's home ~/.claude.json and a repo checkout cannot ship them, so they stay ungated; sidecar BACKFILL stays ungated (it records a drift hash, not a registry command); mms init / mms add --from-clients are unaffected (explicit selection flow). Behavior change: mms import --apply / mms host sync --apply now abort before writing when a candidate originates from a project-local file, unless --allow-project-configs is passed.
  • mms status --json / mms list --json mask env and headers values (GHSA-fpm7-rf53-vjc9) — _redacted_servers_json stripped the imported origin.original block (#476) but passed each server entry's own active env and headers straight through to --json. That machine-readable output is routinely piped to scripts, CI logs, issue comments, and agent transcripts, so a token stored in env / headers could leak verbatim. Every env / headers value is now masked (keys preserved) so the JSON surface is log-safe; all values are redacted rather than only classifier-flagged ones, to catch Cookie-style headers and short/punctuated secrets a key/value heuristic would miss. Because --json reads the raw config (not a validated UpstreamServerConfig), a hand-edited / corrupted non-dict env / headers is replaced wholesale with the sentinel rather than emitted verbatim. Redaction is output-only — the function returns shallow copies and the on-disk config is unchanged. Behavior change: --json no longer emits env / headers values (the keys remain).
  • Surfacing daemon rejects a non-loopback host bind (GHSA-wf6f-mgj9-wf8r) — DaemonConfig.host had no validation, so MEMTOMEM_STM_DAEMON__HOST=0.0.0.0 (or an empty string, which asyncio.start_server treats as all-interfaces) would bind the surfacing daemon to a non-loopback interface with no pushback. The daemon is local-only and authenticated by a per-start random token, not network ACLs, so a non-loopback bind widens the attack surface from local processes to network-adjacent clients. A model validator now rejects a non-loopback host unless the new daemon.allow_non_loopback=true opt-in is set; loopback is detected via ipaddress.ip_address(host).is_loopback plus a literal accept for the hostname localhost, so the whole 127.0.0.0/8 range and IPv6 ::1 forms stay valid while host="" and 0.0.0.0 are treated as non-loopback. SECURITY.md is updated accordingly. Behavior change: a config setting daemon.host to a non-loopback address now fails at load unless daemon.allow_non_loopback=true.