[Security 4/9] MCP tools require confirmation unless positively known-safe - #247
[Security 4/9] MCP tools require confirmation unless positively known-safe#247amal66 wants to merge 1 commit into
Conversation
Split out of the security pack: the tool-confirmation half of 4c44c15. Adapted-from: Open-Legal-Products#227
908e986 to
ae67dad
Compare
|
Converting to draft — an adversarial re-review I ran found a confirmed regression: since no runtime confirmation flow exists yet, |
willchen96
left a comment
There was a problem hiding this comment.
I don’t think this should merge in its current form. I found three blocking issues:
-
There is no user-confirmation flow.
toolRequiresConfirmation()setsrequires_confirmation, but tools with that value are then disabled during refresh, their UI toggle is disabled, the backend rejects attempts to enable them, and both tool discovery and execution filter them out. The user never receives an approval prompt and has no way to approve a call. In practice, “confirmation required” currently means “permanently unavailable.” -
The MCP defaults are interpreted incorrectly. The MCP specification says an omitted
openWorldHintdefaults totrue. However, the new implementation treats a missing value likefalse, so{ readOnlyHint: true }is allowed to run automatically. If the policy requires a positively known closed-world tool, it must requireopenWorldHint === falseexplicitly, and the corresponding test should be reversed. -
External annotations are not proof that a tool is safe. The MCP server controls
readOnlyHint,destructiveHint, andopenWorldHint, and a malicious or incorrectly implemented server can lie. Custom or untrusted connectors should require per-call approval regardless of their annotations. Automatic execution should additionally require a locally controlled trust decision or reviewed allowlist.
To match the PR description, the implementation needs a real pending-call approval flow: store the exact proposed tool and arguments, show them to the user, bind approval to that user/chat/payload, make it short-lived and single-use, and execute only after approval.
Until that exists, the behavior should be described as “blocked by policy,” not “confirmation required.” The narrower classification fix should also require an explicit closed-world declaration:
const annotationSafe =
annotations?.readOnlyHint === true &&
annotations?.openWorldHint === false &&
annotations?.destructiveHint !== true;The backend build and test suite pass, but the tests currently validate the incorrect missing-openWorldHint behavior and do not test an end-to-end user approval because no such path exists.
[Security 4/9] MCP tools require confirmation unless positively known-safe
TL;DR
Flip the tool-confirmation default. Previously a connector tool ran without confirmation unless it declared itself destructive. Now a tool requires confirmation unless it is positively known-safe — i.e. it explicitly claims
readOnlyHint: true, is not flagged destructive, and is not open-world. Anything absent or ambiguous is gated.Risk to user data
Severity: medium–high, and this is a key defense-in-depth layer behind prompt injection (see [Security 5/9], spotlighting). Tool annotations (
readOnlyHint/destructiveHint/openWorldHint) are advisory and controlled by the external MCP server — a hint, not a guarantee. If the LLM is tricked (or a connector is malicious/mis-annotated) into calling a side-effecting tool, the cost in a legal product is severe: exfiltrating a privileged document, mutating a matter, hitting an unknown external system. Requiring positive proof-of-safety means a bad or missing annotation fails toward a confirmation click, not silent execution.Flows affected
toolRequiresConfirmationinmcp/client.ts).Attack precedent
This is the "human in the loop for consequential actions" mitigation from the OWASP Top 10 for LLM Applications (excessive agency / insecure output handling). The general lesson — never trust a security decision to a field the other side controls — is the same failure behind capability-confused deputies.
Possible fixes, and what we chose
destructiveHint(old policy)openWorldHintreadOnlyHint===true && !destructive && !openWorldflowchart TD T["tool call requested"] --> Q{readOnlyHint === true<br/>AND not destructive<br/>AND not open-world?} Q -- yes --> Run["run without confirmation"] Q -- "no / missing / ambiguous" --> Ask["require user confirmation"] Ask -- approved --> Run Ask -- denied --> Stop["do not run"]The subtlety encoded in the tests:
readOnlyHintmust be explicitlytrue— merely absent is treated as untrusted, so a tool that says nothing is gated, not trusted.What's in this PR
backend/src/lib/mcp/client.ts—toolRequiresConfirmationinverted to known-safe.confirmation.test.ts(9 tests covering each annotation combination, incl. the missing-hint case).Reading
OWASP Top 10 for LLM Applications · Confused deputy problem