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

很多 MCP Server 选择 stdio,是因为它不需要额外监听一个网络端口。这个选择确实减少了网络暴露面,但很容易引出一个危险误解:既然请求只从标准输入进来,Server 就天然安全。
事实并非如此。宿主进程仍然可以调用工具,工具仍然可能读文件、访问数据库、发起网络请求或执行写操作;一旦 Server 把完整正文、Token、Cookie 或内部异常原样返回,敏感信息照样会越过进程边界。
核心判断是:stdio 是传输方式,不是授权系统,更不是安全沙箱。 真正的边界应由窄工具、类型化输入、服务端授权、输出过滤和独立批准共同构成。
问题、代价与适用边界
把 Web 应用能力暴露为 MCP 工具时,最省事的做法通常是“一层薄包装”:收到参数后直接调用现有函数,再把结果完整返回。它的问题不在于代码短,而在于没有重新审视信任边界。
至少要回答五个问题:
- 调用者能选择哪些动作?
- 参数是否限制了路径、数量和数据范围?
- 服务端是否重新校验权限,而不是相信模型声明?
- 返回值会不会夹带 Token、Cookie、正文或内部绝对路径?
- 写操作是否需要独立、短期、一次性的批准?
stdio 适合本地宿主与子进程之间的 MCP 通信,也适合把远程传输关闭作为默认值。但如果工具本身拥有广泛文件权限或高权限凭据,换成 stdio 并不会缩小工具的实际能力。

核心结论与关键概念
可以把安全边界分成四层:
- 传输层: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、内容哈希和过期时间,并且只能使用一次。执行后必须回读真实结果;超时不能自动重放,而应进入待核对状态。

风险、失败恢复与反例
反例一:一个工具接收任意命令
即使通过 stdio 调用,run(command: str) 仍然等价于把宿主权限交给调用者。更安全的做法是拆成有限动作,并为每个动作定义输入模型。
反例二:客户端说“我有权限”
模型输出和客户端参数都不可信。权限必须由服务端根据真实身份、对象状态和策略重新判断。
反例三:完整对象直接返回
内部对象今天也许没有秘密字段,明天可能新增 token、storage_state 或绝对路径。白名单展示层能把这种演化风险隔离在服务内部。
反例四:超时后立即重试写操作
调用方未收到结果,不等于服务端没有完成。恢复时先查询对象状态、审计回执和幂等键;事实不明确时停止,而不是猜测失败。
验收清单与参考依据
- stdio 模式不监听远程端口;
- 工具列表中没有任意命令、任意 SQL 或任意路径入口;
- 所有参数都有类型、长度、数量和范围限制;
- CLI、API、MCP 复用同一应用服务规则;
- 输出采用字段白名单,错误不返回堆栈和绝对路径;
- Token、Cookie、完整正文和浏览器状态不进入工具参数与日志;
- 写操作需要独立批准,并绑定对象、修订、哈希和有效期;
- 模拟超时后会先只读复核,不会自动重放;
- 回归测试覆盖未授权、越界路径、敏感输出和重复使用批准令牌。
知识依据来自 RuyiBookCourse《大鹏 FastAPI MCP 网关实战》中“把 Web 应用功能变成 MCP 工具”一节;工程依据来自真实项目的 stdio MCP、loopback API、安全契约实现及相关 API 测试。
收束
选择 stdio 是一个好的默认传输决策,但它只解决“怎么连”,没有回答“能做什么、能看到什么、谁来批准”。把这三件事写进服务端规则,MCP 工具才真正具备可审计的安全边界。
如果你正在设计 MCP Server,可以从工具清单中权限最宽的那一个开始审计:它的输入、输出和副作用分别由谁约束?
发布前门禁
- 保留知识或官方依据
- 代码与命令可以复核
- 不写目标平台运营细节
- 本轮无已认领实验,不自行补写实验结论
更多推荐

所有评论(0)