本地服务器安全
社区译文;来源: MCP 80041ab0
大多数 MCP 安全文档讨论的是协议威胁模型:OAuth 流程、token 处理、 会话劫持以及混淆代理攻击。关于这些内容,请参阅 授权 和 安全最佳实践。
本指南讨论的是另一种威胁模型:你的机器。当你在本地运行一个 MCP 服务器时,你会从互联网下载代码,并以你的用户账户的子进程身份运行它。 该进程继承你的环境变量,可以读取你账户能读取的任何内容——SSH 密钥、云 凭据、浏览器配置文件和主目录,并且可以向任意目标发起 网络连接。这些都不受协议约束,而取决于你如何运行该进程。
本地服务器能触及什么
本地 MCP 服务器并不是运行在协议提供的沙箱中的插件。它是一个普通的 操作系统进程,由你的宿主应用程序启动,并拥有相应的一切访问权限:
flowchart LR
Host["Host application<br/>(MCP client)"] -->|launches| Server["Local MCP server<br/>(ordinary child process)"]
Server --> Env["Environment variables<br/>(tokens, API keys)"]
Server --> FS["Files your account<br/>can read and write"]
Server --> Net["Unrestricted outbound<br/>network access"]
Server --> Deps["Its full dependency tree,<br/>fetched at install time"]
提供一键安装的客户端必须在命令运行前向你展示确切的 命令 (SEP-1024(英文)), 但同意对话框只有在你知道该留意什么时才有帮助。这正是 本指南其余部分要讲的内容。
stdio 信任模型
大多数本地服务器使用 stdio 传输:你的客户端将服务器作为 子进程启动,两者通过 stdin 和 stdout 通信。这种设计 隐含一个重要的假设:客户端与 stdio 服务器共享同一个 信任域。 它们以相同的权限并排运行,协议和 SDK 都不会让其中一方防御另一方——服务器只要被启动 就已经在你的机器上获得了代码执行能力,因此这两个进程之间不存在 传输层可以强制执行的边界。这是有意为之: SDK 假设 stdio 对等方之间完全互信,而围绕本地服务器的任何隔离或 安全围栏都是宿主应用程序以及操作该机器的人的责任。
两个实际的推论:
- 你真正的安全决策发生在启动之前:安装什么 (来源)以及如何运行它(隔离)。stdio 传输不是 沙箱。
- 如果你以降低的权限运行服务器,那道边界由你自己 执行——如下所述,使用容器、沙箱或 VM。
信任域也是累积的。你配置的每个服务器都共享 同一个模型上下文,因此一个恶意的服务器可以影响代理如何使用 其他每个服务器的工具。每安装一个都会扩大其他所有服务器 暴露于其中的攻击面。
本指南应对的威胁
- 恶意服务器。 仿冒拼写的包名、外观相似的 仓库,或被新发布者接管的废弃项目。这样的 “服务器”在正常工作的同时也在窃取你的环境变量或 文件。
- 过度收集的服务器。 一个合法但收集超出其工作所需 数据的服务器:环境变量、文件,或它没有理由触及的 网络流量。不可信内容中的提示注入可以将一个 诚实的服务器引向同样的越权行为,利用它被授予的任何访问权限。
- 被攻陷的依赖。 服务器作者是诚实的,但其 依赖树中的某个包在供应链攻击中被攻陷。
- 被投毒的工具目录。 工具名称和描述是你的 模型会读取的指令。服务器可以在其中隐藏指令,从而操控 代理——包括如何使用_其他_服务器的工具——即使这个被投毒的 工具从未被调用(通常称为 tool poisoning(工具投毒),当它针对 另一个服务器的工具时称为 tool shadowing(工具遮蔽))。定义也并非 在安装时就固定不变:服务器可以在你批准之后悄悄修改它们 (通常称为“rug pull”,即先获取信任再暗中变更),因此一次性的同意对话框和包 版本固定并不能覆盖这一攻击面。
- 存在漏洞的服务器或桥接器。 一个合法、未经修改的组件 仍然可能成为入侵途径。连接 stdio 客户端与远程 服务器的本地桥接器曾出现过严重缺陷,仅仅连接到 一个恶意端点就可能被触发;而监听端口的本地开发工具 也曾被网页直接利用。
范围之外:对机器有物理访问权限的攻击者、针对性的 国家级攻击,以及协议级攻击(请参阅上文链接的文档)。
在介绍缓解措施之前,有一个影响所有措施的事实:没有任何检查能证明 服务器会做什么。 协议无法保证服务器 名副其实,或只做它承诺的事情。来源信号——可读的 仓库、签名、SBOM、精选列表——告诉你你在信任谁,而 不能说明这份信任是值得的;除非你亲自审计并构建每一个发布版本, 否则你无法预先验证其行为。
因此,下面的缓解措施遵循一条原则:将第三方 本地服务器视为不可信代码,只授予你亲手交给它的那些权限—— 而且越少越好。 审查降低错误信任决策的概率; 遏制决定一个错误决策要付出多大代价。每一节都给出 一个基线、一个可选的“更进一步”步骤,以及在你拿不准时的默认做法。
先问是否需要本地运行
缩小潜在影响范围最省力的方法,就是从一开始不引入这项风险。在把某个服务器 安装为本地进程之前,先问它真正需要你的机器提供什么。读取 你的工作树、驱动本地工具,或访问硬件,才是本地运行的理由。 包装一个远程 API 则不是:一个只与互联网上某个服务通信的 服务器,运行在你的笔记本电脑上毫无收益,而你却要为此承担 本指南中其他所有内容带来的负担,没有任何好处。
- 有远程版本时优先用远程版本。 通过 Streamable HTTP 提供的服务器不会在你的机器上运行代码,使用协议的授权流程 而不是本地配置文件中的令牌,并且在一处即可完成修补。 适用于它的是协议威胁模型(上文已链接),而不是本指南。
- 把不必要的本地安装当作一个信号。 当一个第三方 服务器不需要任何本地访问、却只以 stdio 进程形式发布时,问问为什么。 这可能只是作者选择了阻力最小的路径,但也恰恰 就是恶意或过度收集的服务器进入你机器的方式。
- 保留真正的例外。 有些能力只以本地进程的形式存在。 对于这些情况,本指南的其余部分完整适用。
拿不准时:当服务器不需要你的任何文件、本地工具或本地 网络时,使用它的远程版本,完全跳过本地安装。
安装前检查来源
注册表中的收录并不是背书。Docker Hub、ghcr.io、npm、 PyPI 和 NuGet 几乎会托管任何东西,而一个包很流行或 名字起得好,既不保证它安全,也不保证它是最新的。即使是 精选的 MCP 目录也将其自身的审查描述为尽力而为:精选 降低风险,但并不为你在运行的东西承担问责。
安装服务器之前:
- 识别发布者。 你能把这个包与一个源代码仓库 以及一个真实的维护者或组织联系起来吗?该仓库是否显示出近期、 合理的活动?
- 核实确切的包名。 仿冒拼写是最廉价的攻击 手段。确认你安装的正是文档 实际所指的包,而不是一个外观相似的。
- 固定版本。 安装特定版本,而不是
latest,这样 一个被攻陷的未来版本就不会通过自动更新溜进你的机器。 把固定版本与刻意的更新习惯搭配:当某个你已固定版本的 项目发布安全通告时,正是这个固定让漏洞继续留在你身上。 - 弄清楚固定覆盖了什么。 用
npx或uvx之类的启动器固定包的版本 只固定顶层包;它的传递 依赖通常在安装时重新解析,而这正是 被攻陷的依赖进入的地方。按摘要(image@sha256:...)固定容器镜像 则会冻结整个依赖树。 - 优先选择带有来源元数据的构件。 签名构件、构建 证明和 SBOM(软件物料清单)让你可以验证是谁构建了该构件、 里面有什么——包括它使用哪个 MCP SDK 及其 版本,这在你所依赖的某个 SDK 宣布漏洞时很重要。
更进一步:验证签名和证明,而不只是 留意到它们存在(例如使用 Sigstore 工具或你的注册表的 证明验证),并自己从构件生成 SBOM(例如 用 Syft 或 Trivy),而不是信任已发布的那个。某些 注册表和运行时可以在启动时验证签名,而不只是 在安装时,从而把来源从一次性检查变成一项常态化的检查。
拿不准时:只安装源代码可读、发布者可识别的服务器, 并固定确切的版本。
将工具定义视为不可信输入
安装服务器并不是最后一个信任决策。在运行时,服务器会 把它的工具名称和描述发送给你的客户端,模型会把这些内容 当作指令来读——这是一个安装时审查覆盖不到的注入面。
- 在依赖某个服务器之前先读工具列表。 大多数客户端都能显示 每个工具的描述,而 MCP Inspector 无需模型参与就能列出 服务器暴露的每个工具。如果某个描述包含 与该工具职责无关的指令,就移除该 服务器。
- 优先选择会固定工具定义的客户端。 有些客户端在你首次批准一个 服务器时记录工具定义,并在其发生变化时再次询问。 协议并不要求这一点,因此这是一个值得据以 选择客户端的特点。
- 保持清单精简。 由于所有已配置的服务器共享 模型的上下文,你每添加一个服务器,就扩大了其他服务器 暴露于其中的攻击面。
拿不准时:安装少数几个服务器,把它们的工具描述浏览一遍,并 选择在工具定义变化时会再次提示的客户端。
在隔离环境中运行服务器
进程隔离是本指南中杠杆率最高的一项缓解措施。 它把“这个服务器能读取我的 SSH 密钥”变成“这个服务器只能读取一个 空目录”,并且即使来源检查失败,它也能保护你—— 一次性抵御上文所有威胁。
- 基线:在容器内(例如使用 Docker 或 Podman)运行第三方服务器,除了服务器所需之外不挂载任何卷。 stdio 传输无需改动即可工作:客户端与容器的 stdin 和 stdout 通信,与和裸进程通信完全一样。
- 替代方案:专门用于开发 工作的轻量级虚拟机,或操作系统的进程沙箱设施。有些 面向 MCP 的运行时和客户端集成会替你自动将服务器包裹在 容器中;模式比工具更重要。
容器并不是不可逾越的边界,但它移除了对你环境、 文件和钥匙串的默认访问——而这正是 常见攻击所依赖的。
更进一步:加固容器本身——seccomp 配置文件限制 它能发出的系统调用,而 AppArmor 或 SELinux 之类的 Linux 安全模块 限制它能触及的范围,通常只需要启用你 发行版的默认配置即可。再进一步,在 microVM 中运行服务器,或使用 独立的用户账户,并把隔离与下面的网络控制 结合起来,让沙箱连它能看到的那一点点东西也无法外传。
拿不准时:把每个第三方服务器都放在容器中运行,只挂载 它本应处理的那个目录。
隔离凭据
本地启动的服务器可能会继承启动它的任何东西的环境。 如果你的 shell 配置文件全局导出了云凭据和 API 令牌, 你运行的每个服务器都可能读到全部这些内容。
- 按服务器传密,而不是全局传。 使用客户端配置文件中的按服务器环境 配置,而不是在 shell 配置文件中导出机密, 这样每个服务器只能看到它自己的凭据。
- 把令牌范围收窄。 为每个服务器签发它自己的令牌,使用 能工作的最小范围(例如只限 一个仓库的细粒度访问令牌,而不是你整个账户的令牌),这样一次泄漏 就能被限制住并且可以撤销。
- 不要让长期机密以明文形式留在配置中。 客户端配置 文件在磁盘上是一个已知的、可预测的位置。优先使用你操作 系统的机密存储(例如 macOS 钥匙串、Windows 凭据管理器, 或 Linux 上的 freedesktop Secret Service)——只要你的客户端支持 引用它。
- 记住文件也算。
~/.ssh、~/.aws以及类似的目录 就是磁盘上的凭据。进程隔离(上文)才是真正让它们 脱离服务器触及范围的手段。
更进一步:使用机密管理器或代理在启动时签发的短期 凭据,而不是静态令牌;如果你决定移除某个服务器, 就轮换该服务器持有过的任何凭据。
拿不准时:通过每个服务器自己的配置,为它签发独立的窄范围 令牌,绝不要在服务器之间复用同一个个人宽范围令牌。
限制文件系统访问
许多有用的服务器确实需要文件访问。问题在于需要多少。
- 授予能工作的最窄根目录。 为面向文件系统的 服务器配置一个具体的项目目录,通过 服务器的配置或工具参数显式传入——绝不能是你的主目录。
- 优先只读。 如果服务器的工作是读取代码或文档, 就以只读方式挂载或授予该目录。
- 对宽泛心存警惕。 一个为了完成狭窄工作却要求宽泛根 目录的服务器就是在越权。授予更窄的目录, 或者换一个服务器。
更进一步:把允许列表与隔离结合起来,让服务器 能看到且唯一能看到的文件系统,就是你选择的挂载点。
拿不准时:授予对单个项目目录的只读访问权限。
控制网络出口
外泄需要一个出口。本地进程通常拥有不受限制的 出站访问权限,这会把任何过度收集或攻陷变成数据 损失。
- 知道服务器应该与谁通信。 只包装一个 API 的服务器 有一个简短、可预测的目的地集合。流向任何其他地方的流量 都是值得调查的信号。
- 在不需要网络的地方禁止网络。 纯粹在本地
文件或工具上运行的服务器根本不需要网络——在容器中这只是一个
标志(
--network=none)。 - 过滤剩余的部分。 对于确实需要出口的服务器,使用 出站过滤防火墙或带域名允许列表的出口代理, 把它们限制在预期的目的地,并记录落在此范围之外的尝试。
更进一步:通过代理为每个服务器配置各自的出口允许列表, 并对列表之外的目的地发出告警——意外的出口流量往往是 本页所述内容出问题的第一个可观测信号。
拿不准时:让服务器在没有网络访问的情况下运行,除非它们 确实需要;并弄清楚其余服务器预期会访问哪些目的地。
在组织内管理本地服务器
如果你是 IT、安全或平台管理员,上面的各节 就是你的积木,但你的问题不同:很多机器、很多 用户,服务器在你不知情的情况下被安装。有两个事实决定了一切:
- 本地 MCP 服务器就是你的用户安装的普通软件。 协议无法盘点、 修补或遏制它并不管理的机器上的进程。这是端点和软件治理——你 已有的责任,只是多了一种新的包类型。
- 没有人会先问你。 假定用户安装服务器的 方式与他们安装编辑器扩展的方式相同。
一个可行的初始姿态:
- 定义批准的来源。 发布一份简短的经过审查的服务器 允许列表(带固定版本),以及一个新服务器要加入它必须满足的标准: 可识别的发布者、可读的源代码、来源元数据、SBOM。 让经过批准的路径比未经批准的更容易走。
- 设定默认的运行时姿态。 决定并记录服务器在被管理的 机器上如何运行——例如,“第三方服务器在容器中运行, 不挂载主目录”——而不是把隔离留给 每个用户的自觉。
- 在不需要本地访问的地方集中部署。 本指南开头的那个问题 适用于整个设备群:一个只包装 SaaS API 的服务器不需要在每台笔记本电脑上运行。集中管理的远程 部署只需修补、审计和控制一个实例,而不必在大量终端上分别维护。
- 持续盘点。 客户端配置文件位于每个平台的 已知位置。你的端点管理工具可以收集 它们,从而实时回答“谁在运行什么、什么版本、 来自哪个来源”。
- 保留审计轨迹。 在工具支持的地方,记录被管理 机器上的 MCP 活动——哪个服务器、哪个工具、何时——这样 事件复盘就能还原一个被攻陷或越权的服务器实际 做了什么,而不是靠它的权限去猜。
- 为设备群级事件做预案。 当一个被广泛使用的本地服务器(或它 内部的 SDK)被发现有漏洞时,你需要:找出 每一个实例(借助上面的盘点),在响应期间于代理或防火墙处 阻断它的出口,轮换这些配置曾传给它的每一个凭据,并通过 你的批准渠道推送已修复的固定版本。SBOM 让你能在几分钟而不是几天内回答“我们的哪些机器运行了构建在 受影响 SDK 版本之上的东西”。
- 留意你的监管环境。 软件供应链监管(例如 欧盟《网络弹性法》)越来越期望的正是这些 做法——SBOM、更新流程和漏洞处理——所以 上面的工作往往能一举两得。
拿不准时:从盘点整个设备群中的客户端配置 加上一份简短的已批准服务器允许列表开始,并把两者之外的一切 都当作未批准的软件来对待。
检查清单
针对你自己的机器:
- 我清楚客户端配置中的每一个服务器,并且已删除 我不使用的那些。
- 不需要从我的机器获取任何东西的服务器通过远程运行,而不是作为本地 进程运行。
- 每个服务器都来自可识别的发布者,且其版本 已固定。
- 第三方服务器在隔离环境中运行(容器、沙箱或 VM),而不是作为 裸进程运行。
- 每个服务器只通过按服务器的配置获得它自己的窄范围 凭据——不从我的 shell 继承任何东西,不复用任何东西。
- 文件系统访问被限制在特定目录,可能的情况下只读。
- 不需要网络的服务器在出口被禁用的情况下运行;其余服务器 有已知的目的地列表。
- 我已浏览过每个服务器的工具描述,并且我的客户端在 工具定义变化时会再次提示。
针对设备群:
- 我们维护一份已批准服务器允许列表,并公布接收 标准。
- 我们持续盘点被管理机器上的 MCP 客户端配置。
- 我们为本地服务器记录了一种文档化的默认隔离姿态。
- 我们保留被管理机器上 MCP 活动的审计日志。
- 我们能阻断某个特定服务器的出口,并在整个 设备群中轮换它的凭据。
下一步
安全最佳实践
协议威胁模型:混淆代理、token 透传和会话 劫持
授权
OAuth 授权如何为远程 MCP 服务器工作
SEP-1024(英文)
本地服务器安装同意的客户端安全要求
连接本地服务器
在你的客户端中设置本地服务器——本指南所保护的工作流程