Skip to content

Hardening: Tool-definition authorization surfaces and parameter bounds on reference servers #4958

Closed as duplicate

Description

@zariffromlatif

Hello maintainers,

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.

Zarif Latif
Founder, Railo & LIFE FORGE
zarif.latif.biz@gmail.com
https://github.com/zariffromlatif/life-forge

Activity

  1. jarrettdustinqq commented on Oct 4, 2026

    @jarrettdustinqq

    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.

    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 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:

    1. Test whether representative hosts surface a confirmation for the existing destructiveHint: true; any gap found there belongs to host behavior.
    2. If the server itself needs policy enforcement, specify the independent authority and replay/scope semantics rather than a caller-controlled Boolean.
    3. 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.

  2. zariffromlatif commented on Oct 4, 2026

    @zariffromlatif
    Author

    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: 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:

    1. Host-Level Evaluation: Auditing whether representative client hosts actually surface interactive confirmation when destructiveHint: true is advertised.
    2. 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.

  3. zariffromlatif commented on Oct 16, 2026

    @zariffromlatif
    Author

    Correction and update. My original report overstated this, and @jarrettdustinqq's review was right.

    1. 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.
    2. 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.
    3. The suggested fix. I withdraw it. A confirm: true argument 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

  4. SarveshShukla23 commented on Oct 17, 2026

    @SarveshShukla23

    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:

    1. Canonical & Shannon Entropy Inspection: Unescapes nested \uXXXX / double URL-encoding and blocks parameter payloads exceeding 6.2 bits/byte (catching base64/hex credential chunking).
    2. 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

  5. cliffhall commented on Oct 19, 2026

    @cliffhall
    Member

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    duplicateThis issue or pull request already exists

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions