本地 MCP 服务器安全加固实战:MCPB 无沙箱环境下如何守住工具调用边界

发布时间:2026/10/1 7:48:59
本地 MCP 服务器安全加固实战:MCPB 无沙箱环境下如何守住工具调用边界 AI 插件开发工具插件系统【免费下载链接】claude-plugins-officialOfficial, Anthropic-managed directory of high quality Claude Code Plugins.项目地址https://gitcode.com/GitHub_Trending/cl/claude-plugins-official点击查看免费下载本文聚焦claude-plugins-official仓库中 build-mcpb 技能的安全参考文档系统讲解本地 MCP 服务器尤其是 MCPB 打包形式在“平台不提供任何沙箱”前提下开发者必须自行构建的六类安全防线路径穿越防护、用户授权根目录协商、命令注入防御、默认只读设计、资源上限控制与密钥管理。读完你将掌握一套可直接落地的工具处理器tool handler安全编码模式能够为本地文件读写类 MCP 服务器构建经得起对抗性输入测试的权限边界。为什么本地 MCP 的安全责任完全在开发者身上在开始写安全代码之前必须先认清一个残酷的前提MCPB 不提供任何沙箱。正如 build-mcpb 技能主文档 与 manifest 规范 反复强调的manifest 中不存在permissions块没有文件系统作用域没有网络白名单平台不会对服务器进程做任何权限限制服务器进程以当前用户的全部权限运行——它读得到用户能读的任何文件能拉起任何子进程能访问任何网络端点manifest 的env也没有自动前缀机制不存在MCPB_CONFIG_*之类的约定你的服务器读取的环境变量名完全由你在server.mcp_config.env中显式声明。也就是说MCPB 与移动应用商店的权限模型完全不同商店会替你强制权限声明而 MCPB 什么都不强制。任何文件系统、网络、进程层面的边界都必须由开发者在工具处理器内部自行实现。更要命的是第二层威胁模型工具输入是不可信的。虽然调用工具的是用户信任的 Claude但一个被提示注入prompt injection污染的网页可以让 Claude 去调用你的delete_file工具并传入一个你根本没打算删除的路径。这条攻击链里Claude 只是“被操纵的中间人”工具处理器是唯一的防线。因此本文的全部内容都是关于如何亲手构建这道防线。路径穿越防护resolve containment 检查路径穿越path traversal是本地 MCP 服务器中排名第一的漏洞。凡是接收路径参数的工具只要把用户输入直接拼接到某个根目录后面就可能被../../etc/passwd之类的输入带出边界。正确的做法是先 resolve 再检查包含关系containment而不是做字符串匹配。TypeScript 的实现如下import { resolve, relative, isAbsolute } from node:path; function safeJoin(root: string, userPath: string): string { const full resolve(root, userPath); const rel relative(root, full); if (rel.startsWith(..) || isAbsolute(rel)) { throw new Error(Path escapes root: ${userPath}); } return full; }这段代码的精妙之处在于resolve()会先规范化路径——展开..、处理.、合并符号链接段等把所有“伪装”先剥掉relative(root, full)计算最终路径相对于根目录的相对路径若相对路径以..开头说明结果在根目录之外或本身是绝对路径说明根目录与结果不在同一棵树下立即拒绝。千万不要只依赖String.includes(..)之类的字符串判断——编码变体如 URL 编码的%2e%2e、符号链接绕过等攻击手法都能轻松骗过字符串匹配而resolve之后再做包含检查则把这些情况一网打尽。Python 侧的等价实现基于pathlibfrom pathlib import Path def safe_join(root: Path, user_path: str) - Path: full (root / user_path).resolve() if not full.is_relative_to(root.resolve()): raise ValueError(fPath escapes root: {user_path}) return full注意Path.resolve()同样会解析符号链接与..而is_relative_to()从 Python 3.9 起可用是标准的包含关系判断手段。一个值得强调的细节根目录本身也要先 resolveroot.resolve()否则根目录若含符号链接检查结果可能失真。根目录来源优先询问宿主roots/list而非硬编码很多本地服务器会把根目录硬编码成配置环境变量如ROOT_DIR。但在此之前应该先检查宿主是否支持roots/list——这是 MCP 规范原生的、获取用户批准的工作区边界的方式。用户安装时批准了哪些根服务器就该只在这些根内活动。TypeScript 侧的模式是在服务器初始化完成时读取能力并拉取根列表import { McpServer } from modelcontextprotocol/sdk/server/mcp.js; const server new McpServer({ name: ..., version: ... }); let allowedRoots: string[] []; server.server.oninitialized async () { const caps server.getClientCapabilities(); if (caps?.roots) { const { roots } await server.server.listRoots(); allowedRoots roots.map(r new URL(r.uri).pathname); } else { allowedRoots [process.env.ROOT_DIR ?? process.cwd()]; } };fastmcpPython侧则可以在工具处理器内通过Context直接获取# fastmcp — inside a tool handler async def my_tool(ctx: Context) - str: try: roots await ctx.list_roots() allowed [urlparse(r.uri).path for r in roots] except Exception: allowed [os.environ.get(ROOT_DIR, os.getcwd())]策略很清晰宿主支持 roots 就用 roots不支持就回退到配置。但无论走哪条路径最终都必须把每个传入路径与允许根集合逐一校验——上一节的safeJoin在这里正好派上用场。另外注意roots返回的是 URI形如file:///Users/...需要先通过new URL(r.uri).pathnameJS或urlparse(r.uri).pathPython解出实际路径再比较。命令注入永远不要走 shell如果工具需要拉起子进程比如封装git、ffmpeg等 CLI绝对不要把用户输入拼进 shell 字符串。对比一下两种写法的天壤之别// ❌ 灾难级经 shell 展开branch 里的 ; rm -rf ~ 会被执行 exec(git log ${branch}); // ✅ 安全数组传参不经过 shell execFile(git, [log, branch]);exec()/shell: trueNode 与 Python 中对应subprocess的 shell 模式会调用系统 shell 解析命令串用户输入中的分号、管道、重定向、反引号都可能被当作 shell 语法执行execFile()与数组形式的 argv 直接把参数作为独立 token 传给目标可执行文件不经过 shell注入的 shell 元字符只会被当作普通参数。如果工具本身接受旗标flag还应当对每个旗标做白名单校验只放行工具设计时明确支持的参数集合而不是把用户给的 argv 原样透传。默认只读把读写拆成独立工具大多数本地工作流只需要读操作。因此设计上应把读与写拆分成完全独立的工具而不是做一个既能读又能写的“万能工具”list_files ← 可以放心自由调用 read_file ← 可以放心自由调用 write_file ← 独立工具独立审查 delete_file ← 建议干脆不要发布这样做的安全价值很直接一个只读工具无论 Claude 被诱导传入什么参数都不可能被武器化成数据破坏data loss。而写工具尤其是删除类则应严格收敛攻击面——delete_file这种工具甚至值得考虑是否真的要随包发布。**配套工具注解annotations**同样关键。MCP 工具注解是宿主用于权限 UI 的提示信号注解含义宿主行为readOnlyHint: true无副作用可能自动批准auto-approvedestructiveHint: true删除/覆盖弹出确认对话框idempotentHint: true可安全重试瞬时错误时可能自动重试openWorldHint: true与外部世界通信web、API可能显示网络指示在 tool-design 参考文档 中这组注解有更完整的说明并且 Anthropic Directory 的评审硬性要求每个工具都必须声明readOnlyHint、destructiveHint与title。实践建议每个读工具都标readOnlyHint: true删除/覆盖类工具标destructiveHint: true这样宿主就能在权限界面上自动放行安全调用、对危险调用强制人工确认。如果确实需要发布写/删除工具还应考虑两重加固Elicitation规范原生确认在工具调用中途暂停让宿主渲染原生表单向用户请求结构化确认。这是简单确认场景布尔确认、枚举选择、短表单的正确答案比 iframe 组件更轻量。注意宿主支持较新SDK 在不支持时会抛CapabilityNotSupported因此必须先检查caps.elicitation能力并准备回退分支例如返回文本让 Claude 转述问题。完整模式见 elicitation 参考文档确认组件confirmation widget若需要更丰富的 UI文件树预览、可视化选择等可参考 build-mcp-app 技能 中的组件方案让用户为每次破坏性调用单独确认。资源上限一切可能无界的输出都要封顶Claude 会非常“乐意”地请求读取一个 4GB 的日志文件——这既浪费 token也可能拖垮宿主或暴露过多敏感内容。因此一切可能无界的输出都必须设上限。文件读取的典型模式const MAX_BYTES 1_000_000; const buf await readFile(path); if (buf.length MAX_BYTES) { return { content: [{ type: text, text: File is ${buf.length} bytes — too large. Showing first ${MAX_BYTES}:\n\n buf.subarray(0, MAX_BYTES).toString(utf8), }], }; }同样的原则要推广到所有可能失控的场景目录列表限制返回条目数上限搜索结果限制匹配条数超出时提示“Showing 10 of 847 results请缩小查询”任意工具的返回值不要返回数兆字节未经裁剪的 API 响应。这与 tool-design.md 中“返回形状”一节的原则一致——工具返回内容会直接进入 Claude 的上下文巨大 payload 不仅浪费 token还会稀释模型的注意力。密钥安全不进日志、不进工具结果本地服务器处理密钥有三条铁律配置类密钥用宿主密钥链manifest 的user_config中声明sensitive: true的字段宿主会存入 OS 密钥链keychain并通过环境变量注入给服务器进程见 manifest-schema.md 中user_config字段表sensitive: true会存入 OS 密钥链并在 UI 中掩码注意字段名是sensitive不存在secret字段。收到后不要记录日志不要放进工具结果绝不把密钥明文写进文件如果宿主的密钥链集成不够用就自己用keytarNode或keyringPython接入系统密钥链而不是落盘明文工具结果会流入聊天记录凡是工具返回的内容用户以及任何日志导出都能看到。所以返回前必须先做脱敏redact比如把 API key 替换成sk-****1234之类的掩码形式。再补充一个来自 elicitation 文档 的规范约束绝对禁止通过 elicitation 表单请求密码、API key 或令牌——这是规范层面的硬性要求MUST NOT这类机密只能走 OAuth 或user_configsensitive: true的路径绝不能出现在运行时表单里。发布前安全清单对抗性输入测试把以上所有防线收拢成一张发布前的核对清单源自原文档可直接用于 CI 门禁或人工评审每个路径参数都经过 containment 检查safeJoin/safe_join没有任何exec()/shellTrue调用——只用execFile/ 数组 argv写/删除工具与读工具完全分离readOnlyHint/destructiveHint注解已设置文件读取、目录列表长度、搜索结果数量均有上限密钥从不被记录日志、从不出现在工具结果中已用对抗性输入测试../../etc/passwd、; rm -rf ~、10GB 大小的文件。最后一条格外重要发布前请用对抗性输入真实地打一遍。路径穿越、命令注入、超大文件这三类攻击各有典型的测试载荷上述清单中的三个例子正好一一对应。在 build-mcpb/SKILL.md 的测试流程中官方还建议在没有开发工具链的干净机器上测试打包产物——“在我机器上能跑”的失败在 MCPB 中几乎总能追溯到某个没有真正打进包的依赖。小结本地 MCP 服务器的安全模型可以一句话概括平台不设防防线在工具处理器。路径穿越用 resolve containment 消灭根目录优先协商roots/list命令执行只走数组 argv读写工具分离并用注解驱动宿主的权限 UI一切输出封顶密钥只存密钥链。这六件事做完再配合发布前的对抗性测试清单你的本地 MCP 服务器才谈得上“对提示注入免疫”。更完整的配套资料还可以继续阅读本仓库中的 build-mcpb 技能主文档打包与 manifest 全流程、manifest-schema.md配置字段权威参考以及 tool-design.md工具设计规范它们共同构成一套完整的本地 MCP 服务器工程实践。赞分享AI 插件开发工具插件系统【免费下载链接】claude-plugins-officialOfficial, Anthropic-managed directory of high quality Claude Code Plugins.项目地址https://gitcode.com/GitHub_Trending/cl/claude-plugins-official点击查看免费下载相关推荐OpenPencil 安全策略与 MCP 服务加固指南漏洞报告流程与本地服务安全边界OpenPencil 安全策略与 MCP 服务加固指南漏洞报告流程与本地服务安全边界 OpenPencilAI native 设计编辑器开源 Figma前端桌面应用AI 应用MCP 服务BrowserSkill 隐私架构完整审计数据边界逐项验证BrowserSkill 隐私架构完整审计数据边界逐项验证 BrowserSkill 是一个让 AI Agent 驱动用户已登录浏览器完成自动化任务的开源项目人工智能AI 应用AI 技能浏览器控制dsh-pluginQuarkdown安全加固禁用危险函数与沙箱环境全攻略Quarkdown安全加固禁用危险函数与沙箱环境全攻略 引言Quarkdown安全现状与风险矩阵 在文档即代码的时代Quarkdown作为增强型Markd开发工具CLI创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考