You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Hardening: Tool-definition authorization surfaces and parameter bounds on reference servers #4958
I run LIFE FORGE, an MIT-licensed open-source security scanner for MCP tool definitions (github.com/zariffromlatif/life-forge). I recently scanned the most-installed servers in the ecosystem and want to share the findings on your reference implementations directly, before publishing summaries elsewhere.
What the scanner flagged on server-filesystem (probed live over stdio, definitions read off the wire):
write_file performs a state-mutating operation with no confirmation, approval-token, or dry-run argument in its schema. Under indirect prompt injection - an attacker-controlled document instructing the agent to write a file - the only gate is the model's own judgment.
All 14 tools declare string parameters without maxLength/pattern bounds, which admits arbitrary-length payloads into the agent's context.
On server-git: all 12 tools declare unbounded string parameters. No destructive-capability flags - the concern here is parameter bounding only.
These are design-level observations about the definitions, not claims of an exploitable vulnerability, and I recognize reference servers trade hardening for pedagogical clarity. But they are also the templates people copy, and the fix pattern (a confirm: true argument enforced server-side; maxLength on path/string parameters) costs a few lines per tool and would set the ecosystem standard.
Full report: results/MCP_ECOSYSTEM_SCAN.md in the repository above (per-server JSON evidence included; the ruleset is versioned and findings reproduce deterministically). If you believe any finding is wrong, I will re-run and publish a correction. I have not posted about your servers individually and will hold individual publicity until you have had a reasonable window.
I think the two findings need to be separated, because the proposed confirm: true fix would not enforce the authorization property described in the threat model.
On current main, write_file already:
says it overwrites without warning;
is confined by validatePath to the configured allowed directories; and
advertises readOnlyHint: false and destructiveHint: true.
A Boolean confirm argument has the same principal as path and content: under the stated indirect-prompt-injection case, the agent can simply send confirm: true. Unless the server validates an out-of-band, principal-issued approval token or a separate policy decision, that is self-attestation, not an additional gate. I would avoid adding it to a reference implementation as a hardening pattern.
A narrower actionable split might be:
Test whether representative hosts surface a confirmation for the existing destructiveHint: true; any gap found there belongs to host behavior.
If the server itself needs policy enforcement, specify the independent authority and replay/scope semantics rather than a caller-controlled Boolean.
Treat parameter limits per field and resource boundary. A path-length or batch-count ceiling can be a defensible server limit. A generic maxLength on file content changes the tool's supported payload size, and schema validation happens only after the request has already been generated and transported, so it does not by itself establish protection of the model context. Concrete byte/count limits should be tied to measured filesystem/server constraints.
I reviewed current source and the cited MCP guidance; I did not run LIFE FORGE or exercise a host's confirmation UI.
AI assistance disclosure: an authorized ChatGPT assistant inspected the current reference-server source and MCP guidance and drafted this review; I reviewed the evidence and scope represented here.
Thank you for this exceptionally thorough and insightful review. Your analysis is spot on, particularly regarding the self-attestation flaw of a caller-provided Boolean.
1. The Self-Attestation Problem (confirm: true)
You are completely right: under an indirect prompt injection threat model, a naive confirm: true argument generated by the same principal (the LLM) is purely self-attestation. If the model is coerced into calling write_file(path='/important/file', content='...'), an attacker-crafted prompt will trivially append confirm=True. It does not create an independent authorization boundary.
In our broader runtime architecture (e.g., our PolicyGateway and @tool_guard wrappers), real gating requires an out-of-band policy oracle or a non-forgeable approval token (such as a human-in-the-loop signature or capability ticket) that the agent cannot synthesize itself. Suggesting a simple Boolean confirm in reference schemas conflates client-side UX hints with cryptographic/principal authorization.
2. Annotations vs. Authorization Contracts
Your point regarding MCP's tool annotations (destructiveHint: true and readOnlyHint: false) and the cited guidance is crucial:
Annotations serve as hints for trusted hosts (e.g. Claude Desktop or IDEs) to prompt users with confirmation dialogs.
However, as noted in the guidance, they are advisory hints, not an authorization contract enforced by the transport or runtime.
If a downstream host ignores destructiveHint or auto-approves tool calls, the server remains completely unconstrained within validatePath.
Your suggested split is very compelling:
Host-Level Evaluation: Auditing whether representative client hosts actually surface interactive confirmation when destructiveHint: true is advertised.
Independent Authority: If servers implement security controls, they must rely on out-of-band tokens or capability boundaries rather than caller-controlled arguments.
3. Parameter Bounds Scope
I also agree with your distinction on parameter limits:
Server-side parameter validation should enforce concrete filesystem and OS resource constraints (e.g., maxPathLength, batch size, allowable character sets).
Context-flooding and payload size protection are host/transport concerns that belong in the client or proxy layer before tokens are transported or ingested into model context.
Next Steps for Our Ruleset
Based on your feedback, we are updating the scanner heuristics in LIFE FORGE:
Distinguishing between caller-controlled Booleans (self-attestation) versus out-of-band capability tokens (approval_token, ticket_id).
Evaluating MCP tool annotations (destructiveHint, readOnlyHint) as host-facing contract signals rather than conflating them with server-side argument schemas.
Really appreciate you taking the time to review the source and provide this level of architectural clarity.
Correction and update. My original report overstated this, and @jarrettdustinqq's review was right.
Scoring. The first version banded servers by finding volume, so server-filesystem and server-git were labelled CRITICAL with no CRITICAL-severity finding. That was a scoring bug, and it's fixed.
Annotations. My scanner ignored MCP tool annotations. It now reads them: a destructive tool that declares destructiveHint: true is rated MEDIUM, not HIGH, because the host can ask the user first. On current main (bbb701e), write_file, edit_file and move_file all declare it, and validatePath confines them, so server-filesystem has no HIGH finding. server-git never had one.
The suggested fix. I withdraw it. A confirm: true argument is self-attestation, because the manipulated model sets it too.
Relying on static startup allowlists or client-side IAM scopes leaves local stdio child processes exposed to Time-of-Check to Time-of-Use (TOCTOU) argument manipulation. Per the MCP Security Best Practices, once an upstream context window ingests an indirect prompt injection, the agent invokes an already-authorized tool with destructive arguments over the raw OS pipe.
When we benchmarked a 10-agent swarm against the executable Berkeley Function Calling Leaderboard (BFCL) dataset over stdio, static tool allowlists passed 108 unauthorized mutations and high-entropy exfiltrations.
To solve parameter authorization without breaking developer workflows, we built a pure-Go L7 stdio interceptor (mcp-proxy-fips) that enforces checks inline before the frame hits the upstream server:
Ephemeral Wasm Micro-VM Pre-Detonation: Detonates high-risk tools/call arguments inside a zero-dependency wazero sandbox with zero host mounts in <816ms before triggering an out-of-band native OS (osascript/PowerShell) approval modal.
Full architecture breakdown and the 10-agent BFCL benchmark specs are documented here if useful for anyone hardening local stdio deployments: https://getrcf.dev/product
Activity
jarrettdustinqq commentedon Oct 4, 2026
I think the two findings need to be separated, because the proposed
confirm: truefix would not enforce the authorization property described in the threat model.On current
main,write_filealready:validatePathto the configured allowed directories; andreadOnlyHint: falseanddestructiveHint: true.Source: https://github.com/modelcontextprotocol/servers/blob/main/src/filesystem/index.ts#L355-L383
Those annotations can help a trusted host choose confirmation UX, but they are not an authorization contract. MCP's own annotations guidance makes the same distinction: if correctness depends on a field being true, enforcement belongs in the authorization layer, transport, or runtime. See https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/blog/content/posts/2026-03-16-tool-annotations.md#5-is-it-a-hint-or-a-contract
A Boolean
confirmargument has the same principal aspathandcontent: under the stated indirect-prompt-injection case, the agent can simply sendconfirm: true. Unless the server validates an out-of-band, principal-issued approval token or a separate policy decision, that is self-attestation, not an additional gate. I would avoid adding it to a reference implementation as a hardening pattern.A narrower actionable split might be:
destructiveHint: true; any gap found there belongs to host behavior.maxLengthon filecontentchanges the tool's supported payload size, and schema validation happens only after the request has already been generated and transported, so it does not by itself establish protection of the model context. Concrete byte/count limits should be tied to measured filesystem/server constraints.I reviewed current source and the cited MCP guidance; I did not run LIFE FORGE or exercise a host's confirmation UI.
AI assistance disclosure: an authorized ChatGPT assistant inspected the current reference-server source and MCP guidance and drafted this review; I reviewed the evidence and scope represented here.
zariffromlatif commentedon Oct 4, 2026
Hi @jarrettdustinqq,
Thank you for this exceptionally thorough and insightful review. Your analysis is spot on, particularly regarding the self-attestation flaw of a caller-provided Boolean.
1. The Self-Attestation Problem (
confirm: true)You are completely right: under an indirect prompt injection threat model, a naive
confirm: trueargument generated by the same principal (the LLM) is purely self-attestation. If the model is coerced into callingwrite_file(path='/important/file', content='...'), an attacker-crafted prompt will trivially appendconfirm=True. It does not create an independent authorization boundary.In our broader runtime architecture (e.g., our PolicyGateway and
@tool_guardwrappers), real gating requires an out-of-band policy oracle or a non-forgeable approval token (such as a human-in-the-loop signature or capability ticket) that the agent cannot synthesize itself. Suggesting a simple Booleanconfirmin reference schemas conflates client-side UX hints with cryptographic/principal authorization.2. Annotations vs. Authorization Contracts
Your point regarding MCP's tool annotations (
destructiveHint: trueandreadOnlyHint: false) and the cited guidance is crucial:destructiveHintor auto-approves tool calls, the server remains completely unconstrained withinvalidatePath.Your suggested split is very compelling:
destructiveHint: trueis advertised.3. Parameter Bounds Scope
I also agree with your distinction on parameter limits:
maxPathLength, batch size, allowable character sets).Next Steps for Our Ruleset
Based on your feedback, we are updating the scanner heuristics in LIFE FORGE:
approval_token,ticket_id).destructiveHint,readOnlyHint) as host-facing contract signals rather than conflating them with server-side argument schemas.Really appreciate you taking the time to review the source and provide this level of architectural clarity.
zariffromlatif commentedon Oct 16, 2026
Correction and update. My original report overstated this, and @jarrettdustinqq's review was right.
server-filesystemandserver-gitwere labelled CRITICAL with no CRITICAL-severity finding. That was a scoring bug, and it's fixed.destructiveHint: trueis rated MEDIUM, not HIGH, because the host can ask the user first. On currentmain(bbb701e),write_file,edit_fileandmove_fileall declare it, andvalidatePathconfines them, soserver-filesystemhas no HIGH finding.server-gitnever had one.confirm: trueargument is self-attestation, because the manipulated model sets it too.The only open question is documentation: whether reference servers should say that annotations are UX hints, not an authorization contract, for people who copy them. If you consider that covered, I'll close this. Correction note: https://github.com/zariffromlatif/life-forge/blob/main/results/MCP_ECOSYSTEM_SCAN.md
SarveshShukla23 commentedon Oct 17, 2026
Relying on static startup allowlists or client-side IAM scopes leaves local stdio child processes exposed to Time-of-Check to Time-of-Use (TOCTOU) argument manipulation. Per the MCP Security Best Practices, once an upstream context window ingests an indirect prompt injection, the agent invokes an already-authorized tool with destructive arguments over the raw OS pipe.
When we benchmarked a 10-agent swarm against the executable Berkeley Function Calling Leaderboard (BFCL) dataset over stdio, static tool allowlists passed 108 unauthorized mutations and high-entropy exfiltrations.
To solve parameter authorization without breaking developer workflows, we built a pure-Go L7 stdio interceptor (mcp-proxy-fips) that enforces checks inline before the frame hits the upstream server:
Full architecture breakdown and the 10-agent BFCL benchmark specs are documented here if useful for anyone hardening local stdio deployments: https://getrcf.dev/product
cliffhall commentedon Oct 19, 2026
Thank you for the report. This is already tracked in #3537, so I'm closing this one as a duplicate. Please add anything new you've found there.
This is part of the backlog triage in #4875.