fix(security): keep implicit quarantine across server restarts and config writes (Spec fix-quarantine-restart) - #1463
Merged
Conversation
…ntine survives RestartServer re-reads mcp_config.json (#467) and persisted the raw entry of a server that never stated quarantined, erasing the quarantine the config-load admission gate recorded. The entry now goes through the same gate before it is persisted or used, and fails closed when storage is unreadable.
…ecision SaveUpstreamServer and the async saveServerSync now keep a stored Quarantined=true unless the incoming config states quarantined explicitly. The read-check-write runs in one bbolt transaction so a concurrent QuarantineUpstreamServer is not lost. Tests that un-quarantined through a plain save now mark the decision explicit.
…tine The REST PATCH handler marks the explicit bit when the body carries quarantined, and UpdateServer applies the value only then, so an unrelated PATCH can no longer copy a stale false over the stored record.
Deploying mcpproxy-docs with
|
| Latest commit: |
0edf3da
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://89fd7812.mcpproxy-docs.pages.dev |
| Branch Preview URL: | https://fix-quarantine-restart-bypas.mcpproxy-docs.pages.dev |
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Contributor
📦 Build ArtifactsWorkflow Run: View Run Available Artifacts
How to DownloadOption 1: GitHub Web UI (easiest)
Option 2: GitHub CLI gh run download 36994351547 --repo smart-mcp-proxy/mcpproxy-go
|
…one in binding guard e2e
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Security fix. Restarting a server that the config-load admission gate (#937) had quarantined silently cleared its quarantine, and the next unrelated config write, reload or process restart then admitted it and auto-approved its tools. This is not a Spec 108/109 task, so no spec documents change and no names or contracts change on any surface (REST, CLI, MCP, Web UI, macOS app are unaffected apart from the corrected behavior).
Root cause
lookupServerConfigForRestartininternal/runtime/lifecycle.gois the #467 "disk-first restart" helper. It re-readsmcp_config.json, writes the raw file entry to config.db withSaveUpstreamServer, and hands the same entry to the upstream manager. A server entry that never statedquarantineddecodes as not quarantined with no explicit bit. The admission gate had quarantined it at load and recorded that in config.db, so the restart overwrote the recorded quarantine with false. The gate's durability depends on config.db: its "known server" branch only re-holds a server when the stored record is quarantined and the config states nothing. After the overwrite, the nextSaveConfiguration(approving or disabling a different server, a PATCH, enable/disable) rebuilt the file and snapshot from storage without the quarantine, and any later reload or reboot treated the server as known, live and vetted. The restarted client was also created as trusted until the next reconcile.Affected paths
Everything that reaches
Runtime.RestartServer: REST restart and restart_all,mcpproxy upstream restart(including--all), MCPupstream_servers operation=restart, the tray restart, secret changes that restart affected servers, the scanner'sEnsureConnected(manual scan and the automatic baseline sweep shortly after startup), and the server-edition admin handler. The bug has existed since the gate was added in #944, which did not gate this disk read.Fix
SaveUpstreamServerand the asyncsaveServerSyncnever lower a stored quarantine unless the incoming config statesquarantinedexplicitly. The read-check-write happens inside one bbolt transaction, so a concurrentQuarantineUpstreamServercannot be lost. The guard changes only the persisted record, never the caller's struct (which may be a published snapshot pointer), and logs a warning naming the server.quarantined, andUpdateServerapplies the value only then. Previously an unrelated PATCH could copy a stale false from a config snapshot over the stored record.Decisions
"quarantined": falsein the file, or an explicit PATCH, is still obeyed (operator statement keeps its bug: mcp_config.json file-watcher hot-reload doesn't propagate env (and likely headers) changes to running upstreams #467 and security: servers loaded from mcp_config.json bypass the trust-mode admission gate entirely #937 meaning).Runtime.QuarantineServer(name, false)(security approve, tray, REST unquarantine, scan-mode auto-approve) goes throughQuarantineUpstreamServer, which the guard does not touch.docs/features/security-quarantine.mdnow says so.Tests
internal/runtime/restart_quarantine_test.go: restart keeps implicit quarantine (storage, published config and the written file, including after approving a different server), keeps it across a simulated reboot with no tool approvals promoted, gates a first-seen server restarted from disk, obeys an explicit false on disk, does not over-quarantine undertrust_mode: auto, and fails closed when storage is unreadable.internal/storage/quarantine_guard_test.goandasync_ops_test.go: the guard never lowers without an explicit decision (Go-built and JSON-decoded explicit false both lower), new servers and raises are unaffected, the caller's struct is not mutated,QuarantineUpstreamServerstill un-quarantines, and a concurrent quarantine is not lost under-race.internal/httpapi/patch_server_test.go: the explicit bit is set only when the body carriesquarantined.internal/server/update_server_quarantine_test.go: an update that omits quarantine keeps the stored value; an explicit false un-quarantines and is written to the file.go test -raceon runtime, storage, httpapi, server, management, security and config, the server-edition lane with the CI skip regex, the server E2E tests, and golangci-lint (bare and--build-tags server).Follow-up (medium/low, not in this PR)
REST PATCH
quarantined:falsestill bypasses the baseline-approve and activity event thatQuarantineServerperforms; consider routing it throughQuarantineServer.