这次复盘来自一个真实客户的自部署智能体环境。最初的问题很直接:应用能调用外部模型,调用凭据究竟放在哪里?接口没有返回密钥,制品里也没有找到运行配置,数据库从外部连不上。继续盯着这些入口,收益已经很低。
转折出现在 MCP 配置。系统允许网页管理员配置本地工具进程,而后端会在测试连接时启动它。于是,原本只是应用内的管理权限,变成了应用运行环境里的进程执行能力。
本文按已有测试记录重建判断过程,不把没有走过的分支写成实验结果。客户名称、产品名称、账号、口令、地址、接口路径、表名和部署标识均已省略;涉及实现的代码为示意,不提供目标复现载荷。这里讨论的是这套集成的权限设计,不能据此认定所有 MCP 应用都有同样问题。
一、先问凭据如何被应用使用
自部署应用能调用模型,说明调用链上存在某种可用身份。但这不等于本机数据库一定存着明文 API key。工作负载身份、外部密钥服务、模型网关代持凭据,都是可能的实现。
这次记录中的配置逻辑和数据库访问代码,把范围收敛到了应用侧保存的提供方凭据。这个证据很重要:它把“模型调用需要身份”变成了“本环境有一处需要核查的存储位置”。没有这一步,直接跳到查库只是猜测。
接口不返回敏感字段,只能说明这个读接口做了输出控制。服务端为了发起调用,仍需通过某条路径取得可用凭据。接下来要找的是那条路径,以及它依赖的执行身份。
二、为什么放下其他候选路径
路径排除要说明排除了什么,也要保留证据的边界。
| 候选路径 | 当时的判断 | 支持判断的证据与限制 |
|---|---|---|
| 从应用读接口取得密钥 | 不继续投入 | 查询和返回逻辑没有暴露密钥字段;这只覆盖已检查接口 |
| 从对象存储制品寻找环境配置 | 暂时排除 | 已检查制品没有命中,打包规则也排除了该配置;不能外推为所有历史制品都不存在 |
| 从外部直接连接数据库 | 当前路线不成立 | 指定探测位置连接超时;超时本身不足以证明数据库只在某个网络内可达 |
| 在技能或文档中寻找凭据 | 未走 | 当时已有更直接的存储线索;这是优先级选择,不是“内容面绝不泄漏”的证据 |
| 通过主机管理通道读取配置 | 缺少直接入口 | 当前身份没有可用的远程管理权限;不代表应用没有其他执行能力 |
| 利用 MCP 本地进程配置获得应用内执行点 | 采用 | 记录中的命令配置缺少有效限制,测试流程启动进程,应用又显式传入了完整环境变量 |
排除之后,问题变得具体:数据库从当前外部位置不可达,而应用正常运行时必须访问它。需要检查一个与应用处于相同访问条件的执行点。
三、MCP stdio 为什么会涉及命令执行
MCP 用 JSON-RPC 交换消息。stdio 模式下,客户端启动一个工具服务子进程,再通过它的标准输入和标准输出通信。Streamable HTTP 则连接独立运行的 HTTP 服务,不以启动本地子进程作为这条传输的前提。见 [MCP 传输规范](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports)。
在本案例的部署里,智能体后端承担宿主角色,其中的 MCP 客户端连接工具服务。网页保存的命令配置,最终被交给后端的本地进程启动逻辑。
这里有个容易写错的时序:创建 StdioClientTransport 对象通常只是保存参数;连接过程调用传输的 start() 时,才真正启动子进程。随后才进行初始化握手、获取工具列表和工具调用。这个顺序可以在 [TypeScript SDK 的 stdio 实现](https://github.com/modelcontextprotocol/typescript-sdk/blob/v1.x/src/client/stdio.ts)中核对,具体项目仍应以锁定版本为准。
保存工具配置
→ 后端加载配置
→ 连接流程启动 stdio 子进程
→ 初始化握手
→ 获取工具列表
→ 模型选择工具
→ 工具调用与结果返回因此,握手报错不能证明命令没有运行。进程可能已经启动,只是没有按协议返回消息。工具调用阶段的审批,也不能自动补上更早发生的进程启动审批。
四、真正跨过去的是哪道权限边界
测试记录里的入口,是仍可使用的初始化管理员凭据。它首先带来的是网页管理权限,不是操作系统管理员权限。问题出在后续设计:网页管理员可以指定后端要启动的工具程序,而这项能力没有与机器上的执行权限分开。
长度限制不能替代可执行程序与参数的约束。即使进程创建没有启用 shell,只要调用者能选择危险的解释器或任意可执行程序,仍然可能获得超出“配置工具”预期的执行能力。是否能注入 shell 元字符,是另一个具体问题。
这也不是一次自动获得宿主机 root 的提权。首先得到的是应用进程所处执行环境中的能力;如果应用在容器里,边界首先是该容器。能否到达物理宿主机,需要额外证据,不能因为文章里写了“宿主”就默认完成了容器逃逸。
五、环境变量和网络位置让后续路径成立
记录中的应用显式把自身环境变量传给工具子进程。下面只保留这个危险决定的形状:
// 实现模式示意,不是客户源码原文
const toolEnvironment = { ...process.env };全量继承是这套应用的集成选择,不是 MCP 协议要求。SDK 的默认环境策略和应用自行传入完整环境,是两回事。
这种传递会扩大子进程能看到的信息范围:如果数据库连接参数、签名秘密或其他服务凭据存在于父进程环境里,子进程也会拿到。哪些值实际存在、对应权限有多大,仍要逐项验证,不能把所有可能的环境变量都写成已经泄漏。
网络位置同样关键。子进程通常沿用应用所在的网络环境,而不是神奇地“绕过云路由”。当应用本来就能访问数据库、子进程又能使用应用身份时,外部直连失败并不会阻止这条内部路径。最后能读哪些数据,取决于数据库账户的实际权限和存储方式。
六、把整条链串起来
根据原始记录,这条链的成立条件可以压缩成五步。每一步都依赖前一步的能力,不应省略中间条件。
- 初始化管理员凭据仍然有效,取得应用管理权限。
- 管理配置能影响后端本地进程的启动参数。
- 测试连接触发进程启动,不需要先完成模型工具调用。
- 子进程获得应用侧的环境信息和网络访问条件。
- 在数据库身份权限允许的范围内,访问应用保存的提供方凭据。
记录还涉及一个输出风险:应用可写的位置同时被作为公网静态资源提供。两种能力组合后,原本留在服务端的数据就可能进入公开读取范围。这里不保留实际目录、文件名和输出内容。静态文件服务与写入目录之间的隔离,本身也是独立的整改点。
从路径选择看,MCP 提供的是执行点;环境和数据库权限决定这个执行点能做什么;公开静态目录决定结果是否可能被外部读取。把三者分开,才看得出每一层该怎么修。
七、哪些是本次路径,哪些只是相关风险
实际路径涉及四类边界:身份到管理配置、配置到进程执行、子进程到应用秘密和数据权限、服务端写入到公开读取。
另外三类风险需要单独讨论,不能混成“这次也验证过”:工具名称与描述可能影响模型判断;工具返回文本可能携带提示注入;工作区级配置可能把管理员的选择扩散到其他成员的会话。前两项属于模型输入信任问题,最后一项涉及配置作用域与触发范围。
本次选择并不依赖诱导模型做决定。关键动作发生在工具服务连接阶段,所以仅加强提示词或工具调用审批,覆盖不到这条已经存在的进程启动路径。
八、这条链对应哪些攻击手法
下面按动作标注手法。ATT&CK 用来描述攻击行为,CWE 用来描述实现缺陷,两者不混用。映射依据是本文已有记录,外部引用解释分类,不替代客户环境的测试证据。
| 链路位置 | 手法与引用 | 本文中的对应关系 |
|---|---|---|
| 初始化凭据登录 | 默认账户滥用:[ATT&CK T1078.001](https://attack.mitre.org/techniques/T1078/001/) | 使用仍然有效的预置管理员身份取得应用权限;没有破解口令或绕过登录流程 |
| MCP 配置触发服务端执行 | 可控程序选择导致任意命令执行:[CWE-78](https://cwe.mitre.org/data/definitions/78.html) | 网页配置决定后端启动什么程序。CWE-78 也讨论了攻击者能完整选择程序的情形,不限于拼接 shell 元字符;这里的根因是配置权越过了执行授权边界 |
| 子进程取得应用秘密 | 未受保护的凭据获取:[ATT&CK T1552](https://attack.mitre.org/techniques/T1552/) | 应用把包含凭据信息的运行环境交给可控子进程,扩大了秘密的可见范围;映射停留在父技术,不将环境继承写成读取配置文件 |
| 使用应用身份查库 | 从数据库收集数据:[ATT&CK T1213.006](https://attack.mitre.org/techniques/T1213/006/) | 借助已有网络位置与数据库身份访问应用保存的数据;读取的是提供方凭据,因此结果同时服务于凭据获取。记录没有证明 SQL 注入 |
| 将结果写到可公开读取的位置 | 本地数据暂存的相关模式:[ATT&CK T1074.001](https://attack.mitre.org/techniques/T1074/001/) | 若数据先写入本地再由外部取走,写入阶段可按本地暂存理解;本文保留的是输出风险,不能只凭目录可写、可读就认定外带已经完成 |
还有几处容易贴错标签:
- 制品中寻找环境配置,对应[文件中的凭据(T1552.001)](https://attack.mitre.org/techniques/T1552/001/)这一调查方向,但本次没有命中,不能列作成功的凭据获取步骤。
- 只有确认通过 shell 或脚本解释器执行,才进一步映射[命令与脚本解释器(T1059)](https://attack.mitre.org/techniques/T1059/)。单看
spawn不能确定用了哪种解释器,也不能直接指定它的子技术。 - 工具描述投毒、工具结果中的间接提示注入,属于另一条模型输入攻击路径,可参照 [OWASP LLM01:2025 提示注入](https://genai.owasp.org/llmrisk/llm01-prompt-injection/)。本文的进程执行发生在模型选择工具之前,没有用这条路径。
- 应用内部连接数据库,不等于已经证明横向移动、路由绕过或容器逃逸。仍然需要分别证明跨了什么身份、网络或隔离边界。
用一句话概括实际路径:默认身份取得配置权,配置权触发服务端执行,执行进程继承应用访问条件,再在数据库身份权限内取得数据。MCP stdio 是其中的进程启动机制,协议本身并不要求把网页管理权限直接兑现成后端执行权限。
九、修复要落在启动之前
本地工具进程天然具有代码执行属性。桌面程序里,通常是机器使用者主动安装并运行自己选择的工具;搬到多用户 Web 服务后,必须重新定义谁能授予这种能力。不能直接把“网页管理员”视为“后端机器的所有者”。
- 初始化凭据必须失效或完成强制轮换,避免默认身份成为管理入口。
- 将工具安装、运行授权与普通业务管理权限分离,记录配置来源和变更。
- 多用户服务优先使用受控工具目录;确有本地进程需求时,限制程序与参数,并隔离执行身份、文件系统、网络和资源。
- 环境变量按工具需要显式传入,不把整个应用环境复制给子进程。
- 数据库账户按业务最小权限配置;模型凭据的存储、解密和使用路径分开审视。
- 把审批放在进程启动前;测试连接同样要执行安全检查,不能当作无副作用操作。
- 应用可写目录与公开静态目录分离,对工具进程启动、异常文件写入和敏感数据访问保留审计。
这次路径选择的依据并不复杂:接口不返回密钥,就检查调用凭据的存储与使用路径;外部连接不成立,就核对应用内部的访问条件;发现配置可以启动进程,就沿着进程的身份、环境和数据权限继续分析。真正的问题是,一项面向工具集成的配置权,最终承载了后端执行权,却没有被当作同等级别的权限管理。