fix: scope backup_diff to server sections and mask secrets
The diff preview in RestoreDialog was serializing the full config dict,
exposing secret env values and token args in cleartext on a pasteable surface.
- Add _redact_server_data / _redact_servers_block helpers that apply
redact_args to args and mask env values for is_secret_key() keys
- Rewrite backup_diff to compare only {mcpServers, _disabledMcpServers}
sections (sanitized), not the whole file — also avoids double-serializing
multi-MB ~/.claude.json for a servers-only diff
- Add clarifying comment in _restore_from_backup about why full_config
is the right base after confirm-discard
- Add test: backup_diff with secret args/env → MASK in output, raw values absent
- Add test: restore_backup restores _disabledMcpServers correctly
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -433,3 +433,70 @@ def test_restore_with_current_cfg_passed(config_with_backup):
|
||||
restored = json.loads(config_with_backup.read_text())
|
||||
assert restored.get("extraKey") == "preserved"
|
||||
assert "server-v1" in restored.get("mcpServers", {})
|
||||
|
||||
|
||||
def test_backup_diff_masks_secrets(tmp_path):
|
||||
cfgpath = tmp_path / "cfg.json"
|
||||
current = {
|
||||
"mcpServers": {
|
||||
"s": {
|
||||
"command": "node",
|
||||
"args": ["--token", "super-secret"],
|
||||
"env": {"API_KEY": "my-api-key-value"},
|
||||
}
|
||||
}
|
||||
}
|
||||
cfgpath.write_text(json.dumps(current, indent=2))
|
||||
bdir = cfgpath.parent / c.BACKUP_DIRNAME
|
||||
bdir.mkdir()
|
||||
import time as _t
|
||||
|
||||
bp = bdir / f"cfg.{_t.strftime('%Y%m%d-%H%M%S')}.json"
|
||||
bp.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"mcpServers": {
|
||||
"s2": {
|
||||
"command": "python",
|
||||
"args": ["--api-key", "another-secret"],
|
||||
"env": {"SECRET_KEY": "backup-secret-value"},
|
||||
}
|
||||
}
|
||||
},
|
||||
indent=2,
|
||||
)
|
||||
)
|
||||
diff = c.backup_diff(cfgpath, bp)
|
||||
# Raw secret values must not appear in the diff
|
||||
assert "super-secret" not in diff
|
||||
assert "my-api-key-value" not in diff
|
||||
assert "another-secret" not in diff
|
||||
assert "backup-secret-value" not in diff
|
||||
# The mask placeholder must appear (secrets were present in both sides)
|
||||
assert c.MASK in diff
|
||||
|
||||
|
||||
def test_restore_backup_restores_disabled_servers(tmp_path):
|
||||
cfgpath = tmp_path / "cfg.json"
|
||||
original = {
|
||||
"mcpServers": {"active": {"command": "node"}},
|
||||
c.DISABLED_KEY: {"parked": {"command": "python"}},
|
||||
}
|
||||
cfgpath.write_text(json.dumps(original, indent=2))
|
||||
|
||||
# Overwrite: add new server, drop the disabled one
|
||||
cfg = c.load_config(cfgpath)
|
||||
servers = c.extract_servers(cfg)
|
||||
servers = [s for s in servers if s.name != "parked"]
|
||||
servers.append(c.ServerEntry("newcomer", {"command": "uvx"}, True))
|
||||
c.apply_servers(cfg, servers)
|
||||
c.write_config(cfgpath, cfg)
|
||||
|
||||
# Restore from the v1 backup
|
||||
backups = c.list_backups(cfgpath)
|
||||
c.restore_backup(cfgpath, backups[0])
|
||||
restored = json.loads(cfgpath.read_text())
|
||||
|
||||
assert "parked" in restored.get(c.DISABLED_KEY, {})
|
||||
assert "active" in restored.get("mcpServers", {})
|
||||
assert "newcomer" not in restored.get("mcpServers", {})
|
||||
|
||||
Reference in New Issue
Block a user