"模型自己关掉了沙箱------把钥匙交给了囚犯"
OpenClaude项目安全公告里有一句话被我用荧光笔划了三遍:"The model/agent is not a trusted principal."这个项目的Bash工具输入Schema里暴露了一个叫dangerouslyDisableSandbox的参数。LLM可以在tool_use响应中把它设为true,配合默认的allowUnsandboxedCommands: true配置------一条提示注入就能让模型逃出沙箱,在宿主机上执行任意命令。
开发者把沙箱开关做成了模型可控制的输入参数。这相当于把监狱的钥匙挂在囚犯的脖子上,还指望他自觉不打开。
同样的错误正在整个行业密集重演。2026年初,Cursor IDE爆出MCP协议RCE漏洞(CVE-2025-54135)。攻击者构造一个恶意Git仓库,AI会在后台静默连接攻击者定义的MCP Server,执行恶意指令。更早的LangChain CVE-2025-68664(CVSS 9.3)暴露了序列化注入漏洞------一个包含lc键的字典在反序列化时被误识别为合法的LangChain对象,攻击者可提取OpenAI API密钥等敏感环境变量。2026年7月,pydantic曝出CVE-2026-65975------从不受信任的消息历史中剥离"悬空"的工具调用,导致远程客户端能够以客户端提供的参数而非模型生成的参数来触发已注册的工具。Spring AI的CVE-2026-59318更直接:向模型广告的工具列表没有被严格执行,未授权工具可被调用。
这些漏洞有一个共同点:信任边界被放在了错误的位置。 模型输出被当成了可信输入,安全控制字段被模型自由操控,协议层面的"承诺"没有被运行时强制校验。
今天,我们从协议校验、执行沙箱、审计追溯三个层面,拆解Tool Calling从"模型输出"到"工具执行"的完整信任链路设计。
一、协议校验:信任的第一道闸门
1.1 模型的输出不可信
Tool Calling的第一步是模型生成一个结构化的工具调用请求------工具名称、参数、以及可能的控制字段。但一个根本性的问题在于:模型的输出天然不可信。
OpenClaude的漏洞暴露了这种错误的极端版本。Spring AI的漏洞性质相同------框架将工具列表"广告"给模型作为边界,但调度时没有严格执行,攻击者通过提示注入即可触发未授权工具。CVE-2026-65975则展示了另一种攻击路径:攻击者绕过模型,直接向系统提交伪造的工具调用。
信任边界的第一个原则:工具调用的安全决策------调用什么工具、传什么参数------必须由确定性的代码做出,而不是由LLM"决定"。
1.2 五项前置校验:网关层的"五道闸门"
生产级部署在工具调用到达执行器之前,应该在网关层运行五项校验:
第一,Schema校验(Schema Validation) 。模型生成的每一个参数必须严格匹配工具的JSON Schema。拒绝不合规的调用,不要试图"修复"。
第二,工具白名单校验(Tool Allowlist) 。检查工具名称是否在当前请求的允许列表中。Spring AI的漏洞根源就在于缺少这一层。
第三,参数授权(Argument Authorization) 。不仅检查"调什么工具",还要检查"传什么参数"------某些参数组合可能绕过权限控制。
第四,幂等键附加(Idempotency Key) 。为每个可变的写操作附加幂等键,防止重试导致重复副作用。
第五,审计记录生成(Audit Record) 。在工具执行前就生成审计记录,记录"谁、什么时候、调了什么工具、传了什么参数"。
python
from pydantic import BaseModel, validator
from typing import List, Optional, Dict, Any
class ToolCall(BaseModel):
"""工具调用------在进入执行器前必须通过校验"""
name: str
arguments: dict
request_id: str
tenant_id: str
@validator('name')
def name_must_be_allowed(cls, v, values):
# 从请求上下文获取该租户允许的工具列表
tenant_allowed = get_allowed_tools(values.get('tenant_id'))
if v not in tenant_allowed:
raise ValueError(f"Tool {v} not in allowlist for tenant")
return v
@validator('arguments')
def arguments_must_match_schema(cls, v, values):
schema = get_tool_schema(values.get('name'))
if not validate_against_schema(v, schema):
# 拒绝,不"修复"
raise ValueError("Arguments do not match tool schema")
return v
@validator('arguments')
def no_security_controls_in_input(cls, v):
"""安全控制字段绝不能被模型控制"""
forbidden_keys = ['dangerouslyDisableSandbox', 'allow_unsandboxed',
'skip_validation', 'bypass_audit']
for key in forbidden_keys:
if key in v:
raise ValueError(f"Forbidden parameter: {key}")
return v
核心原则:工具调用的安全决策必须由确定性的代码做出,而不是由LLM"决定"。
1.3 准入即信任:工具注册的"默认拒绝"
MCP官方SDK默认给工具"不受限制地访问网络、文件系统、环境变量和子进程"。这是一种"默认允许"的安全模型------工具一旦注册,就拥有了全部权限。
ZeroMCP的做法是反过来的:默认拒绝,显式授予。工具必须在声明中明确列出需要的权限------网络允许列表、文件系统读写声明、执行控制。超出声明范围的任何操作,都被沙箱拦截。
python
# ZeroMCP风格的工具权限声明
tool = {
"name": "fetch_data",
"description": "Fetch data from our internal API",
"permissions": {
"network": ["api.internal.com"], # 只能访问这个域名
"filesystem": "read", # 只能读,不能写
"exec": False # 不能创建子进程
},
"input_schema": {...}
}
准入控制是信任链的第一环------通过白名单和权限声明,把"信任"变成"可审计的契约"。
1.4 反序列化的"禁地"
LangChain的load()函数有一条严厉的安全警告:"永远不要对不受信任或用户提供的输入调用" 。这个函数看起来只是在加载一个JSON对象,实际上它在做反序列化------解析lc标记、解析构造函数、执行类初始化。一个看似无害的JSON解析,在特定条件下就是eval()。
CVE-2025-68664正是利用了这一机制------攻击者通过LLM响应注入一个包含lc键的字典,在反序列化时被误识别为合法的LangChain对象,从而窃取环境变量。
生产级防御:
- 永远不要对LLM输出或用户输入调用
load()/loads() - 使用
allowed_objects模式限制反序列化范围 - 将序列化数据视为信任边界
二、执行沙箱:信任的物理边界
如果说协议校验是"安检门",执行沙箱就是"隔离室"------即使恶意调用通过了校验,也无法造成实际破坏。
2.1 一个沙箱不够:按工具隔离
当前大多数AI Agent沙箱犯了一个相同的错误:把所有工具放在同一个沙箱里。一个Web搜索工具和一个Shell执行工具共享同一个容器------Web搜索工具能读到Shell工具的环境变量,包括API密钥。
按工具隔离的核心思想是:每个工具调用运行在自己的沙箱中,策略从该工具的声明能力中派生。没有声明的能力就没有权限。
2.2 三层隔离:从容器到内核
生产级Agent沙箱通常采用分层隔离策略:
L1:容器级隔离(Docker + seccomp) ------每个工具调用在临时容器中运行。网络禁用、无持久存储、无特权、资源配额限制。配合严格的seccomp配置文件,阻止ptrace、bpf、mount等危险系统调用。
L2:用户态内核隔离(gVisor) ------当需要更强的隔离时,使用gVisor运行时。gVisor在用户空间拦截系统调用,捕获seccomp无法预测的攻击模式。冷启动约50ms。
L3:微虚机隔离(Firecracker) ------对于最高风险的执行场景,使用Firecracker microVM。每个Agent运行在完全隔离的虚拟机中。
python
# 按风险等级选择沙箱层级的示例
class SandboxManager:
def select_sandbox(self, tool_call: ToolCall) -> str:
risk_level = self._assess_risk(tool_call)
if risk_level == "low":
return "docker_seccomp" # L1
elif risk_level == "medium":
return "gvisor" # L2
else:
return "firecracker" # L3
def _assess_risk(self, tool_call: ToolCall) -> str:
# 基于工具类型、参数、权限声明综合评估
if tool_call.name in ["execute_command", "write_file"]:
return "high"
if tool_call.name in ["read_file", "search"]:
return "low"
return "medium"
关键设计决策:默认即安全------如果未指定执行位置,一律使用最高级别的沙箱。
2.3 按需行为沙箱:先试运行,再放行
2026年IEEE发表的一项研究提出了一种按需行为沙箱 方法。核心思路是:当工具调用被评估为模糊或高风险时,先在隔离沙箱中执行一次,监控其文件、网络、进程和权限相关的影响;只有通过检查的操作才会被释放到真实环境。
在200个高风险任务的评估中,该框架将执行幻觉率降至0.0%,额外沙箱延迟约350ms。
核心原则:沙箱不是"设置一次就高枕无忧"的静态防御,而是"在真实执行前先验证"的动态边界。
2.4 MCP v2.0的多层沙箱架构
MCP v2.0框架下构建了从网络层、系统层到应用层的多层沙箱隔离架构 。同时引入沙箱逃逸检测与响应系统,对沙箱逃逸行为进行实时检测和自动响应。
Claude Managed Agents在2026年5月宣布支持自托管沙箱 ------Agent可以在你控制的沙箱中运行,并通过MCP隧道连接到私有网络中的MCP Server。这标志着沙箱从"可选功能"变成了"托管服务的标准配置" 。
三、审计追溯:信任链的闭环
3.1 看不见的工具调用等于没有调用
如果一次工具调用没有被记录下来,那它可能根本没发生过------也可能发生过但没人知道。
AgentTrust 在2026年提出了一种运行时安全拦截层------在工具调用执行前拦截、分类,返回allow、warn、block或review四种裁决。它达到了95%的裁决准确率 和毫秒级延迟。这套结构可以理解为Agent时代的"工具调用防火墙" 。
3.2 不可变审计轨迹
nolabs在其开源运行时nono中提供了内核级沙箱 ,并创建了Agent操作的审计轨迹。
ZeroMCP的另一个核心设计是权限日志 ------每一个权限决策都被记录。不可变的审计日志回答了三个问题:谁调用了什么工具、传了什么参数、结果是什么。
python
import json
from datetime import datetime
from typing import Dict, Any
class ToolCallAuditor:
"""工具调用的审计层------拦截、记录、决策"""
def __init__(self):
self.audit_log = []
async def intercept(self, tool_call: Dict[str, Any]) -> Dict[str, Any]:
# 1. 记录原始请求(执行前)
entry = {
"timestamp": datetime.utcnow().isoformat(),
"tool": tool_call.get("name"),
"arguments": tool_call.get("arguments"),
"caller_id": self._get_caller_id(),
"request_id": tool_call.get("request_id"),
"status": "pending"
}
# 2. 执行工具
try:
result = await self._execute_tool(tool_call)
entry["status"] = "success"
entry["result_preview"] = str(result)[:500]
return result
except Exception as e:
entry["status"] = "error"
entry["error"] = str(e)
raise
finally:
# 3. 不可变审计日志------append-only
self.audit_log.append(entry)
await self._persist_audit_entry(entry)
3.3 可验证性:审计不是"信任",是"证明"
IETF的一份草案提出了WCA(Warrant Certificate Authority)------一种端到端的加密证明基础设施。它关注的不是工具调用"对不对",而是工具调用**"有没有发生过、是谁发起的、传了什么参数"**。
审计不是事后诸葛亮,它是信任链的闭环。 没有审计,你就无法知道信任边界是否被突破;没有不可变审计,你就无法在被突破后追溯根源。
四、完整链路:从模型输出到工具执行的信任传递
把三个层面串联起来,完整的Tool Calling信任链路应该是这样的:
第一层:协议校验(准入)
- 模型输出结构化的工具调用请求
- Schema校验------参数类型、必填项、枚举值强校验
- 工具白名单校验------检查工具是否在允许列表中
- 拒绝任何包含安全控制字段的输入 (如
dangerouslyDisableSandbox) - 敏感操作触发二次确认(人机协同)
第二层:执行沙箱(隔离)
- 每个工具调用在独立沙箱中执行
- 沙箱策略从工具的声明能力中派生------默认拒绝所有未声明的能力
- 网络、文件系统、环境变量、子进程全部受限
- 高风险操作先在行为沙箱中试运行
第三层:审计追溯(闭环)
- 每一次工具调用都被记录
- 审计日志不可篡改
- 支持事后追溯和实时拦截
python
# 完整信任链路的简化实现
class TrustedToolCallPipeline:
"""从模型输出到工具执行的完整信任链路"""
def __init__(self):
self.validator = ToolCallValidator()
self.sandbox = SandboxManager()
self.auditor = ToolCallAuditor()
self.policy_engine = PolicyEngine()
async def execute(self, raw_call: dict) -> Any:
# Layer 1: 协议校验
validated = self.validator.validate(raw_call)
# Layer 2: 策略引擎(权限+配额)
if not self.policy_engine.check(validated):
raise PermissionError("Policy check failed")
# Layer 3: 审计(执行前记录)
await self.auditor.record_pending(validated)
# Layer 4: 沙箱执行
sandbox = self.sandbox.get_sandbox(validated)
result = await sandbox.execute(validated)
# Layer 5: 审计(执行后记录)
await self.auditor.record_completed(validated, result)
return result
五、总结:信任边界的三条铁律
铁律一:模型/Agent不是可信主体。 模型的输出可以被提示注入操控,工具调用的参数可以被恶意构造。安全决策必须由确定性的代码做出,而不是由LLM"决定" 。
铁律二:默认拒绝,显式授予。 不要让工具"注册即拥有全部权限"。每个工具应该声明它需要什么能力------访问哪些域名、读写哪些目录、是否需要网络------超出声明的任何操作都被拦截。最小权限是MCP安全的四大支柱之一。
铁律三:审计不是可选项,是必需品。 没有审计的信任是盲目的信任。每一次工具调用都应该被记录、被追溯、被审计。OWASP最新发布的《AI Agent安全Top 10》已将"过度自主"列为核心风险。Forrester调研显示,58%的企业Agent故障源于工具调用错误------这些错误中相当一部分本可以被审计追溯发现。
Tool Calling让LLM从"只会说"变成了"还能做"。但"能做"的能力越大,"做坏事"的后果也越大。信任边界的本质不是"信不信任模型",而是**"在模型不可信的前提下,如何设计一个让伤害最小化的系统"** 。这需要协议校验、执行沙箱和审计追溯三层防线共同构成一个完整的信任链路------任何一层的缺失,都是把钥匙交到了囚犯手上。