SSD Nodes Learn 🎉 VPS $5.50/月起
指南 Matt Connor作者: Matt Connor

编码代理提示注入:攻击面与防御措施

编码代理会把文件、网页、拉取请求和工具结果当作文本处理。了解攻击者如何让恶意内容到达服务器,以及哪些防御措施真正减少数据泄露与系统损害。

什么是针对编码代理的提示注入

针对编码代理的提示注入可以简单概括为:代理读取的文本会被当作指令,并由代理执行。代理可能打开文件、拉取请求评论、网页或工具调用的结果。这些内容都会以与您的请求相同的文本形式传入。如果攻击者控制其中任何一部分文本,就等于可以向您的会话中写入内容。

目前所有代理产品都具有这一特性。模型接收的是一串 token。您的请求、系统提示、文件内容和工具结果会被拼接在一起,然后由模型预测后续内容。token 没有权限标记。其格式不会说明哪些内容经过您的授权,哪些内容来自陌生人的 README 文件。

本页介绍威胁模型:攻击者控制的文本如何到达服务器上运行的代理,攻击者在每个环节能获得什么,以及哪些防御措施值得投入。只有了解需要防止代理受到什么影响后,我们其他指南中的隔离建议才有意义。

模型为什么无法将内容与指令分开

训练有帮助,但不能彻底解决问题。当前模型经过训练,会对检索到的文本保持警惕,也会拒绝许多粗糙的攻击尝试。拒绝是一种概率行为,不是规则。攻击者可以改写提示、反复尝试,也可以将文本隐藏在没人预料到的格式中,而尝试的措辞数量不受限制。

OWASP GenAI 项目将此问题列为 LLM01:2025 提示注入,并将其分为两类。直接注入是用户自己的提示改变模型行为。间接注入是网站或文件等外部内容在模型处理这些内容时改变模型行为。间接注入与服务器关系更大,因为代理读取的文本远多于您手动输入的内容。

第一项系统性研究由 Greshake 及其同事完成,题为 并非您注册时所期待的内容(2023)。需要记住的结论是:当应用将检索到的文本提供给能够调用工具的模型时,处理这些文本接近于执行任意代码。

将读取变成入侵的条件

读取恶意文本本身不会造成损害。损害必须有途径离开这台机器。

Simon Willison 在 2025 年 6 月将这种组合称为致命三要素。如果代理同时持有私有数据、接触不受信任的内容并且能够向外发送数据,就可能被诱导通过第三项将第一项数据传出去。

VPS 上的编码代理从第一天起就具备这些条件。私有数据包括源代码、.env 文件、SSH 密钥和 shell 历史记录。不受信任的内容包括它读取的每个代码仓库、页面和工具结果。数据可以通过 git pushcurlnpm publish、拉取请求正文,或终端中显示并由您点击的链接传出去。

您无法移除第二个条件,因为读取不受信任的文本正是您部署代理的目的。因此,所有实际可行的防御措施都应针对另外两个条件。

不受信任文本如何到达服务器上的编码代理

代理正在处理的代码仓库

检出目录中的每个文件都是输入。源代码注释、README.md、变更日志、测试固件、供应商代码,以及代理指令文件本身:CLAUDE.mdAGENTS.md及其等效文件,全部如此。要求代理理解代码库时,它会读取这些内容,因为这正是您的要求。

攻击者由此可以影响所有克隆该仓库并让代理处理它的人。指令文件是最直接的途径,因为它们本来就是供代理读取的指令。拉取请求只需向 CLAUDE.md 添加4行有用内容,再添加1行重定向代理的内容,人类审查者很容易略过这一改动。

Issue、拉取请求和代码审查评论

陌生人可以在跟踪系统中输入的任何内容,都会在您要求代理进行分类时到达代理。2025年5月,Invariant Labs 发布了一个GitHub MCP 调查结果,情况正是如此。某开发者的代理可以访问一个公共仓库和多个私有仓库。攻击者在公共仓库中提交了一个 issue。开发者要求代理查看未关闭的 issue 后,代理读取了私有仓库内容,并将其写入公共侧的拉取请求。

该报告描述的是架构问题,而不是 MCP 服务器中的代码缺陷。代理持有一个权限范围过大的访问令牌,从公共收件箱读取内容,并且拥有写入权限。通常意义上没有配置错误,因此解决方案是限制权限范围,而不是打补丁。

代理获取的网页

文档、论坛回答、供应商页面、搜索结果,任何内容都可能包含写给代理而不是写给您的文本。将 HTML 转换为文本会给攻击者更多空间,因为浏览器从不显示的内容仍会到达模型。

攻击者会在代理最不受监控时获得控制权。代理查找信息时获取的页面,没人会完整阅读其文本。

MCP 工具输出

MCP(模型上下文协议)是代理连接外部工具的常用方式。工具返回的结果以文本形式进入上下文窗口。这里有两个入口,而不是一个。工具返回的数据是显而易见的入口。工具自身的名称和描述是另一个入口,模型会读取它们来决定何时调用工具;而您无法控制的服务器可以在两次调用之间修改其中任意一项。

攻击者只要将文本放入一个工具的输出,就能影响代理拥有的其他所有工具。低价值操作中的注入,正是这样最终驱动高价值操作的。

CI 日志、构建输出和依赖项元数据

npm install 会输出您未编写的软件包中的文本。测试失败时,库会输出断言消息。持续集成(CI)任务日志通常包含数千行第三方输出。要求代理修复失败的构建时,它会读取全部内容。

在这里,攻击者获得的是构建机器的控制权。构建机器通常保存部署凭据和镜像仓库令牌,但受到的关注却少于笔记本电脑。

攻击者实际上能获得什么

有四种结果值得提前规划。

凭据窃取。 代理进程能够读取的内容都属于攻击范围:环境变量、~/.aws/credentials~/.sshgh令牌、Docker 配置文件。将这些内容发送出去不需要 curl。向分支提交代码、在拉取请求描述中写入内容、向注册表发布软件包,或查询攻击者控制的域名,都会将数据移出服务器。

您批准的代码更改。 编写代码是代理的工作,因此诱使它写入一行看似正常但实际错误的代码,是最容易实现的结果。可能是添加一个依赖项,也可能是添加一条日志调用,将令牌带入随后发送到其他位置的日志中。

持久化。 文件只需写入一次,即可在没有模型参与的情况下持续生效:在 .git/hooks 中添加钩子、在 package.json 中添加 postinstall 脚本、向 shell 启动文件追加一行,或在 CLAUDE.md 中添加一行。下一条命令执行它。

在网络内部横向移动。 代理在哪台主机上运行,取决于您的部署位置。如果该主机能够访问 loopback 上的数据库、内部管理服务、云服务商的元数据服务,或私有网络中的其他主机,那么驱动代理的任何操作也都能访问这些目标。

自动批准模式会移除最后一道检查

在默认模式下,Claude Code 在运行命令或编辑文件前会询问您。这个提示就是阻止上述每个入口直接执行实际操作的人工检查。移除提示的模式也会移除这道检查。

文档明确说明 bypassPermissions:仅在容器或虚拟机等隔离环境中使用,因为在这些环境中,Claude Code 无法造成损害。自动模式的限制更少,并会在后台执行安全检查,验证工具调用是否符合您的请求。这些检查可以拦截许多问题。但它们仍然是模型对模型输出的判断,因此应将其视为过滤器,而不是安全边界。

管理员可以同时移除这两项限制。在设置文件中将 permissions.disableBypassPermissionsModepermissions.disableAutoMode 设置为 "disable",然后将该文件放入受管设置中,以防止检出的项目覆盖它。我们的 Claude Code 自动模式和权限规则 指南介绍了每条规则的生效位置。

防御措施:按实际收益排序

这些措施都不是彻底修复。每项措施要么减少代理持有的内容,要么限制代理可以使用这些内容执行的操作。

  1. 使用可以销毁并重建的机器。这样,服务器遭到入侵时,损失是一个小时,而不是一次安全事件。
  2. 使用与您自己的凭据分离、仅限一个代码仓库且有效期较短的凭据。
  3. 不要在代理命令继承的环境中存放长期有效的密钥。
  4. 在操作系统层面限制网络出站流量和文件访问。这些限制适用于代理启动的每个进程。
  5. 保持写入操作和网络调用的审批提示启用。
  6. 为可以明确描述的具体操作配置 hook,作为确定性的兜底措施。
  7. 合并前阅读 diff。

顺序很重要。第 1 至第 4 项即使在模型已完全受攻击者控制时也仍然有效。第 5 至第 7 项依赖人工关注,而长时间运行代理时,最容易停止的正是人工关注。

将代理运行在可以丢弃的机器上

一台存放代码检出副本和一个受限令牌的 VPS,价值远低于一台存有您个人密钥的笔记本电脑。让代理以独立的非特权用户运行,不要使用您的登录账户,也不要使用 root。我们的 用于代码代理的可销毁 VMVPS 上的最小权限用户 指南介绍了相关设置,在 VPS 上安全运行 Claude Code 介绍了日常使用方式。

从环境中移除密钥

环境变量对每个子进程都可读,这意味着代理运行的每条命令都可以读取它。Claude Code 的沙箱可以在每次沙箱命令执行前取消指定变量。在 Linux 上,沙箱首先需要安装两个软件包:

sudo apt-get install bubblewrap socat

然后在 ~/.claude/settings.json 中:

{
  "sandbox": {
    "enabled": true,
    "network": {
      "allowedDomains": ["github.com", "*.npmjs.org"]
    },
    "credentials": {
      "envVars": [
        { "name": "GITHUB_TOKEN", "mode": "deny" },
        { "name": "NPM_TOKEN", "mode": "deny" }
      ]
    }
  }
}

deny 配置项会在每次沙箱命令运行前取消该变量,allowedDomains 则限制沙箱命令只能访问您列出的主机。credentials 配置块需要 Claude Code v2.1.187 或更高版本,已于 August 2026 检查。请在会话中运行 /sandbox,查看当前启用的层以及缺少的依赖项。更重要的是决定哪些密钥根本不应存在于该主机上,让密钥脱离 AI 代理的访问范围对此进行了说明。

在操作系统层面限制网络出站流量

防火墙规则不关心模型做出了什么决定。让代理以专用的 agent 用户运行,然后丢弃该用户发出的流量:

table inet agentcage {
  chain output {
    type filter hook output priority filter; policy accept;
    meta skuid "agent" ct state established,related accept
    meta skuid "agent" oif lo accept
    meta skuid "agent" counter drop
  }
}

这样,agent 用户只能访问 loopback。其流量必须经过您在同一台机器上运行的代理,而主机名允许列表由该代理保存。将 https_proxy 指向该代理后,客户端会发送 CONNECT 请求,由代理执行名称解析,因此代理自身不需要出站 DNS(域名系统)。使用 sudo nft list ruleset 检查配置是否生效;当代理尝试访问新目标时,观察丢弃规则上的计数器是否增加。

修改防火墙规则时,保留第二个 SSH 会话处于打开状态。同时检查容器运行时如何处理这些规则:Docker 会写入自己的链,已发布的 Docker 端口会绕过 ufw 介绍了由此产生的意外情况。

Hook:模型无法通过辩解绕过的检查

权限规则和 hook 由 Claude Code 强制执行,而不是由模型执行。文档对此有明确说明:提示词中的指令或 CLAUDE.md 会影响 Claude 尝试执行的操作,但不会改变 Claude Code 允许执行的操作。这一区别正是其全部价值所在。CLAUDE.md 中的“绝不运行 curl”只是建议,注入的段落可以对其提出反驳。hook 是一个返回退出码的进程。

.claude/settings.json 中注册 PreToolUse hook:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/no-egress.sh"
          }
        ]
      }
    ]
  }
}

hook 会通过标准输入接收 JSON 格式的工具调用。退出码 2 会阻止该调用,并将标准错误中的原因显示给 Claude。退出码 0 会让调用继续进入正常的权限流程。

#!/usr/bin/env bash
# PreToolUse: stdin holds the tool call, exit 2 blocks it.
cmd=$(jq -r '.tool_input.command // ""')
if printf '%s' "$cmd" | grep -qE '(^|[;&|]|\s)(curl|wget|nc|ncat)(\s|$)'; then
  echo "Blocked: this repository does not allow outbound network commands." >&2
  exit 2
fi
exit 0

下面说明实际限制。这是针对 shell 字符串的拒绝列表,而针对 shell 字符串的拒绝列表很容易被绕过。python3 -c 无需使用 curl 一词即可打开套接字。make deploy 目标可以再向下隐藏一层相同的调用。针对您能够明确描述的错误编写 hook,并将实际依赖的边界放在内核或网络层。

