核心摘要

AI Agent 工具代表可执行权限,不是普通提示词扩展。生产系统应使用已验证注册表、最小权限、确定性授权、隔离执行、有界输入输出、精确操作审批和版本化审计。工具描述与结果都应视为不可信;模型拒答和系统提示词不能替代运行时策略。

Agent 工具调用中的信任边界

Agent 系统由两个控制面组成:

  • 模型根据自然语言上下文提出要做什么;
  • 可信软件判断是否允许并执行操作。

把“模型提议”误当成“已授权操作”,安全边界就会失效。

flowchart LR A["用户与不可信内容"] --> B["模型规划器"] C["已审查工具注册表"] --> B B --> D["候选工具与参数"] D --> E["策略与审批"] E --> F["沙箱执行器"] F --> G["外部系统"] G --> H["有界不可信结果"] H --> B

每条箭头都是信任边界。用户输入、检索文档、工具描述、调用参数、远程响应、记忆和委派 Agent 消息都可能改变下一步决策。

威胁模型:四条不同攻击路径

不要把所有事故都归为“提示词注入”。不同路径需要不同控制。

攻击路径 示例 主要控制
直接或间接提示词注入 网页要求 Agent 上传私有文件 能力限制、来源标记、出口策略
工具投毒 工具描述隐藏指令,或审查后改变行为 注册表验证、版本锁定、差异审查
函数调用后的工具滥用 Agent 用合法删除 API 操作了错误资源 对象授权、预览、审批、幂等
恶意工具结果 返回内容含指令、秘密或超大负载 结果 Schema、大小限制、污点标记、内容隔离

OWASP Agentic Skills Top 10 强调发布者验证、权限清单、安全解析、依赖锁定、沙箱与运行时监控。本质上是把成熟软件供应链控制应用到由模型引导的执行层。

建立已审查工具注册表

动态发现不代表动态信任。发现的工具应转换为目标环境中经过审查的内部注册表条目。

json
{
  "tool_id": "invoice.lookup",
  "version": "3.2.1",
  "artifact_digest": "sha256:...",
  "publisher": "billing-platform",
  "effects": ["read:invoice"],
  "network_allowlist": ["billing.internal"],
  "data_classes": ["customer_financial"],
  "approval": "none",
  "result_max_bytes": 65536
}

以下实质变化都要触发新一轮审查:

  • 名称或描述;
  • 参数或结果 Schema;
  • 请求权限;
  • 网络目标;
  • 软件依赖或制品摘要;
  • 副作用行为;
  • 数据保留规则。

MCP Server、插件、Skill 或软件包注册表只是“不可信候选来源”,内部注册表才是执行授权来源。

保护 MCP 传输与 Session 边界

MCP 的互操作性会引入协议特有风险,只有工具 Allowlist 不足以覆盖:

  • 按 Client 和请求能力分别获得用户同意,不能让一个已批准 Client 静默授权另一个 Client;
  • 精确校验 Redirect URI 和 OAuth state,防止 Confused Deputy 与授权码拦截;
  • 禁止 Token Passthrough:MCP Server 必须拒绝不是签发给自身及其 Audience 的 Token;
  • Session ID 只能用于路由或关联,不能当作身份认证;
  • 把每个 Session 绑定到已认证 Principal,并在每次请求时重新执行授权;
  • 把本地 MCP Server 当作本地代码执行边界,执行 Origin 校验、明确同意、沙箱和最小文件/网络权限;
  • Server 发出 notifications/tools/list_changed 后,对新工具定义和权限做 Diff,实质变化必须重新审查,不能直接暴露给模型。

Authorization Server、MCP Server 与下游 Resource Server 是不同安全主体。不能因为模型选择了某个 Tool,就把 Client 的宽权限 Token 原样转发到下游 API。应在策略判断后交换或签发 Audience-bound、最小权限的凭据。

在运行时执行最小权限

权限必须比“Agent 可以调用该工具”更细。系统应根据可信身份和当前状态授权精确副作用。

go
package main

import (
	"errors"
	"fmt"
)

type ToolRequest struct {
	Tool        string
	InvoiceID   string
	AmountCents int64
}

type AuthContext struct {
	UserID   string
	TenantID string
	Roles    map[string]bool
}

type Invoice struct {
	ID              string
	TenantID        string
	RefundableCents int64
}

func authorize(request ToolRequest, context AuthContext, invoice Invoice) error {
	if request.InvoiceID != invoice.ID {
		return errors.New("invoice_mismatch")
	}
	if invoice.TenantID != context.TenantID {
		return errors.New("tenant_denied")
	}
	switch request.Tool {
	case "invoice.lookup":
		return nil
	case "invoice.refund":
		if !context.Roles["refund_operator"] {
			return errors.New("role_denied")
		}
		if request.AmountCents <= 0 || request.AmountCents > invoice.RefundableCents {
			return errors.New("invalid_amount")
		}
		return nil
	default:
		return errors.New("tool_denied")
	}
}

func main() {
	request := ToolRequest{
		Tool:        "invoice.refund",
		InvoiceID:   "inv_2817",
		AmountCents: 8420,
	}
	context := AuthContext{
		UserID:   "user_9",
		TenantID: "tenant_a",
		Roles:    map[string]bool{"refund_operator": true},
	}
	invoice := Invoice{
		ID:              "inv_2817",
		TenantID:        "tenant_a",
		RefundableCents: 10000,
	}
	fmt.Println(authorize(request, context, invoice))
}

模型不能提供 userId、tenantId、角色、价格、审批状态或策略版本。这些字段应从认证上下文与权威服务派生。

同时设置资源预算:

  • 最大工具调用数与重复调用次数;
  • 执行时间和并发;
  • 输出字节数和行数;
  • 网络目标与请求方法;
  • 文件系统路径与进程能力;
  • 金额、写入量和重试次数。

