v0.1.30
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 --applyandmms host sync --applyfeddiscover("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 — andmms host sync's confirmation gate covered only the REMOVE/RESTAMP buckets, leaving ADD entirely ungated. A newis_repo_localmarker onImportCandidateis 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 inmms host sync— unless the new--allow-project-configsflag 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.jsonand 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-clientsare unaffected (explicit selection flow). Behavior change:mms import --apply/mms host sync --applynow abort before writing when a candidate originates from a project-local file, unless--allow-project-configsis passed. mms status --json/mms list --jsonmaskenvandheadersvalues (GHSA-fpm7-rf53-vjc9) —_redacted_servers_jsonstripped the importedorigin.originalblock (#476) but passed each server entry's own activeenvandheadersstraight through to--json. That machine-readable output is routinely piped to scripts, CI logs, issue comments, and agent transcripts, so a token stored inenv/headerscould leak verbatim. Everyenv/headersvalue is now masked (keys preserved) so the JSON surface is log-safe; all values are redacted rather than only classifier-flagged ones, to catchCookie-style headers and short/punctuated secrets a key/value heuristic would miss. Because--jsonreads the raw config (not a validatedUpstreamServerConfig), a hand-edited / corrupted non-dictenv/headersis 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:--jsonno longer emitsenv/headersvalues (the keys remain).- Surfacing daemon rejects a non-loopback
hostbind (GHSA-wf6f-mgj9-wf8r) —DaemonConfig.hosthad no validation, soMEMTOMEM_STM_DAEMON__HOST=0.0.0.0(or an empty string, whichasyncio.start_servertreats 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-loopbackhostunless the newdaemon.allow_non_loopback=trueopt-in is set; loopback is detected viaipaddress.ip_address(host).is_loopbackplus a literal accept for the hostnamelocalhost, so the whole127.0.0.0/8range and IPv6::1forms stay valid whilehost=""and0.0.0.0are treated as non-loopback.SECURITY.mdis updated accordingly. Behavior change: a config settingdaemon.hostto a non-loopback address now fails at load unlessdaemon.allow_non_loopback=true.