MCP stdio 不是安全沙箱:工具、正文和 Token 的边界怎么划

MCP 工具安全边界封面

很多 MCP Server 选择 stdio,是因为它不需要额外监听一个网络端口。这个选择确实减少了网络暴露面,但很容易引出一个危险误解:既然请求只从标准输入进来,Server 就天然安全。

事实并非如此。宿主进程仍然可以调用工具,工具仍然可能读文件、访问数据库、发起网络请求或执行写操作;一旦 Server 把完整正文、Token、Cookie 或内部异常原样返回,敏感信息照样会越过进程边界。

核心判断是:stdio 是传输方式,不是授权系统,更不是安全沙箱。 真正的边界应由窄工具、类型化输入、服务端授权、输出过滤和独立批准共同构成。

问题、代价与适用边界

把 Web 应用能力暴露为 MCP 工具时,最省事的做法通常是“一层薄包装”:收到参数后直接调用现有函数,再把结果完整返回。它的问题不在于代码短,而在于没有重新审视信任边界。

至少要回答五个问题:

  1. 调用者能选择哪些动作?
  2. 参数是否限制了路径、数量和数据范围?
  3. 服务端是否重新校验权限,而不是相信模型声明?
  4. 返回值会不会夹带 Token、Cookie、正文或内部绝对路径?
  5. 写操作是否需要独立、短期、一次性的批准?

stdio 适合本地宿主与子进程之间的 MCP 通信,也适合把远程传输关闭作为默认值。但如果工具本身拥有广泛文件权限或高权限凭据,换成 stdio 并不会缩小工具的实际能力。

MCP 工具安全的四层边界

核心结论与关键概念

可以把安全边界分成四层:

  • 传输层:stdio 默认不监听远程端口;
  • 工具层:每个工具只表达一个窄动作,不提供任意命令或任意路径;
  • 应用层:所有入口复用同一服务层规则,权限不能只写在 CLI、API 或 MCP 外壳里;
  • 发布层:公开、支付、删除等重要副作用使用独立批准,并绑定对象、修订和有效期。

RuyiBookCourse 的 FastAPI MCP 网关章节强调,把 Web 应用功能变成 MCP 工具,不是简单暴露内部函数,而是把已有业务能力映射为边界清晰的工具。真实项目里的一个可验证做法是:CLI、loopback API 和 stdio MCP 都调用同一个应用服务,业务规则不在入口层复制;远程 API 默认关闭,启用远程绑定时必须显式配置 Token。

可复现的实现步骤

第一步:定义窄输入模型

不要接收一个万能 payload,而应显式约束字段:

from pydantic import BaseModel, Field


class ArticleQuery(BaseModel):
    article_id: str = Field(min_length=1, max_length=64)
    include_metrics: bool = False

如果工具允许文件路径,服务端还要把解析后的路径限制在已授权根目录,并拒绝 ..、绝对路径和符号链接逃逸。

第二步:让所有入口调用同一用例

def get_article_tool(query: ArticleQuery, service: ArticleService) -> dict:
    article = service.get_visible_article(
        article_id=query.article_id,
        include_metrics=query.include_metrics,
    )
    return present_article(article)

权限判断在 get_visible_article 内完成。这样 CLI、API 和 MCP 不会因为各写一套条件而逐渐漂移。

第三步:只返回白名单字段

def present_article(article) -> dict:
    return {
        "articleId": article.article_id,
        "title": article.title,
        "status": article.status,
        "updatedAt": article.updated_at.isoformat(),
    }

不要直接 model_dump() 后返回全部对象。输出白名单能挡住以后新增的内部字段意外穿透边界。

第四步:敏感信息只作为进程内配置

Token、Cookie 和浏览器状态不应进入工具参数、日志或持久化回执。工具只返回“是否配置”“是否授权”这类最小状态,不返回具体值。异常输出也应经过统一映射,避免堆栈和工作站路径泄漏。

第五步:把高风险写操作拆成两段

prepare -> approve(object, revision, hash, expiry) -> execute once -> read back

批准令牌绑定对象 ID、revision、内容哈希和过期时间,并且只能使用一次。执行后必须回读真实结果;超时不能自动重放,而应进入待核对状态。

MCP 高风险操作的一次性批准链

风险、失败恢复与反例

反例一:一个工具接收任意命令

即使通过 stdio 调用,run(command: str) 仍然等价于把宿主权限交给调用者。更安全的做法是拆成有限动作,并为每个动作定义输入模型。

反例二:客户端说“我有权限”

模型输出和客户端参数都不可信。权限必须由服务端根据真实身份、对象状态和策略重新判断。

反例三:完整对象直接返回

内部对象今天也许没有秘密字段,明天可能新增 tokenstorage_state 或绝对路径。白名单展示层能把这种演化风险隔离在服务内部。

反例四:超时后立即重试写操作

调用方未收到结果,不等于服务端没有完成。恢复时先查询对象状态、审计回执和幂等键;事实不明确时停止,而不是猜测失败。

验收清单与参考依据

  • stdio 模式不监听远程端口;
  • 工具列表中没有任意命令、任意 SQL 或任意路径入口;
  • 所有参数都有类型、长度、数量和范围限制;
  • CLI、API、MCP 复用同一应用服务规则;
  • 输出采用字段白名单,错误不返回堆栈和绝对路径;
  • Token、Cookie、完整正文和浏览器状态不进入工具参数与日志;
  • 写操作需要独立批准,并绑定对象、修订、哈希和有效期;
  • 模拟超时后会先只读复核,不会自动重放;
  • 回归测试覆盖未授权、越界路径、敏感输出和重复使用批准令牌。

知识依据来自 RuyiBookCourse《大鹏 FastAPI MCP 网关实战》中“把 Web 应用功能变成 MCP 工具”一节;工程依据来自真实项目的 stdio MCP、loopback API、安全契约实现及相关 API 测试。

收束

选择 stdio 是一个好的默认传输决策,但它只解决“怎么连”,没有回答“能做什么、能看到什么、谁来批准”。把这三件事写进服务端规则,MCP 工具才真正具备可审计的安全边界。

如果你正在设计 MCP Server,可以从工具清单中权限最宽的那一个开始审计:它的输入、输出和副作用分别由谁约束?

发布前门禁

  • 保留知识或官方依据
  • 代码与命令可以复核
  • 不写目标平台运营细节
  • 本轮无已认领实验,不自行补写实验结论
Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