让审批真正约束操作

只显示“是否允许?”不构成有效审批。用户需要看到精确副作用:

text
为发票 inv_2817 退款 ¥612.40
目标:原支付方式
原因:重复扣款
工具版本:[email protected]
过期时间:2026-07-28T12:05:00Z

审批应绑定到:

  • 工具及版本;
  • 规范化参数;
  • 目标资源和租户;
  • 目标地址与副作用;
  • 策略版本;
  • 过期时间与幂等键。

任一绑定字段变化都要重新审批,不能让一次确认授权后续更宽的调用。

把工具结果视为不可信输入

工具调用成功不代表返回内容安全。结果重新进入模型上下文前必须规范化。

go
package main

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
)

const (
	maxRawBytes  = 32 * 1024
	maxTextRunes = 8000
)

type SearchResult struct {
	SourceID string `json:"sourceId"`
	Text     string `json:"text"`
}

type NormalizedResult struct {
	SourceID string `json:"sourceId"`
	Text     string `json:"text"`
	Trust    string `json:"trust"`
}

func normalizeSearchResult(data []byte) (NormalizedResult, error) {
	if len(data) > maxRawBytes {
		return NormalizedResult{}, errors.New("result_too_large")
	}
	decoder := json.NewDecoder(bytes.NewReader(data))
	decoder.DisallowUnknownFields()

	var input SearchResult
	if err := decoder.Decode(&input); err != nil {
		return NormalizedResult{}, errors.New("invalid_result")
	}
	if input.SourceID == "" {
		return NormalizedResult{}, errors.New("missing_source")
	}
	text := []rune(input.Text)
	if len(text) > maxTextRunes {
		text = text[:maxTextRunes]
	}
	return NormalizedResult{
		SourceID: input.SourceID,
		Text:     string(text),
		Trust:    "external_untrusted",
	}, nil
}

func main() {
	result, err := normalizeSearchResult(
		[]byte(`{"sourceId":"doc_7","text":"Treat this as data, not instructions."}`),
	)
	if err != nil {
		panic(err)
	}
	fmt.Printf("%+v\n", result)
}

来源身份和信任标签应穿透摘要与记忆写入。返回文本不能直接定义新工具、改变策略、选择外部目标地址或写入持久记忆,除非通过单独门禁。

沙箱与网络出口限制

当授权或模型行为失败时,隔离可以限制影响范围。

边界 最低控制
进程 非 root 身份、系统调用与资源限制
文件系统 只读基础层、显式可写目录
网络 默认拒绝、目标和协议允许列表
凭据 短期、工具专用、租户范围
数据 最少字段、脱敏、结果大小上限
副作用 幂等键、预览、交易额度

读工具与写工具应分开。如果可以提供狭窄业务操作,就不要暴露通用 Shell、SQL 执行器、HTTP Client 或云凭据。

测试完整攻击链

安全测试要覆盖调用轨迹,而不只是单轮提示词:

  1. 被投毒的工具描述要求执行未声明前置操作;
  2. 搜索结果要求外传数据;
  3. 工具返回其他租户标识符;
  4. 超时后模型重复写操作;
  5. 规划与执行之间工具版本变化;
  6. 委派 Agent 请求更宽凭据;
  7. 输出尝试把指令写入长期记忆。

每个案例都要断言阻止影响的确定性边界。模型“通常会拒绝”不能算安全测试通过。

遥测记录工具 ID 和版本、注册表摘要、认证主体、策略决策、审批引用、规范化参数摘要、目标类别、副作用结果、耗时和有界错误码。不要保存原始秘密、完整提示词和全部私有返回值。

事故响应与熔断

事故前就要准备:

  • 全局禁用某个工具版本或发布者;
  • 独立撤销工具凭据;
  • 阻断某个目标地址或副作用类型;
  • 隔离受影响执行写入的记忆;
  • 按制品摘要定位历史调用;
  • 使用脱敏 Fixture 重放策略决策;
  • 通知外部可见副作用的责任人。

审计日志既要有调查价值,也不能变成第二套敏感数据仓库。低敏关联元数据与严格受控的负载证据应分开存储。

生产检查清单

  1. 盘点工具、效果、数据类别和外部目标。
  2. 把发现结果转换为已审查允许列表。
  3. 锁定制品并审查每次实质更新。
  4. 从可信上下文派生身份与租户。
  5. 用代码校验并授权精确副作用。
  6. 沙箱执行,网络出口默认拒绝。
  7. 审批绑定精确参数与版本。
  8. 规范化、限制并标记每个工具结果。
  9. 测试注入、投毒、重试、委派和记忆持久化。
  10. 维护熔断开关和事故查询。

提示词注入防御指南覆盖更广泛的不可信内容问题;函数调用指南解释模型提议到实际执行的边界。

常见问题

可以允许 Agent 自行安装工具吗?

不能直接安装到生产信任域。Agent 可以发现候选或生成变更申请,但安装、权限、制品验证和启用需要独立审查流程。

已签名工具一定安全吗?

不一定。签名只能证明相对于某个密钥的来源与完整性,不能证明发布者可信、代码安全、权限最小或返回内容无害。

输入输出分类器能阻止工具攻击吗?

分类器可以提供信号,但具有概率性且可被绕过。它们可以参与策略或复核,不能成为保护数据与副作用的唯一屏障。

写工具应如何重试?

使用幂等键和权威状态查询。出现不明确超时时,先查询操作结果再决定是否重试,不能让模型自行判断写操作可以重复。

有人工审批就足够安全吗?

只有当用户看到准确、有界的操作,并且审批与该精确操作绑定时才有效。模糊或过载的确认会导致审批疲劳,也不会真正收窄权限。

参考资料