Skip to content

TPA scanner: unvalidated trust_mode, invisible signature bundle (FR-019 unimplemented), and hold state missing from CLI/UI surfaces #938

Description

@Dumbris

Three findings from the same end-to-end QA pass that produced #937. None is a security break — the runtime fails closed in every case — but each misleads an operator about what the scanner is actually doing.

1. MEDIUM — REST accepts any string as trust_mode, with no validation

curl -X PATCH -H "X-API-Key: ..." .../api/v1/servers/<name> -d '{"trust_mode":"yolo"}'
→ {"success": true, "message": "Server '<name>' updated successfully"}

GET /servers then echoes trust_mode: "yolo" verbatim, and it is persisted to mcp_config.json.

The runtime does fail closed — a poisoned tool change under "yolo" is correctly held, and so is one under "ScAn" — so this is not exploitable. The problem is that an operator who typos "Scan" silently gets manual behaviour while the API and UI display their typo back to them as though it were a valid mode. Someone intending scan and typing Scan would believe scanning is on.

Expected: reject with 400, or normalize and warn.

2. MEDIUM — the signature bundle is invisible to operators, and FR-019 is unimplemented

Three related parts:

FR-019 is not implemented. The bundle is go:embed-only (internal/security/scanner/tpa_bundle.go:22). There is no config field, no env override, no filesystem load and no hot-reload. FR-019 requires all of these and states the path "MUST NOT be hardcoded". Verified by grep: zero hits for any bundle-path config or env key.

The one status report never reaches a log. tpa_bundle.go:190 logs "loaded embedded TPA scanner bundle" with runnable/skipped counts, but does so via the unconfigured global zap.L(). Searching every console and file log across four instances — including one started with --log-level=debug — produced zero occurrences. Repro: start the core with --log-level=debug, run any scan, then grep -r "TPA scanner bundle\|runnable_rules" <logdir> → nothing.

No freshness signal exists anywhere. There is no bundle version or signature count in security overview, security scanners, the REST API, or the UI — and the bundle format itself carries no generated_at, expires, or equivalent. A years-stale corpus is indistinguishable from a freshly exported one, both to a human and to any automation.

The practical consequence: an operator cannot answer "which signatures is my proxy actually running, and how old are they?" by any supported means.

3. LOW — CLI and UI hide hold state in places where an operator would look

mcpproxy tools list --server <name> shows no HELD column and no approval status — only the global mcpproxy tools list carries it. Worse, the server-scoped view prints the raw poisoned description unescaped to the terminal, where the global view correctly escapes control characters.

Repro: create a held changed tool, then compare mcpproxy tools list --server <name> (no state shown) against mcpproxy tools list (HELD populated).

mcpproxy upstream list shows a green ✅ Connected (1 tool) while a poisoned tool sits held on that server.

The Web UI server card reads "Clean" — a green shield from the last full server scan — directly above the text "1 tool changed since approval — re-review needed", while the tool-level gate is holding that tool with a dangerous verdict and two TPA signature hits. The juxtaposition reads as reassurance next to a warning.

No --trust-mode flag on mcpproxy upstream add. The mode can only be set by pre-seeding config or by a follow-up REST PATCH, so a server cannot be added directly into scan mode from the CLI.

Context

The hold-evidence surfacing itself is good where it exists — the global tools list renders matched TPA ids first so truncation never drops them (FR-018 confirmed working), and the Web UI hold panel with its signature chips and before/after description diff is genuinely useful. These findings are about the places that state does not reach.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions