写在前面:这篇文章是怎么来的
这篇文章不是从一份安全规范开始的,而是从我实现自己的 Coding Agent 时产生的一点不安开始的。
在 PaiCLI 的 Policy 模块里,我先后实现了几项看起来很合理的安全能力:
- 用 PathGuard 解析路径的真实位置,阻止文件工具越过当前工作区;
- 用 CommandGuard 拦截删除根目录、格式化磁盘、关机重启等明显破坏性命令;
- 对文件写入、Shell 和非只读 MCP 操作增加 HITL 人工审批;
- 用脱敏 JSONL AuditLog 记录工具输入、执行结果、审批信息和工作目录。
做到这里,Agent 已经不像最初那样直接相信模型输出并执行了。它有路径边界、有命令检查、有人工确认,也留下了操作记录。
有一天和朋友聊天,说起我这个小demo,我说这是我要写在简历上去秋招的项目,然后简单介绍了一下。他说我来考考你。
如果危险命令没有出现在黑名单里怎么办?你又怎么能保证黑名单和危险命令就可以拦住所有危险呢?难不成自己一直去不断维护一直增加黑名单?即使操作在cwd项目目录,就能说明安全吗?实际上就随便问了两三个问题,我就答不上来了了。
回来之后,我就开始思考agent的安全策略。这些问题已经不是继续添加几条正则就能解决的了。于是我就和 GPT5.6 讨论,然后又试了试最新的Qwen3.8,在AI的帮助下,加深了对安全策略的一些理解并整理了笔记,最后再让 Codex 帮我润色修改,最终整理成了这篇博客。
因此,本文是从 PathGuard、CommandGuard、HITL 和 AuditLog 这一层防护的漏洞为起点,抛开代码本身从逻辑上去思考如何去做agent的安全策略。当 Agent 从“会回答问题”变成“能读文件、执行命令、访问网络和调用外部系统”之后,一套安全策略究竟还缺什么?
这套安全策略到底在防谁
整理整套安全策略时,我一直有一个疑问:我们到底是在防模型,还是在防外部攻击者?
如果是在防外部攻击者,对方可能根本不知道我的 Agent 是怎么实现的,又怎么影响它?
答案是:大多数时候,模型不是攻击者,而是一个可能被误导、被借用的决策者;Tool Runtime 才是把决定变成真实副作用的执行者。
外部攻击者也不需要直接登录 Agent,更不一定需要知道内部代码。他只要控制 Agent 将来可能读取或执行的一段内容,就可能影响整条执行链:
外部攻击者 │ ├── 在 README、Issue、网页、邮件或日志中放入恶意指令 ├── 在仓库中放入 conftest.py、Makefile、Git hook 或安装脚本 └── 提供被污染的依赖、插件、MCP 工具描述或工具返回值 │ ▼ Agent 读取内容或运行项目 │ ▼ 模型把恶意内容误当成可信指令 │ ▼ 模型提出 Tool Call │ ▼ Tool Runtime 使用开发者的文件、网络和身份权限执行 │ ▼ 文件被修改、秘密被读取、数据被外传这就是间接攻击。攻击者没有直接控制模型,而是控制了模型的输入、执行对象或工具供应链,再让 Agent 替他完成后续动作。
举个例子,假设 Coding Agent 收到任务:“修复这个项目的测试失败。”
它会读取 README、执行测试、根据报错安装依赖、修改文件并重新运行命令。对用户来说,这是一条正常的开发流程;对攻击者来说,这条流程却提供了很多可以提前布置的入口:
- README 可以把恶意指令伪装成项目说明;
pytest可以自动加载仓库里的conftest.py;npm install可以执行依赖包的安装脚本;- Shell 可以读取工作区之外的文件;
- 网络可以把环境变量和源码发送出去;
- 后台子进程可以在主命令结束后继续运行。
攻击者不一定要正面突破某条安全规则。他只要找到一条 Agent 会自然经过、但防守方没有建模的路径,就可以从旁边绕过去。
所以 Agent 安全实际上同时防三种情况:
- 防模型在没有恶意的情况下犯错;
- 防外部攻击者通过不可信内容、恶意仓库和供应链借用模型;
- 防某一层已经失守后,Tool Runtime 继续把影响扩大到宿主机、秘密和外部系统。
路径围栏、命令黑名单、人工审批和审计日志分别解决了其中一部分问题,却没有任何一层能够独自承担完整的安全责任。本文接下来就从进攻方寻找入口、防守方逐层收缩能力的博弈出发,讨论一套 Agent 安全策略应该如何形成。
一、真正的对手不是“恶意模型”
讨论 Agent 安全时,很容易把问题简化成:“如果模型生成了危险命令怎么办?”
这个问题太窄了。真实环境里的威胁至少来自四个方向。
模型误操作
模型没有恶意,但可能幻觉路径、拼错参数、误解用户意图,或者忽略变量为空、工作目录错误等运行条件。路径围栏、参数校验、危险模式检测和人工确认,主要是在处理这一类问题。
间接提示注入
Agent 会读取 README、Issue、网页、日志、邮件、图片文字和工具返回值。这些内容原本应该作为数据,却可能夹带“忽略原任务并执行某个动作”的指令。
模型需要理解自然语言,攻击内容同样使用自然语言。防守方无法只靠一句“把外部内容当作数据”就保证模型永远不会被劫持。
恶意仓库和供应链
仓库本身就是代码。测试配置、构建脚本、Git hook、依赖安装脚本、编译插件都可能执行程序。
当 Agent 运行 pytest、make 或 npm install 时,它不是在运行一个抽象的“安全开发动作”,而是在执行仓库作者和依赖作者提供的代码。
恶意或被入侵的工具
MCP Server、插件和远程 API 也属于供应链。工具描述可以隐藏提示注入,工具可以错误声明自己是只读,也可能在升级后改变行为。
因此,Agent 安全要处理的不是“模型会不会变坏”,而是:
当模型、上下文、仓库或工具中的任意一环不可信时,它们最多能够造成多大影响?
二、攻防博弈的核心:攻击者寻找“假设裂缝”
很多漏洞都可以还原成同一个结构:
防守方以为系统会怎样 ↓实际运行时可能完全不同 ↓两者之间的差,就是攻击入口例如:
| 防守方的假设 | 实际可能发生的情况 |
|---|---|
| 黑名单已经覆盖危险命令 | 危险语义可以通过其他程序和参数无限变形 |
| 路径检查通过就能安全使用 | 检查后文件可能被替换成符号链接 |
| 文件位于工作区就可以访问 | 工作区内也有密钥、特殊文件和挂载目录 |
| 命令退出就代表任务结束 | 子进程和后台任务可能继续运行 |
| 用户看到的就是实际执行的 | 控制字符、Unicode 和解析差异可以制造两份内容 |
| 写了审计日志就能追踪 | Agent 可能拥有修改或删除日志的权限 |
攻击者的策略是寻找这些裂缝。防守方可以用三个基本动作应对。
翻默认
如果坏情况数量无限,就不要追着补黑名单,而是默认拒绝,只放行能够明确描述的好情况。
默认允许 + 枚举禁止项 ↓默认拒绝 + 枚举允许项限能力
如果无法可靠判断一个动作的真实意图,就不要把全部希望放在预测上,而要限制它实际拥有的文件、网络、进程、身份和凭据能力。
统一事实
检查、审批、执行和审计必须围绕同一个规范化动作展开,不能让系统在不同阶段看到不同版本。
这三个动作不是三项具体产品,而是可以不断推导安全措施的认知框架。
三、第一轮博弈:PathGuard 对抗路径越界
最常见的路径围栏实现大致如下:
class PathGuard: def __init__(self, root: str | Path): self.root = Path(root).resolve()
def validate(self, value: str | Path) -> Path: candidate = Path(value) if not candidate.is_absolute(): candidate = self.root / candidate
resolved = candidate.resolve() resolved.relative_to(self.root) return resolved防守方先把工作区根目录解析成绝对路径,再把输入路径中的 .、.. 和符号链接解析掉,最后通过 relative_to() 判断真实路径是否仍位于工作区。
它可以有效阻止:
../../secret.txt之类的普通目录穿越;- 直接访问工作区外的绝对路径;
- 检查时已经存在、并指向工作区外的符号链接;
/project与/project-evil这种字符串前缀误判。
这是必要的第一道防线,但攻击方不会停在字符串层。
攻方一:检查和使用之间调包
PathGuard 检查路径后返回一个 Path,文件工具稍后再调用 open()。如果攻击者可以并发修改工作区,就可能在检查通过后、真正打开前,把文件或父目录替换成指向外部的符号链接。
这类问题被称为 TOCTOU:Time of Check to Time of Use。MITRE 在 CWE-367 中指出,只要检查和使用仍然通过文件名分两步完成,资源就可能在中间发生变化。
攻方二:路径在工作区,资源却不属于工作区
路径层级无法完整描述底层对象:
- 硬链接可能让工作区内外的两个名字指向同一个 inode;
- bind mount 可能把宿主机目录挂载到工作区内部;
- Windows junction 和 reparse point 有自己的解析语义;
- FIFO、Unix Socket、设备节点并不是普通文件;
- 工作区内部的
.env、私钥和 Git 配置,本来就不应该被随意访问。
因此,“位于工作区”只能回答位置问题,不能代替资源授权。
攻方三:绕过文件工具
PathGuard 只有在工具主动调用它时才有效。新工具忘记接入、第三方插件直接打开文件、Shell 使用绝对路径或先 cd ..,都可以绕开这层保护。
cwd 只是进程的初始目录,并不是文件系统沙箱。
守方升级
生产环境里的文件安全至少需要继续增加:
- 资源级授权:分别定义哪些目录允许读、允许写、允许创建和允许执行。
- 敏感区域保护:默认禁止
.env、密钥、.git、凭据目录和策略配置。 - 安全打开文件:在打开阶段禁止跟随符号链接,尽量让检查和使用围绕同一个文件描述符完成。
- 文件类型约束:拒绝设备、FIFO、Socket 等特殊文件。
- 写入配额:限制单文件大小、文件数量、总磁盘和写入速率。
- 操作系统隔离:让进程从根本上看不到未授权目录。
PathGuard 仍然值得保留,但它的正确定位是“低成本路径预检”,不是文件系统沙箱。
四、第二轮博弈:CommandGuard 对抗危险命令
命令黑名单通常会先标准化空白,再检查自定义字符串和内置正则:
normalized = " ".join(command.strip().split())
for blocked in blacklist: if blocked in normalized: raise CommandPolicyError(...)
for pattern in destructive_patterns: if pattern.search(normalized): raise CommandPolicyError(...)它适合拦截删除根目录、格式化磁盘、关机重启等明显破坏性命令,也能减少模型幻觉造成的低复杂度事故。
问题在于:防守方检查的是字符串,攻击方利用的是程序语义。
攻方一:改变表达,不改变效果
同一个效果可以通过不同参数顺序、长短参数、解释器、脚本、重定向、构建工具或间接调用完成。
一个命令没有出现 rm,不代表它不能删除文件;没有出现 curl | sh,不代表它不能先下载脚本再执行;只有 python、node 或 make,也不代表行为安全。
OWASP 的 OS Command Injection Defense Cheat Sheet 将白名单、参数化和避免直接调用 Shell 作为主要防御,黑名单只能承担补充作用。
攻方二:利用 Shell 解释器
如果执行器使用 shell=True 或 create_subprocess_shell(),整个字符串会交给 Shell 解释。管道、重定向、变量展开、命令替换、子 Shell 和后台任务都会改变行为。
Python subprocess 安全说明 明确提醒:一旦显式调用 Shell,调用方就要负责处理空白和元字符带来的注入风险。
在 Agent 场景里,问题比传统“固定命令拼接一个用户参数”更严重,因为模型可能控制整条命令。仅靠转义无法把任意 Shell 变成安全能力。
攻方三:使用合法命令完成危险动作
危险并不等于“删除系统目录”。以下动作同样可能造成严重后果:
- 读取环境变量并发送到外部地址;
- 修改 Git hook、Shell 启动文件和定时任务;
- 发布软件包、推送代码或部署生产环境;
- 扫描内网和访问云元数据;
- 启动后台进程;
- 写满磁盘、耗尽内存或创建大量进程。
这些动作可能全部由常见开发工具完成。
守方升级
防守方不应该试图写出一份“完美黑名单”,而应该逐步减少自由 Shell 的使用:
任意 Shell 字符串 ↓解析后的程序与参数 ↓固定程序 + 结构化参数 ↓面向业务的专用 Tool例如,不让模型生成任意 git 命令,而是提供 git_status、git_diff、git_commit 等边界明确的工具。每个工具拥有独立的参数 Schema、风险等级和授权规则。
对于必须保留的 Shell:
- 默认拒绝,明确允许少量程序和参数组合;
- 使用参数数组执行,避免不必要的 Shell 解释;
- 固定可信可执行文件路径;
- 不继承未经清理的 PATH;
- 在沙箱里运行;
- 将未知命令升级为人工审批,而不是默认放行。
CommandGuard 也应该保留,但它只是已知风险探测器,未命中不能证明安全。
五、第三轮博弈:HITL 对抗自动化风险
人工审批解决的是“高风险动作是否得到了人的授权”。它适合文件写入、Shell、删除、发布、外部通信和非只读 MCP 操作。
但是,攻击方也会攻击审批过程本身。
攻方一:让危险动作看起来合理
间接提示注入可以让模型为危险命令生成一段非常可信的解释,例如“这是项目初始化要求”或“必须上传日志才能诊断问题”。
用户审批的往往不只是命令,还包括模型对命令的叙述。如果叙述被劫持,HITL 就可能从安全门禁变成社会工程入口。
攻方二:制造展示与执行差异
控制字符、超长参数、Unicode 同形字符、零宽字符和不同层的转义规则,都可能让用户看到的内容与 Shell 实际解释的内容不同。
攻方三:利用审批疲劳
当所有普通读操作都弹窗时,用户很快会形成机械点击习惯,最终选择“全部允许”。审批越多,不一定越安全。
守方升级
可靠的 HITL 需要满足几个条件:
- 按副作用和不可逆程度划分风险,不是所有动作一刀切;
- 展示规范化后的程序、参数、目标文件、网络目的地和外部副作用;
- 转义控制字符,并对异常 Unicode 提升风险;
- 审批结果绑定工具名、完整参数、用户、会话和有效期;
- 参数发生任何变化后,旧审批自动失效;
- 高影响动作支持二次确认、回滚预览或更强身份验证;
- 保留明确的拒绝、跳过和全局熔断能力。
OWASP AI Agent Security Cheat Sheet 同样强调高影响动作分类、执行前预览、明确审批、审计和中断回滚。
HITL 的职责是确认风险,不是替代底层隔离。
六、第四轮博弈:恶意仓库对抗执行环境
到这里,防守方已经拥有路径检查、命令策略和人工审批。但攻击者仍然可以把恶意行为藏在“被允许的开发动作”里。
pytest → 加载 conftest.py 和插件npm install → 执行生命周期脚本make → 执行仓库定义的 Makefile编译项目 → 执行编译插件和构建脚本在防守方眼里,这是测试和安装;在操作系统眼里,这是任意代码执行。
这说明所有依赖“理解命令含义”的策略都会存在上限。真正的兜底必须进入能力层。
守方:把执行放进笼子
一个面向 Coding Agent 的基础沙箱可以这样设计:
宿主机├── LLM 调用与任务编排├── Policy / HITL / Audit└── 隔离的 Tool Runtime ├── 非 root 用户 ├── 系统目录只读 ├── 仅挂载当前工作区 ├── 默认断网 ├── 不挂载主目录、SSH Agent、Docker Socket ├── 不继承宿主敏感环境变量 └── 限制 CPU、内存、进程和磁盘Linux 可以使用容器、bubblewrap 或 Landlock。Linux 内核的 Landlock 文档 将其定位为非特权进程可使用的访问控制机制,用于限制文件系统和网络等环境权限。
Docker 也提供只读根文件系统、进程数限制等运行参数;网络方面可以使用 --network none 完全隔离容器网络。
沙箱解决的不是“判断命令是否善良”,而是:
即使模型被提示注入、仓库带毒、命令策略判断失败,恶意代码仍然看不到宿主机重要资源,也没有足够能力扩大影响。
七、第五轮博弈:网络出口与数据外传
只限制文件写入还不够。只要进程能够联网,它就可能把读到的数据送出去。
攻击方可以利用:
- HTTP 请求上传源码和环境变量;
- DNS 查询编码少量秘密;
- Git、npm、pip 等合法协议建立外部连接;
- URL 重定向访问内网;
- DNS 重绑定改变目标地址;
- 访问云元数据服务获取临时凭据。
所以“允许联网”本身就是一项高价值能力。
守方策略
防守方应从默认断网开始,再按任务开放最小出口:
- 沙箱默认没有外部网络。
- 安装依赖时通过受控代理,只允许可信仓库域名。
- 每次 DNS 解析和 URL 重定向后重新检查最终地址。
- 拒绝回环、私网、链路本地和云元数据地址。
- 根据工具限制 HTTP 方法、请求体和上传大小。
- 网络工具不应同时持有工作区秘密和任意文件读取权限。
- 记录最终 URL、解析地址、重定向链和数据分类。
网络策略不能只看域名字符串,还要关注最终连接到哪里、发送了什么数据。
八、第六轮博弈:超时、子进程和资源耗尽
很多实现会给命令设置超时,然后在超时时调用 proc.kill()。这并不等于任务已经停止。
如果被杀的是 Shell 父进程,它创建的子进程可能仍然存活;后台任务可能已经脱离;大量输出可能在截断前就占满内存;循环写入可能在超时前耗尽磁盘。
攻击者不一定追求控制宿主机。让系统不可用、持续消耗 API 和计算成本,同样是有效攻击。
守方策略
- 每个任务使用独立进程组或隔离 PID Namespace;
- 超时后终止整个进程树,而不是只杀父进程;
- 使用 cgroup 限制 CPU、内存、进程数和 I/O;
- 限制文件数量、总写入量和临时目录大小;
- 对 stdout/stderr 流式限流,不能等读完后再截断;
- 禁止未托管的
nohup、终端复用器和任意后台驻留; - 必须运行的后台任务进入任务管理器,具备身份、状态、资源和终止接口;
- 对 Agent 循环增加最大轮数、最大工具调用数、Token 和费用预算。
真正的运行时治理要保证所有副作用都“可见、可算、可杀”。
九、第七轮博弈:身份、环境变量与秘密
如果 Tool Runtime 与开发者使用同一个系统身份,它自然拥有开发者的文件权限、网络权限和环境变量。
即使 sudo 被命令黑名单阻止,进程仍可能访问:
- LLM API Key;
- GitHub Token;
- 数据库连接串;
- SSH Agent;
- 云厂商凭据;
- Docker Socket;
- 内部管理接口。
攻击者真正需要的不是 root,而是一把已经存在的高价值钥匙。
守方策略
- Agent 使用独立、低权限身份运行;
- 每个用户、项目和任务之间建立隔离;
- 默认构造干净环境,不直接复制宿主
os.environ; - Tool 按任务领取短期、最小 Scope 的临时凭据;
- 凭据与工具、资源、用户和有效期绑定;
- 使用后立即撤销,不写入工作区和普通日志;
- 把“可以读秘密”和“可以向外发送”拆成不能同时自动获得的能力。
最小权限的价值在于:即使前面的判断全部失败,攻击者拿到的也只是一个没有重要钥匙的低权限进程。
十、第八轮博弈:MCP 扩展与信任转移
MCP 让 Agent 可以动态接入文件、数据库、浏览器、邮件和业务系统。它也把本地安全问题扩展成跨服务器的信任问题。
攻击方可以尝试:
- 在工具描述和返回值里植入提示注入;
- 把有副作用的工具标记为只读;
- 用相似工具名遮蔽可信工具;
- 在用户批准安装后改变工具定义;
- 使用 MCP Server 自身的高权限替低权限用户执行动作;
- 让多个 MCP Server 之间互相传递或外泄数据。
OWASP MCP Security Cheat Sheet 将工具投毒、定义变更、工具遮蔽、Confused Deputy、过宽权限和供应链攻击都列为关键风险。
守方策略
- MCP Server 和工具必须进入显式允许列表;
- 工具描述、Schema 和版本经过审核并固定哈希;
- 定义变化后重新授权,不能沿用旧审批;
- 客户端独立判断风险,不能盲信
readOnlyHint; - 每个 Server 使用独立的最小权限凭据;
- 本地 MCP Server 运行在沙箱中;
- 工具输入和输出都要验证、分类和审计;
- 不同信任级别的 Server 隔离上下文和数据;
- 高风险 MCP 操作仍然走参数绑定的 HITL。
MCP 的本质不是“安全工具协议”,而是一条新的能力接入通道。通道越通用,越需要独立授权和隔离。
十一、第九轮博弈:AuditLog 与反取证
脱敏 JSONL AuditLog 是很好的起点。一行一个事件,方便追加、读取尾部和容忍单行损坏。
一条有价值的安全日志至少应该包含:
{ "timestamp": "2026-07-28T10:00:00Z", "session_id": "session-123", "tool_name": "execute_command", "input": {"command": "..."}, "risk_level": "high", "policy_version": "policy-v3", "approval_id": "approval-456", "approver": "user-789", "outcome": "error", "cwd": "/workspace/project"}但是审计面对两个主要对手。
攻方一:秘密进入日志
仅按 token、password、secret 等字段名脱敏,会漏掉命令字符串、URL、文件内容和嵌套文本中的秘密。日志系统本身可能成为新的数据泄露源。
攻方二:删除或篡改证据
如果日志文件位于 Agent 能访问的位置,恶意进程可以清空、修改或伪造记录。审计和执行使用同一系统身份时,可信度天然受限。
守方升级
- 日志写到 Agent 无权修改的外部系统;
- 使用只追加存储、哈希链或签名提高篡改可见性;
- 记录结构化动作,而不是只记录展示文本;
- 对秘密进行数据分类和内容级脱敏;
- 为调用、审批、执行结果和子进程建立统一关联 ID;
- 记录策略版本和工具定义版本,保证事后可以还原决策;
- 对异常频率、连续拒绝、权限提升和外传行为告警;
- 提供全局熔断,能够停止一个用户、会话或全部 Agent Runtime;
- 高风险写操作前创建快照,失败后能够恢复。
审计不是为了假装永远不会出事,而是为了出事后能够发现、止血、追踪和恢复。
十二、把所有防线连成一条执行链
一套更完整的 Agent 安全执行链可以表示为:
用户输入 / 网页 / 仓库 / MCP 返回值 │ ▼ 不可信上下文边界 │ ▼ LLM 生成行动提议 │ ▼ Tool Schema / 参数校验 / 规范化 │ ▼ 默认拒绝 / 白名单 / 风险分类 │ ▼ 参数绑定的 HITL 审批 │ ▼ 隔离的 Tool Runtime / 沙箱 │ │ │ ▼ ▼ ▼ 文件策略 网络策略 进程与资源配额 │ │ │ └──────────┴──────────┘ │ ▼ 低权限身份与临时最小凭据 │ ▼ 实际副作用与 ToolResult │ ▼ 外置审计 / 监控 / 熔断 / 快照恢复这条链可以按时间分成三个阶段。
| 阶段 | 核心问题 | 主要控制 |
|---|---|---|
| 执行前 | 这件事是否应该开始? | 信任来源、结构化工具、默认拒绝、风险分类、HITL |
| 执行中 | 即使判断错了,后果能否受控? | 沙箱、文件边界、网络隔离、资源限制、最小权限 |
| 执行后 | 能否发现、终止、追踪和恢复? | 外置审计、监控告警、进程治理、熔断、快照 |
纵深防御并不是不断增加相似规则,而是承认每一层都会失败,然后让下一层专门接住上一层漏过来的风险。
十三、PathGuard 和 CommandGuard 还有没有价值
有,而且应该保留。
PathGuard 很适合挡住普通目录穿越和模型生成错误路径;CommandGuard 很适合拦截已知的明显破坏性命令。它们速度快、结果确定、容易测试,也容易向用户解释。
问题不在于它们“没用”,而在于不能把它们承担不了的责任交给它们。
| 控制 | 正确定位 | 不能证明什么 |
|---|---|---|
| PathGuard | 路径规范化与越界预检 | 路径对应的资源一定安全 |
| CommandGuard | 已知危险模式探测 | 未命中的命令一定无害 |
| HITL | 人对高风险动作进行授权 | 被批准的动作不会造成损失 |
| AuditLog | 提供追踪和取证线索 | 日志已经防止事件发生 |
| Sandbox | 限制实际能力和爆炸半径 | 上层策略可以不再校验 |
一套成熟的设计不是在这些控制中选一个,而是让它们分别承担最适合自己的职责。
十四、如何为一个新 Tool 推导安全策略
以后新增数据库、浏览器、邮件或代码执行工具时,不需要先背一份安全清单。可以顺着七个问题推导:
- 定资产:它会接触哪些文件、秘密、网络、进程和外部系统?
- 定来源:哪些参数可能被用户、模型、网页、仓库或第三方工具影响?
- 找裂缝:当前设计依赖了哪些“我以为”?实际可能怎样偏离?
- 翻默认:哪些安全操作可以明确枚举?剩余情况是否应该拒绝或审批?
- 限能力:即使决策被劫持,它最多能读什么、写什么、连到哪里、消耗多少资源?
- 统一事实:校验、审批、执行和日志是否使用同一个规范化动作?
- 准备下一层:这一层失效后,怎样发现、终止、追踪和恢复?
以网页读取工具为例:
| 假设裂缝 | 推导出的防御 |
|---|---|
| 以为网页是数据,实际可能包含恶意指令 | 内容标记为不可信;读取器不持有写权限和秘密 |
| 以为访问公网 URL,实际可能重定向到内网 | 默认拒绝私网和元数据地址;每次重定向重新校验 |
| 以为展示的是原 URL,实际连接到另一个地址 | 记录最终 URL、重定向链和解析地址 |
| 以为读取不会外传,实际后续工具可能发送内容 | 拆分读取与外发能力;网络出口独立授权 |
同样的方法可以迁移到数据库写入、邮件发送、代码发布和远程 MCP 操作。
十五、结语:安全不是预测模型,而是约束后果
Agent 安全最容易陷入的误区,是不断研究“模型为什么会生成这条危险命令”,然后继续向黑名单添加规则。
这种工作有价值,却永远无法完成。Shell、脚本、仓库和外部工具能够表达的行为近乎无限,而防守方能够枚举的字符串始终有限。
更可靠的思路是:
先找出“我以为”和“实际可能”之间的裂缝。数不尽的坏,就翻转默认;猜不透的意图,就限制能力;担心出现两份现实,就让检查、审批、执行和审计共享同一份事实。一层无法消除风险,就让下一层继续接住。
PathGuard 和 CommandGuard 是这套体系的起点,不是终点。
真正的生产级安全策略,需要把结构化工具、默认拒绝、HITL、文件授权、执行沙箱、网络隔离、供应链治理、资源配额、最小权限、秘密管理、外置审计和恢复机制连接成一个闭环。
目标不是证明 Agent 永远不会犯错,也不是证明模型永远不会被劫持。
目标是:即使判断出错,系统仍然能够控制最坏后果。