权限拒绝规则还有一个值得了解的匹配限制。ReadEdit 拒绝规则适用于 Claude 自己的文件工具,以及它在 Bash 中识别的文件命令,例如 catheadtailsed。但这些规则不适用于自行打开文件的 Python 或 Node 脚本。规则的评估顺序是先拒绝,再询问,最后允许,因此拒绝规则无法通过允许列表例外放行。

{
  "permissions": {
    "deny": [
      "Read(.env)",
      "Read(./secrets/**)",
      "Bash(git push *)"
    ]
  }
}

监控流出内容,并阅读 diff

代理运行会产生一个 diff 和一组网络调用。在合并或部署任何内容前,都应检查这两项。对 diff 执行自行托管的安全审查流程,可以发现与人工快速浏览不同类型的变更;了解代码代理会将哪些内容发送到主机外,则可以帮助您了解正常流量,使异常请求更加明显。

仍未解决的问题

目前还没有可靠的方法将内容与指令分离。现有的每种防御措施,要么是存在误判率的过滤器,要么是限制攻击后果的机制。整个技术栈中没有任何机制能标记一段文本,明确表示该文本属于绝不能执行的数据。

过滤器确实有帮助,但也确实会失效。能够拦截大多数注入尝试的分类器,仍然必须每次都判断正确,而攻击者只需成功一次。这种不对称性意味着,防御措施公布的成功率只能作为下一次尝试的起点,而不是保证。

目前最有前景的工作位于设计层,而不是模型层。CaMeL 来自 Defeating Prompt Injections by Design(Debenedetti 等人,2025),它首先从受信请求中提取控制流和数据流,使不受信数据无法改变程序行为,然后在调用工具时执行能力检查。论文在 AgentDojo 基准测试中给出的数据,展示了这种方案的代价。

ChartAgentDojo tasks solved, published figures from the CaMeL paper (2025)
The data behind this chart
[
  {
    "label": "Undefended agent",
    "tasks_solved_pct": 84
  },
  {
    "label": "CaMeL",
    "tasks_solved_pct": 77
  }
]

未进行防御的 agent 完成了 84% 的任务。CaMeL 在附带安全保证的情况下完成了 77% 的任务。这些是论文在一个基准测试中公布的数据,并不代表您的工作负载。两者之间的差距,大致反映了当前实现真正安全保证所需付出的代价。

在这种设计进入您日常使用的工具之前,应假设 agent 迟早会遭到入侵,并让这类事件尽可能不造成影响。这正是应使用一次性机器、限定范围的凭据、受控出站流量,并养成查看差异习惯的全部理由。

FAQ

我可以通过告诉代理忽略文件中的指令来阻止提示注入吗?

不能。这句话与攻击内容位于同一个上下文窗口中,会与攻击者的文本处于同等地位并相互竞争。Claude Code 的文档明确区分了两者:提示或 CLAUDE.md 中的指令会影响代理尝试执行的操作,但不会改变工具允许执行的操作。应将指令文件视为意图声明,并将任何依赖的限制写入权限规则、PreToolUse hook 或防火墙规则。

如果代理只操作我自己的代码仓库,提示注入仍然是实际风险吗?

是,因为代码仓库中充满了并非由您编写的文本。依赖项 README 文件、lockfile URL、测试 fixture、vendored code 以及 npm install 的输出,都会在普通任务中进入代理上下文。通过 issue tracker 或文档网站获取的任何内容也会以同样方式进入。代理读取的内容越多,风险越高,而有用的代理通常会读取大量内容。

在容器中运行代理能解决这个问题吗?

这可以限制损害,但前提是同时移除凭据。若容器转发了您的 SSH agent、环境变量中包含云凭据,且网络访问不受限制,攻击者几乎可以获得主机能够提供的一切。容器真正提供的是一个可以删除的文件系统,以及一个可用于强制实施出站规则的干净环境。请再配合使用限定为单个代码仓库的 token。

哪项单独的改动最能降低风险?

从代理命令继承的环境中移除长期有效的凭据,然后为该机器设置默认拒绝的出站网络策略。两者结合后,可以破坏致命三要素中的第三个条件:文本仍然可以劫持代理,但代理能够访问的数据无处可去。审批提示和 diff 审查同样有帮助,但它们依赖人员在长时间运行过程中持续保持警惕,因此优先级低于前两项改动。