All posts

本文译自英文原文。 阅读原版

工程技术

AI 智能体连接器的安全工程:九类故障

StaffOS 如何保护其 AI 智能体调用的外部工具:从 SSRF、URL 模板到密钥作用范围,共九类故障,以及我们为每一类故障构建的控制措施。

Vin Lim联合创始人 / 首席技术官,StaffOS
·
·
27 min read

调用外部 API 的 AI 智能体可以读取私有数据,会读取未经任何人审核的内容,还可以向外发送数据。模型外围的软件决定工具可以访问哪些主机、每个请求携带什么、谁可以把已保存的密钥指向某个目标,以及什么内容会返回给模型。本文介绍 StaffOS 如何为我们的智能体所调用的外部工具做出这些决定:九类故障、我们为每一类构建的控制措施,以及常见 HTTP 软件中决定控制措施能否生效的细节。

示例中的主机名、密钥和记录均为虚构。关于第三方软件的陈述,均引用其文档或源代码。

检查清单

以下十二项检查适用于任何向其用户所配置的 URL 发送请求的连接器。每一项都给出一个用于测试的输入,以及解释该项的章节。

  1. 判断传输层实际会发送的 URL。 http://{x}127.0.0.1/admin 必须被拒绝,而不是被 HTTP 客户端展开为回环地址(第 3 节)。
  2. 依据一张明确的表,拒绝每一个不可全局访问的地址。 100.100.100.200、64:ff9b::7f00:1 和 ::ffff:127.0.0.1 都必须被拒绝(第 2 节)。
  3. 固定检查过的地址,并以请求中原样书写的主机作为键。 对 api.example.test. 的请求必须连接到已固定的地址,而不是重新解析(第 2 节)。
  4. 关闭重定向和来自环境的代理设置。 设置了 HTTPS_PROXY 时,请求仍必须直接连接;302 响应必须在未被跟随的情况下返回给你(第 2 节)。
  5. 为 DNS 设定一个能够强制执行的截止时间。 一台永不应答的域名服务器,不得让工作进程被占用超过工具的超时时间(第 2 节)。
  6. 按每个值所处的位置写入它。 Tan Ah Kow 到达 JSON 请求体时必须仍是 Tan Ah Kow;值中的引号不得添加新的键;值为 .. 的路径参数必须被拒绝(第 3 节)。
  7. 既按所属方、也按用途限定已保存密钥的作用范围。 没有管理员权限的成员,不得更改会发送已保存密钥的工具的 URL,也不得运行该工具(第 4 节)。
  8. 为自动调用制定单独的契约。 没有任何所有者确认为只读的 POST 工具,绝不能被自动调用(第 7 节)。
  9. 让平台值处于模型的控制范围之外。 在使用 {{conversation:phone_number}} 的同时又声明了 phone_number 参数的模式,必须被拒绝(第 8 节)。
  10. 替换传输层错误文本,并按每一种写法对已知值脱敏。 DNS 失败时必须返回一句不提及任何主机的固定语句;{"echo":"sk\u002dlive\u002d4f9c"} 返回时必须已被脱敏(第 5 节)。
  11. 把无法读取的内容视为不安全。 未知的认证模式、平台无法解密的凭据,以及嵌套层级超出解码器上限的 JSON 响应体,都必须分别导致拒绝或不予返回(第 4 节和第 5 节)。
  12. 在字节到达时就施加上限。 流式传输 100 MB 的上游,必须在达到上限时被切断,超出上限的部分一律不缓冲(第 9 节)。

四个示例

下面每个示例都能突破一个看似正确的防护。我们在本地用 curl 8.7.1 和 PHP 8.5.7 复现了全部四个示例。

1. 检查你的 HTTP 客户端是否会展开 URL 模板

URL as configured:    http://{x}127.0.0.1/admin
Host a parser sees:   {x}127.0.0.1      (not an IP address, no DNS answer)
URL Laravel sends:    http://127.0.0.1/admin

如果防护把没有 DNS 应答的主机视为无害,就会放行这个 URL。随后,Laravel 的 HTTP 客户端把它交给 Guzzle 的 RFC 6570 展开器处理,{x} 没有值,展开为空,请求便发往回环地址。第 3 节解释这种展开,以及如何让展开器无事可做。

2. 以请求中主机的原样写法作为 DNS 固定条目的键

# The pin applies: cURL connects to 203.0.113.10 without a DNS lookup.
curl --resolve api.example.test:443:203.0.113.10 https://api.example.test/

# The fully qualified name is a different key, so cURL ignores the pin and resolves again.
curl --resolve api.example.test:443:203.0.113.10 https://api.example.test./

如果防护在构建固定条目的键之前去掉了末尾的点号,那么它检查的是一个 DNS 应答,连接却交给了另一个应答。第 2 节介绍地址固定。

3. 先解码,再脱敏

{ "echo": "sk\u002dlive\u002d4f9c" }

在这段消息体中按字节搜索已知密钥 sk-live-4f9c,什么也找不到,而 JSON 解码器返回的恰好就是这个密钥。第 5 节列出了已知值可能采用的其他写法。

4. 拒绝会沿路径向上跳转的路径值

Template as configured:  https://api.example.test/v1/customers/{id}/orders
Argument from the model: id = ..
URL after encoding:      https://api.example.test/v1/customers/../orders
Path cURL sends:         /v1/orders

rawurlencode('..') 返回 ..,而 libcurl 在发送请求之前会移除点号路径段,RFC 3986 §5.2.4 描述了这一处理,CURLOPT_PATH_AS_IS 的文档也有记载。受客户消息操纵的模型,可以带着工具自身的凭据,把调用移到运营方 API 中更上层的路径。第 3 节介绍这项拒绝规则。

1. 智能体连接器的威胁模型

连接器让 AI 智能体能够在平台之外采取行动。在 StaffOS 上,工作空间可以为其智能体提供两类外部工具:自行定义的 HTTP API 工具,以及由其他服务托管的远程工具端点。两者都在实时的客户对话中运行,因此威胁模型涵盖客户、模型、上游服务,以及配置该工具的每一个人。

一个 HTTP API 工具描述一个请求:请求方法、URL 模板、请求头、认证模式、可选的请求体,以及描述模型所提供参数的 JSON Schema。模板中可以包含模型提供的值(例如订单号),也可以包含平台填入的值(例如当前对话的电话号码,或工作空间已保存的密钥)。一个远程工具端点指定一个函数及其模式,平台把模型的参数转发到该端点的 URL。它不使用 Model Context Protocol。

Simon Willison 把私有数据、不可信内容和对外通信的组合称为AI 智能体的致命三要素。连接器把三者集中到了一起,本文中的每项控制措施都收窄其中一个要素:

要素 智能体掌握的内容 收窄该要素的控制措施
私有数据 客户详细信息,以及工具返回的任何内容 模型无法提供的平台值(第 8 节);工作空间范围与记录范围(第 6 节和第 8 节);对工具发送过的个人数据值进行脱敏(第 5 节)
不可信内容 客户消息和上游响应 标注为数据并限制大小的结果(第 7 节和第 9 节);整条呈现的记录(第 10 节)
对外通信 发送请求的工具,其中一些携带凭据 目标控制(第 2 节);请求构建(第 3 节);按用途限定作用范围的密钥(第 4 节);受所有者门槛约束的自动化(第 6 节和第 7 节)

参与方

参与方 可以信任它做的事 不能信任它做的事
客户 与智能体对话 选择工具把数据发往何处,或查看其他客户的记录
模型 从提供给它的工具目录中选择工具调用 提供由平台填入的值,或代替所有者做决定
上游 API 或远程端点 返回数据 保持响应小而及时、不在回显中带出凭据,或向模型下达指令
工作空间所选主机的 DNS 无 一致或及时地应答
没有管理员权限的工作空间成员 编辑普通工具 决定已保存密钥的去向,或开启没有人选择发起的调用
工作空间所有者或管理员 选择密钥、目标和自动化 访问回环、私有或链路本地地址,或读取平台凭据
其他工作空间 无 获知关于本工作空间的任何信息

资产

  • 平台凭据,例如平台与其消息合作方对接时使用的密钥。
  • 工作空间密钥,即工具发送给上游 API 的密钥。
  • 客户个人数据:电话号码、用户名、电子邮件地址、外部标识符,以及被标记为个人数据的自定义字段。
  • 内部网络:回环地址、RFC 1918 私有地址段、链路本地地址和云元数据服务。
  • 容量:工作进程的时间和内存、模型的上下文窗口,以及提供方对每个请求中工具数量的限制。
  • 智能体告诉客户的内容的准确性。
  • 所有者的决定:当所有者关闭某个工具,或选择已保存密钥的去向时,该决定必须得到维持。

九类故障

# 故障类别 边界 章节
1 出站目标控制 渲染后的请求到网络 2
2 请求构建 工作空间模板到渲染后的请求 3
3 密钥作用范围 配置到请求 4
4 错误与响应的遏制 响应和异常到模型与日志 5
5 按效果授权 设置请求到已存储的工具 6
6 自动调用 自动化到分发 7
7 模型、工作空间与平台的值 模型参数到请求 8
8 资源上限 网络到工作进程;工具目录到提供方 9
9 输出完整性 响应到模型的回答 10

2. 出站目标控制

在连接器中,服务器端请求伪造(SSRF)指的是工作空间配置的 URL 访问到了它不该访问的地址:回环地址、私有网络,或位于 169.254.169.254 的云元数据服务。只有当 URL 检查判断的主机、地址、代理和重定向策略与 HTTP 传输层将要使用的相同时,它才能阻止 SSRF,因此我们的检查针对的是传输层自身的输入。

严格解析器无法识别的主机写法

PHP 的 FILTER_VALIDATE_IP 会拒绝 2130706433、0x7f000001、0177.0.0.1 和 127.1,因此基于它构建的防护会把这些值当作主机名。DNS 对它们不返回任何结果,而 libcurl 会把这四者都当作 127.0.0.1 来连接。因此,放行 DNS 应答为空的域名的防护会失效放行。百分号编码写法和区域标识符写法也是如此:parse_url() 会原样返回 %31%32%37.0.0.1 和 [::1%25lo0],而 cURL 会把两者都规范化为回环地址。

我们的目标检查只在主机是普通 DNS 主机名或规范形式的 IP 地址时才接受它,并在分发时拒绝任何无法解析的主机名。它检查的是所有值(包括模型提供的值)都写入之后的 URL。地址检查以整块为单位,拒绝 IANA IPv4 和 IPv6 特殊用途地址注册表标记为不可全局访问的每一个地址段,以及组播地址。这涵盖回环地址、0.0.0.0/8、RFC 1918 私有地址段、运营商级 NAT 和覆盖网络使用的共享地址空间 100.64.0.0/10、包括云元数据地址在内的链路本地地址,以及文档和基准测试地址段。IPv6 仅限于全局单播地址段 2000::/3,并扣除其中的文档、6to4 和 Teredo 地址块。IPv4 映射地址、IPv4 兼容地址、6to4 地址和 Teredo 地址一律拒绝;位于知名 NAT64 前缀 64:ff9b::/96 中的地址,则按其携带的 IPv4 地址来判断。

该策略是一张明确的地址段表,因为 PHP 的地址段标志会放行非公网地址。在我们测试的每个 PHP 版本(8.1、8.2 和 8.5)上,FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE 都会接受 100.64.0.1、198.18.0.1、192.0.2.1、224.0.0.1、回环地址的 NAT64 形式 64:ff9b::7f00:1 和 6to4 形式 2002:7f00:1::1;在 8.1 和 8.2 上,它还会接受 ::ffff:127.0.0.1。它所接受的共享地址空间中有一个云元数据服务:阿里云的元数据服务在 100.100.100.200 上应答。同一张特殊用途地址表同时驱动该策略的单元测试、保存时检查和分发时检查。

检查的地址与连接的地址

如果防护先解析一次域名,再让 HTTP 客户端重新解析,那么它批准的是一个 DNS 应答,连接的却是另一个。用于 DNS 重绑定的区域,可以在检查时返回公网地址,在连接时返回私有地址,这就是检查时刻与使用时刻之间的间隙(CWE-367)。我们用 cURL 的 CURLOPT_RESOLVE 把批准的地址固定到连接上,使套接字连接到防护检查过的地址。

有两个细节决定固定能否生效:

  • 固定条目的键必须与请求的主机完全一致。 cURL 按主机字符串匹配 CURLOPT_RESOLVE 条目,因此为 api.example.test 设置的固定条目,不适用于对带末尾点号的完全限定名 api.example.test. 的请求。
  • 代理是第二个解析器。 通过代理时,cURL 发送 CONNECT host:port,由代理解析域名,于是固定条目不再决定请求的目标。

重定向目标是防护从未见过的第二个 URL。下面四个 HTTP 客户端默认都会跟随重定向,其中三个会从环境中读取代理设置:

HTTP 客户端 默认从环境读取代理 默认跟随重定向
PHP,Guzzle 7 是:HTTPS_PROXY 和 NO_PROXY,在命令行进程中还有 HTTP_PROXY 是,最多 5 次
Python,requests 是:http_proxy、https_proxy、no_proxy、all_proxy 及其大写形式 是,HEAD 以外的所有方法
Go,net/http 默认配置 是:HTTP_PROXY、HTTPS_PROXY 和 NO_PROXY 是,最多 10 次
Node.js,fetch 仅在设置 NODE_USE_ENV_PROXY=1 时,Node 24.0 和 22.21 起可用 是

因此,连接器请求在运行时关闭重定向、忽略来自环境的代理设置,并固定检查过的地址。在 Guzzle 中,这对应三个请求选项:

// $host is the request's host exactly as written, trailing dot included.
$options = [
    'allow_redirects' => false,
    'proxy' => '', // an empty proxy overrides HTTP_PROXY and HTTPS_PROXY
    'curl' => [CURLOPT_RESOLVE => ["{$host}:{$port}:{$checkedAddress}"]],
];

CURLOPT_PROXY 的文档说明,即使环境变量设置了代理,空的代理字符串也会禁用代理。OWASP SSRF Prevention Cheat Sheet 推荐同样的做法:验证目标,解析并检查其地址,并禁用重定向。

防护读取的主机就是 cURL 连接的主机

如果防护用一个解析器提取主机,而传输层使用另一个解析器,就可能批准一个主机,却连接到另一个主机。经典的攻击载荷会把真实主机藏在 \@、#@、?@、第二个 @ 或 userinfo 中经过编码的斜杠之后,例如 https://allowed.example.test#@127.0.0.1/。把这些载荷同时送入我们的防护和传输层,每一个要么被拒绝,要么连接到防护检查过的主机。DNS 应答中的每个地址都经过地址段检查,连接也被固定到这组检查过的地址上。

DNS 需要截止时间

PHP 的 dns_get_record() 不接受超时参数,因此,工作空间所选域名的权威服务器如果响应缓慢,就能随意长时间占用工作进程。在查询返回之后再计时,只能测量延迟,无法限制延迟。我们的连接器在一个独立进程中解析域名,该进程在工具时间预算以内的某个截止时间被终止;无法完成的查询与失败的查询一样被拒绝。

文档解析器是网络客户端

PhpWord 默认会加载图片;对于关系被标记为外部的 VML 图片,它会从文档指定的 URL 加载图片。因此,上传的 Word 文件可以让服务器获取它所选择的任意地址。我们的文档读取器提取文本,而提取文本从不需要图片,所以它不加载任何图片。

两类工具使用同一套防护

HTTP API 工具和远程端点代理应用相同的目标检查和相同的传输选项。该代理还只接受指明了主机的绝对 http 和 https URL。手动测试运行的审计记录保存的是工具的标识符,而不是其 URL。

3. 请求构建

通过检查的请求,在发送之前仍可能发生变化。有些 HTTP 客户端把每个 URL 都当作模板,请求头和 JSON 请求体各有各的语法,查询字符串解析器对参数名的解读也各不相同。我们在一次处理中按每个值所处的位置写入它,并拒绝任何会被后续阶段重新解读的内容。

会展开 URL 模板的 HTTP 客户端

Laravel 的 HTTP 客户端支持 URI 模板,其 send() 方法会让每个 URL 都经过 Guzzle 的 RFC 6570 展开器,无论调用方是否绑定了任何模板参数。按照 RFC 6570 §3.2.1,没有值的表达式展开为空:

Template as configured:  https://api.example.test/members/{member_id}
Sent when unfilled:      https://api.example.test/members/

第二个 URL 请求的是整个集合。{?member_id}、{/member_id} 和 {+member_id} 这类带运算符的表达式,同样会展开为空。保留字符展开({+var})和片段展开({#var})会让 /、?、# 和 @ 不经编码直接通过,因此一段花括号内容可以改变 URL 的路径、查询或主机。如第一个示例所示,没有值的表达式也能移动主机。只识别 {name} 的检查会放行以上所有情况。

Guzzle 的展开器会原样返回不含花括号的字符串。我们的渲染器依赖这一点。它自行写入每一个值,把没有被任何占位符使用的参数追加到 GET 或 DELETE 请求的查询字符串中;如果还残留任何 { 或 },就在发送任何内容之前终止调用。错误信息会说明是缺少某个已声明的参数、使用了未声明的参数,还是模板本身无效。

一个渲染器,一次处理

工具发送的每一个模板(URL、请求头、请求体和凭据),在每一条发送路径上都由同一个渲染器写入:模型自己的调用、第 7 节所述的自动预取,以及手动测试运行。它在同一次处理中写入模型的参数和平台的值,已写入的值不会再被读取。因此,已写入的值无法补全占位符,参数也无法把 {{secret:{key}}} 变成对模型所选密钥的引用。

按所处位置写入值

值所处的位置 写入方式 拒绝的情况
URL 百分号编码,包括已保存的密钥 未声明的占位符,即使调用传入了它;缺失或为空的值;构成 . 或 .. 路径段的值
请求头 原样写入 包含换行符、除制表符以外的控制字符或非 ASCII 文本的参数;未声明的占位符;缺失或为空的值
请求体,JSON 字符串内部 作为 JSON 字符串内容写入,只在 JSON 要求时转义 调用中缺少的已声明参数
请求体,值位置 写入模型发送的 JSON 值:数字、布尔值、null、数组或对象 调用中缺少的已声明参数

百分号编码不会处理 .,因此值仍可能在 URL 路径中构成点号路径段,而 libcurl 会在发送前移除它(第四个示例)。渲染器会跟踪路径中的哪些字符来自某个值,并拒绝由值构成的 . 或 .. 路径段,无论该值来自模型、平台还是已保存的密钥。运营方写入模板的点号路径段则按原样发送:那是运营方自己主机上运营方自己的路径。

对请求体中的值做百分号编码,会让上游按字面读取编码后的文本:Tan Ah Kow 到达时变成 Tan%20Ah%20Kow,按这个名字查询什么也找不到。把原始文本直接写入请求体,则会让引号添加新的键。请求体中的平台值必须位于 JSON 字符串内部,联系人姓名中的引号在那里会被转义;下文所述的可选值是唯一的例外。所有值写入之后,如果请求体不是有效的 JSON,或者把某个值放进了对象的键中,该请求体在工具保存时会被拒绝,在工具运行时还会再次被拒绝。JSON 无法表示的参数,例如模型给出的 1e999(解码后为无穷大),在每一个入口上都会导致调用被拒绝。在请求体中,模式没有声明的花括号按原样发送,因为 GraphQL 的选择集就是用花括号书写的。

含义相同的参数名

没有被任何占位符使用的模型参数会被追加到查询字符串中,因此渲染器必须防止这类参数与运营方在模板中固定的参数(例如租户过滤条件)发生冲突。PHP 的 parse_str() 无法完成这种比较:它会把参数名中的点号和空格转换为下划线,并把带方括号的名称解析为嵌套结构,因此 tenant.id 被读作 tenant_id,filter[tenant_id] 被读作 filter。我们用一个能合并所有解读方式的统一标识来比较参数名:在第一个 [ 处截断,把 . 和空格折叠为 _,并忽略大小写。如果某个模型参数的标识与渲染后 URL 中已有的某个参数相同,该模型参数会被丢弃,运营方的值保持不变。

保存时与运行时保持一致

工具的 URL 或所发送请求头中的每一个占位符,都必须在其参数模式中声明并标记为必填,工具才能保存;请求体中每一个已声明的占位符也必须是必填的。运行时,缺少其中任何一个的调用会在发送任何内容之前被拒绝。否则,可选占位符会从请求中消失,请求随之指向与运营方所描述的资源不同的另一个资源。

请求携带的请求头

一次调用发送哪些请求头,在解析任何值之前,根据工具的配置一次性决定:工具自身的请求头、认证模式的 Authorization、请求方法会发送请求体时的一个 Content-Type、一个 Accept,以及传输层自身的请求头(例如 Host)。决定调用能否发出的各项检查(缺失值、第 4 节的密钥限制、第 7 节的预取契约)读取的都是这同一个决定。如果应用共享的 HTTP 客户端增加了任何默认选项或中间件,就会有一项测试失败,因为租户工具会把它带到租户所选的主机上。

可选的平台值

有些查询接受多个标识符中的任意一个,例如电话号码或用户名。标记为可选的平台值只能位于请求体中的 JSON 值位置。存在时,它被写为 JSON 字符串;缺失时,写为 null;它出现在 URL、请求头、凭据、引号内部,或作为对象键时,都会被拒绝。密钥永远不能是可选的。当所有可选值都缺失,且没有其他对话、联系人或自定义字段的值进入请求时,调用会被拒绝,因此查询绝不会在不带任何客户标识信息的情况下发出。

4. 密钥作用范围

只有当两个问题的答案相同时,已保存的密钥才是安全的:谁可以管理它,以及谁可以决定把它发往何处。StaffOS 把每个工作空间已保存的密钥放在该工作空间自己的加密存储中,只从工具所属的工作空间解析密钥引用,并把每一项可能改变已保存密钥去向的修改,限定给获准管理该密钥的人。

一个限定范围的存储

工具中的密钥引用只从该工具所属工作空间的存储中解析,不从任何其他地方解析。如果引用的密钥未在该工作空间中保存,调用会被终止,不发送任何内容。

管理密钥与使用密钥是两项不同的权限

限制谁能创建密钥,仍会留下第二条路径。无法读取某个已保存密钥的成员,可以编辑发送该密钥的工具,把工具指向自己控制的主机,然后运行一次测试。这就是 Norm Hardy 所说的混淆代理人:权限由平台持有,而本不应行使该权限的一方却能选择如何使用它。

对于会发送已保存密钥的工具,只有工作空间所有者或管理员可以:

  • 更改其请求方法、URL、请求头、认证设置或请求体;
  • 在设置页面中手动运行它;
  • 在它被禁用后重新启用它。

我们只对较低角色无法通过其他途径实现的操作设置门槛。不发送已保存密钥的工具,其手动测试运行对所有角色开放,因为成员本来就能编辑或复制此类工具,为测试设门槛起不到任何保护作用。

为什么一次处理对密钥很重要

请求模板可以混合已保存的密钥、对话值、联系人值和模型参数。分多次处理来解析它们,会让一个值注入另一个值。如果先替换密钥,再在结果中扫描对话占位符,那么文本中恰好含有对话占位符的密钥,就会把客户的电话号码带进请求。把顺序反过来,同样的缺陷会转移到含有密钥形式文本的对话值上。第 3 节中的单次处理只读取运营方的源文本,因此任何被替换进去的值都不会被当作占位符解析。

拒绝无法解析的内容

不匹配任何匹配规则的引用(例如拼写错误或未闭合的占位符),不得作为字面文本出现在请求头、请求体或凭据中。宽松的外层匹配规则找出每一段候选内容,严格的内层匹配规则对其进行验证,任何未解析的片段都会导致调用被拒绝。

凭据同样采用失效拒绝。解析后得不到可用内容的密钥、bearer 令牌中未保存的密钥,或未知的认证模式,都会在发送前终止调用。否则就会发出未经认证的请求,而接受匿名写入的上游会执行其副作用。

平台无法解密的凭据

用平台已不再持有的加密密钥加密的凭据无法读取,也没有任何办法检查它会发送什么。凭据无法解密的工具会从智能体的工具目录中移除;无法解密的已保存密钥解析为空,从而导致调用被拒绝。就所有者或管理员限制而言,这样的工具视为会发送已保存密钥。普通的保存操作会保留已存储的密文,因此恢复加密密钥后仍能读取它;只有有人输入的新值,或明确选择删除它,才会替换它。在读取结果以占位符代替已存储凭据的地方,写入时会拒绝把该占位符作为值,因此“读取、修改、写入”的流程不会用掩码替换真实凭据。

为某一用途存储的凭据

为某一用途保存的凭据,不得被选用于其他用途。远程端点代理可以使用工作空间已存储的凭据进行认证;如果认证设置指定的是该工作空间的某个 AI 模型提供方,代理会在读取任何凭据之前拒绝该设置。任何工具配置都无法把已存储的模型密钥发送到其配置者选择的主机。

日志中的密钥

有些 OAuth 授权码交换会把客户端密钥放在查询字符串中。Meta 的文档把其 WhatsApp Embedded Signup 令牌交换描述为对 /oauth/access_token 的 GET 请求,查询参数中包含 client_secret;而 HTTP 客户端通常会在传输错误中包含完整的 URL。PHP 的异常字符串还包含异常链中每个前序异常的消息,而错误日志和失败任务记录保存的正是这个字符串。我们的 Meta API 调用中,凡是在查询字符串中携带凭据的,遇到传输失败时都会重新抛出一个异常,其中去掉了所有查询字符串,也不附带原始异常。

5. 错误与响应的遏制

错误消息和响应体,是凭据离开工具调用的两条途径。对于传输失败,我们返回一个分类和一句固定语句,并丢弃库生成的文本。对于两类工具中任一工具的响应,我们按 JSON 允许的每一种写法,对它发送过的凭据进行脱敏;HTTP API 工具还会对它发送过的个人数据值进行脱敏。

替换错误文本,而不是清洗它

HTTP 库会引用导致错误的输入。libcurl 会在 Could not resolve host 这类消息中重复主机名,因此放在主机名中的凭据,无论 URL 被怎样清洗都会保留下来。Guzzle 的请求头验证会抛出一个引用无效请求头值的异常。清洗器必须预料到凭据可能出现的每一个位置(任意名称和大小写的查询参数、重复的键和带方括号的键、userinfo、路径段、主机名、数字),以及每一个可能引用它的库。

我们不做清洗。在传输中失败的请求,会返回一小组分类中的一个,以及一句不含任何源自请求或异常的内容的固定语句。唯一读取该异常的代码判断两件事:失败是否为超时(有 cURL 错误号时依据该错误号判断),以及响应是否过大。

静态强制执行遏制

一项架构测试会解析每一个发送 HTTP 请求的工具,并要求每一个触及网络的调用都位于某个 try 块的主体之内,且该块的处理程序会捕获并遏制连接失败。它基于语法树工作,能解析导入的名称和别名,追踪通过依赖注入传入的 HTTP 客户端,并把任何它无法读懂的代码形态视为违规。新增的工具只要含有未受保护的请求,就会让测试套件失败。

处理程序必须捕获正确的类。在 Laravel 中,ConnectionException 和 RequestException 是 HttpClientException 之下的同级类,因此针对 RequestException 的处理程序永远捕获不到 DNS 失败或超时。

按每一种写法对已知值脱敏

当响应必须传给模型时,问题就从“什么可能是凭据?”变成了“这个已知凭据出现在哪里?”。HTTP API 工具会提供它视为凭据的值:已保存的密钥、其认证设置中的凭据,以及名称表明其为凭据的请求头(例如 Authorization)的值。它还会加入它发送过的客户个人数据值(见下文),包括它发送这些值时所用的每一种写法,其中也包括请求体中的 JSON 写法。远程端点代理提供自己的一组值:它已存储的凭据、以凭据命名的请求头的值、Cookie 请求头及其中较长的 cookie 值,以及其 URL 中可能携带密钥的部分;每一项都同时包括原样写法和解码后的写法。两类工具都把各自的一组值交给同一个脱敏器,由它在响应可能使用的每一种写法中查找每一个值。

写法 为什么字节匹配会漏掉它 处理方式
JSON 转义 RFC 8259 §7 允许转义任何字符,因此 sk\u002dlive 并不包含 sk-live 解码消息体,对解码后的值脱敏,再重新编码
大整数 除非设置了 JSON_BIGINT_AS_STRING,否则 PHP 会把大于 PHP_INT_MAX 的整数解码为浮点数,丢失匹配时要比较的数字位 把大整数解码为字符串
浮点数 PHP 把浮点数转换为字符串时,默认打印 14 位有效数字,因此 1234567890123456 会变成 1.2345678901235E+15 按规范值比较数字
对象键 上游可能把凭据作为键回显 键和值都要检查
Basic 认证 该请求头携带的是 user:password 的 base64 编码,而解析过它的上游会返回明文密码 把编码后的令牌和解码后的密码都视为已知值
相互重叠的值 使用数组参数的 str_replace() 可能改写之前某次替换插入的文本,从而破坏标记 使用 strtr(),它优先尝试最长的键,且从不重新扫描已替换的文本
深层嵌套 嵌套层级超出解码器深度上限的 JSON 消息体无法解码,而退回字节匹配会重新打开转义绕过的漏洞 返回一句固定语句

脱敏不能损坏数据。对数字做子串匹配,会替换掉不相关的值,例如计数 10;而只有一个字符的凭据可能改写超时分类器读取的文本。数字按精确值匹配,超时则依据 cURL 的错误号分类。

工具结果中的个人数据

当 HTTP API 工具把客户的电子邮件地址、外部标识符或被标记为个人数据的自定义字段发送给上游时,这些值会在响应进入模型上下文和已存储的追踪记录之前,从响应中脱敏。客户的姓名和记录标识符则不做处理,因为结果会出于正常原因重复它们,而对一个较短的值脱敏,会破坏它出现的每一处。

6. 按效果授权

附加在表单字段上的权限,可能覆盖不到该字段产生的效果。我们列出一个开关的安全性所依赖的内容(目标、载荷、请求方法),在开关开启期间限制对这些输入的修改,对开启该对象设置门槛,也对按需使用设置门槛。我们验证的是保存后将保留的那一行数据,它可能与收到的请求不同。

对效果设置门槛

第 7 节所述的预取设置,会把一个工具变成没有人选择发起的调用。对于 POST 工具,预取还需要一项“该端点只做读取”的确认。只有工作空间所有者或管理员可以开启这两项设置中的任何一项,而任何人都可以关闭它们。只对这些设置设门槛会留下缺口,因为成员可以不动这些设置,转而在其之下改变工具的目标。当工具处于自动状态时,所有者或管理员限制也覆盖它的请求字段。重新启用一个已禁用的自动工具,出于同样的原因也设有门槛:它已存储的预取设置会在启用的那一刻恢复自动调用。

记录可见性比租户归属更窄

手动测试运行可以从真实对话中取值。检查该对话属于同一工作空间,证明的是租户归属;而收件箱只向每个用户显示他可以查看的对话。测试端点通过与收件箱相同的可见性范围来解析对话,因此用户无法通过指定同事的隐藏对话来借用其客户的详细信息。

验证保存后将保留的数据行

省略某个字段的更新会保留该字段的已存储值。因此,只读取请求的验证,判断的是一行永远不会存在的数据。对于缺失或为空字符串的属性,Laravel 不会运行任何规则(包括自定义规则),除非该规则是隐式规则。设想一条跨字段规则,只允许管理员改变发送密钥的工具的目标。如果这条规则读取的是请求,那么一次发送新 URL、省略凭据的稀疏更新,就会把已存储的凭据转移到新主机上。

在验证运行之前,我们的更新路径会恢复跨字段规则读取的或附加在其上的每一个已存储字段,使这些规则判断的是即将保存的那一行数据。保存工具的每一条路径都通过同一套规则进行验证。

7. 自动调用

上下文预取会在模型的轮次开始前调用选定的工具,使智能体一开始就掌握它原本需要查询或猜测的事实。模型的工具策略允许部分实时写入,因为这类调用是模型根据上下文选择的。预取在每一条入站消息上都会运行,任务重试时还会再运行一次,因此写入操作会在没有任何人决定的情况下重复执行。所以,预取有自己的资格规则,在工具保存时应用,并在每次调用前再次应用。

只有同时满足以下所有条件时,工具才有资格被预取:

  • 它是 GET,或是工作空间所有者或管理员已确认只做读取的 POST。
  • 它的参数模式是封闭的:没有属性,没有模式属性,没有必填项,且 additionalProperties 设为 false。
  • 它的 URL、所发送的请求头和请求体中都不出现参数占位符。

经确认只读的 POST 这一选项,是为接收个人数据的读取 API 而设的。如果改成 GET,客户的电话号码就会出现在 URL 中,而网关和访问日志会把它保留下来。

只读确认会让 POST 具备资格,但不会降低它的风险级别,因此平台的其他门槛仍然适用。在未启用外部写入的沙盒中、在评测运行中,以及在该对话有未关闭的支持工单期间,这类调用都会被拒绝。

预取基于该轮次的一份快照工作。它的候选项是模型在该轮次收到的工具目录中各工具数据行的副本。每个候选项都会针对工作空间进行检查,并通过与模型调用相同的门槛,因此轮次中途所做的编辑,无法把目录中的 GET 变成 POST。每个轮次都有工具数量上限、单次调用和整个步骤的截止时间,以及上下文的字节预算;预算用完后,不再发起任何调用。

结果放在分隔符内传给模型,并标注为数据。关键的控制措施并不依赖模型遵守这一标注:该轮次的工具目录、模型可以填写的槽位,以及平台提供的每一个值,都由模型无法编辑的配置固定下来。

8. 模型、工作空间与平台的值

模型选择参数。平台填入当前客户的电话号码等值。这两种来源绝不能重叠,否则遭到提示注入的模型就能选择查询使用谁的数据。

  • 模式冲突。 如果工具模式中某个参数与平台填入的对话字段或联系人字段同名,该模式在保存时会被拒绝,在为某一轮次构建工具时还会再次被拒绝。已保存的密钥从不从参数中读取,因此任何模式都无法代替已保存的密钥。
  • 工作空间范围。 联系人的值,以及对话渠道身份上的电话号码或用户名,只有在属于该对话所在的工作空间时才会被读取。
  • 身份值带有其所属平台。 用户名只在其所属的消息应用内唯一,因此只有当身份所在平台上的用户名能够识别发送者时,工具才会收到该对话的用户名。当发送者放弃某个用户名时,身份会清除它,因为之后可能有其他人认领这个用户名。延迟处理的较早消息无法恢复它:身份保留的是其最新消息中的用户名,这项比较在一次数据库更新中完成。
  • 保留名称。 如果工作空间工具或远程端点的名称与平台工具相同,它会遮蔽该平台工具,或者自身被丢弃。平台在客户对话中可以提供的每一个工具名称都被保留,如果某个新的平台工具不在该列表中,清点测试就会失败。名称匹配规则以 \z 结尾,因为在 PCRE 中,$ 也会匹配末尾换行符之前的位置。

提供方的模式规则是契约的一部分

模型提供方的模式规则,决定了请求究竟会不会被接受。空的 JSON 对象会被解码为空的 PHP 数组,带数字键的对象会被解码为列表。重新编码后,这样的模式在送达提供方时,要求为对象的地方变成了数组。强制检查类型的提供方(例如 Gemini 的函数声明)会拒绝整个请求,包含该工具的每一个对话轮次都会失败。我们用一个遍历器来验证远程端点的模式,它知道每个 JSON Schema 关键字的值应当包含什么。

9. 资源上限

每一种资源都需要在其被消耗的地方设置上限。消息体到达之后才做的大小检查、没有截止时间的 DNS 查询,以及为零的超时,都各自允许无上限的工作。

资源 故障模式 控制措施
响应字节 在客户端返回之后才做大小检查,传输量不受限制:Guzzle 的 cURL 处理器已经把整个消息体写入了临时流 响应接收器在字节到达时就拒绝超过固定上限的写入;cURL 自身的大小限制保留为第二道上限
时间 在 Guzzle 中,超时为 0 表示无限期等待,而这正是默认值 工具的超时必须在 1 至 120 秒之间,低于一秒的超时会在运行时被拒绝
DNS dns_get_record() 没有超时 在一个进程中解析,该进程在调用预算以内的某个截止时间被终止
工具目录 每一个已启用的工具,在每一轮次都会被描述给每一个面向客户的智能体,而提供方会限制每个请求中的函数声明数量 每轮次的工具目录中最多 32 个工作空间 API 工具,每个模式最多 16,384 个字符

工具目录上限是参照已公开的最保守的提供方限制来确定的。至少到 2025 年 4 月为止,OpenAI 的 API 参考文档都写明 Chat Completions 工具最多支持 128 个函数;Google 的函数调用参考文档给出的上限是每个请求 128 个函数声明。在包含 32 个工作空间工具时,StaffOS 最大的智能体的完整工具目录仍低于 128。

10. 输出完整性

保密性控制措施并不能覆盖工具可能造成的每一种危害。当 API 返回一组带有并列字段的记录时,模型可能把一条记录的名称与另一条记录的标签配对,从而告诉客户错误的信息。我们的响应渲染可以在模型看到记录之前,把每条记录合并成一行文本,使一条记录的各个字段在到达模型时已经连在一起。

单行渲染在失败时回退到原始数据。缺少被引用字段的记录会被完整保留,无法解析的模板则让载荷保持原样。渲染拒绝把配置路径上的对象改写为列表,路径和模板要么一起保存,要么都不保存。

11. 我们如何构建和验证安全控制措施

对这些边界的修改,按以下方式构建和检查。

  • 对抗性评审。 一个被指示去攻破该修改的自动评审智能体,会评审每一个修订版本,并通常为每项发现附上复现方法。作者在修改代码之前,会先复现每一项论断。
  • 经过变异检验的防护。 每个新的防护都会在一份一次性代码副本中被移除或禁用,只有当测试随之失败时,这项修改才算数。如果某个测试在没有其防护的情况下依然通过,就重写它,直到它无法通过为止。这一方法见于 DeMillo、Lipton 和 Sayward 1978 年关于测试数据选择的论文。
  • 用契约表代替示例。 每项契约都是一张输入类别表,每一行都断言实际发送的精确字节,或断言没有发送任何内容:
    • 特殊用途地址,由地址策略的单元测试与保存时、分发时检查共用同一张表;
    • URL 形态,经由模型调用和预取运行;
    • 请求体和请求头中的位置,经由模型调用和每一条手动测试路径运行;
    • 连接器从不发送的请求头,与一旦被渲染就会导致调用被拒绝的文本交叉组合;
    • 凭据遏制,把两类工具与凭据可能出现的每一个位置、每一种失败类型交叉组合;
    • 保存操作,覆盖保存工具的每一条路径、其初始状态和输入形态。
  • 每条规则只有一个实现。 在保存时和运行时都要检查的规则,是同一个函数被调用两次。同一谓词的两种推导方式会逐渐偏离。
  • 对无法读取的内容失效拒绝。 未知的认证模式、无法解密的凭据、无法解析的主机、嵌套层级超出解码器上限的 JSON 响应体,以及静态防护无法分析的代码形态,全部会导致拒绝。

12. 与公开分类体系的对应关系

这一对应关系由我们制定,用于帮助定位。它避免使用 CWE-200 和 CWE-269,因为 MITRE 不建议将二者用于漏洞映射。

故障类别 CWE OWASP Top 10 for LLM Applications 2025 OWASP API Security Top 10 2023
出站目标控制 CWE-918 Server-Side Request Forgery;CWE-367 Time-of-check Time-of-use Race Condition,对应 DNS 重绑定 API7:2023 Server Side Request Forgery
请求构建 CWE-180 Incorrect Behavior Order: Validate Before Canonicalize;CWE-116 Improper Encoding or Escaping of Output;CWE-235 Improper Handling of Extra Parameters LLM05:2025 Improper Output Handling
密钥作用范围 CWE-441 Unintended Proxy or Intermediary ("Confused Deputy");CWE-863 Incorrect Authorization;CWE-532 Insertion of Sensitive Information into Log File LLM02:2025 Sensitive Information Disclosure
遏制 CWE-209 Generation of Error Message Containing Sensitive Information;CWE-532 LLM02:2025 API10:2023 Unsafe Consumption of APIs
按效果授权 CWE-863;CWE-639 Authorization Bypass Through User-Controlled Key,对应记录可见性 API1:2023 Broken Object Level Authorization;API3:2023 Broken Object Property Level Authorization;API5:2023 Broken Function Level Authorization
自动调用 CWE-863;CWE-367,对应在轮次中途重新读取配置 LLM06:2025 Excessive Agency
模型与平台的值 CWE-639,对应模型原本可能选择的查询键;CWE-441;CWE-116;CWE-625 Permissive Regular Expression LLM01:2025 Prompt Injection;LLM05:2025 API1:2023 Broken Object Level Authorization
资源上限 CWE-770 Allocation of Resources Without Limits or Throttling LLM10:2025 Unbounded Consumption API4:2023 Unrestricted Resource Consumption
输出完整性 LLM09:2025 Misinformation

对于基于 MCP 构建的团队,Model Context Protocol 的安全最佳实践在该协议自身的场景中讨论了相关攻击,包括混淆代理人攻击、令牌透传和 SSRF。

附录:实现说明

供构建相同控制措施的团队参考的细节。

测试运行与输入规范化

Laravel 在其默认中间件中会去除字符串输入的首尾空白,并把空字符串转换为 null。手动测试路由对参数值豁免这一处理,因此测试运行渲染出的字节,与模型调用会渲染出的字节完全相同。

闪存的表单输入

当设置表单验证失败时,闪存回会话的输入会略去两类工具的凭据字段:HTTP API 工具的 URL、请求头、认证设置和请求体,已保存密钥的值,访问令牌,以及列表中每个远程端点的 URL、请求头和认证设置。这些字段在任何闪存输入的代码执行之后才被移除,因此以名称为键的列表、自行闪存输入的控制器,也同样被覆盖。

版本令牌

持有凭据的工具数据行带有一个用于乐观锁的版本令牌。如果对数据行做未加盐的摘要,任何持有该令牌的人都能离线验证对该行凭据的猜测,因此令牌由数据行连同一个服务器端密钥共同计算得出。计算时还使用了 JSON_THROW_ON_ERROR:遇到无效 UTF-8 时,json_encode() 会返回 false,如果不加这个标志,所有这样的数据行都会共用同一个令牌,并通过每一次过期检查。

用一种方式度量模式

模式的长度上限在保存时和运行时用同一种方式度量。不带 JSON_UNESCAPED_UNICODE 的 JSON 编码会把每个非 ASCII 字符存为一个六字符的 \uXXXX 转义序列,因此按存储文本度量,一个中文模式大约会被计算六倍;按提交的文本度量,则会把设置表单中的缩进也计算在内。两项检查共用一个函数:它度量紧凑写法下的模式,每个字符都按其本身计数。

替换列表

提供远程端点列表的写入操作,会整体替换已存储的列表。array_replace_recursive() 按索引合并列表,因此合并会保留所有者已删除的条目,连同它们已存储的凭据。

304 响应声明的长度

RFC 9110 §8.6 允许 304 响应携带 200 响应本应具有的 Content-Length,尽管 304 响应不包含消息体。响应大小检查度量实际收到的消息体,从不读取声明的长度。

作者与引用

Vin Lim 是 StaffOS 的首席技术官,负责智能体编排、业务系统集成,以及 AI 智能体所调用工具的安全。

建议引用格式: Lim, V. (2026). Security engineering for AI agent connectors: nine failure classes. StaffOS technical paper, version 1.0, September 24. https://staffos.xyz/blog/ai-agent-connector-security

下载 BibTeX 引用

关于我们如何构建和评测使用这些工具的智能体,请参阅我们关于智能体运行框架工程的论文。

Frequently asked questions

为什么 AI 智能体的工具调用存在安全风险? +

调用外部 API 的智能体可以读取私有数据,会读取未经任何人审核的内容,还可以向外发送数据。Simon Willison 把这种组合称为“致命三要素”。除非模型外围的软件控制请求目标、请求内容和响应,否则一个被操纵的提示词或一个配置错误的工具,就可能把数据送到不该去的地方。

客户自行配置 API URL 时,如何防止 SSRF? +

只接受普通主机名和规范形式的 IP 地址。用一张明确的地址段表,拒绝 IANA 特殊用途地址注册表标记为不可全局访问的每一个地址段,包括回环、私有、共享和链路本地地址空间,以及云元数据地址。在截止时间内完成域名解析,并把检查过的地址固定到连接上,使第二次 DNS 应答无法改变它。关闭重定向,以及从环境继承的代理设置。

为什么在发送前检查 URL 还不够? +

防护检查的 URL 可能与实际发送的 URL 不同。有些 HTTP 客户端会在应用构建好 URL 之后,把每个 URL 都当作 RFC 6570 模板展开;cURL 会连接到严格解析器不认作地址的主机写法;代理也会自行解析域名。只有当检查判断的输入与传输层将要使用的输入相同时,检查才能生效。

模型参数应如何写入 JSON 请求体? +

以 JSON 形式写入,并按每个参数所处的位置来写。在 JSON 字符串内部,把参数作为字符串内容写入,只在 JSON 要求时转义。在值位置,写入模型发送的 JSON 值。百分号编码会让上游按字面读取编码后的文本,直接粘贴原始文本则会让引号添加新的键。所有值写入之后,如果请求体不是有效的 JSON,就拒绝它。

模型参数能改变工具所调用的 API 端点吗? +

不应该能。对写入 URL 的每个参数做百分号编码,拒绝任何残留的模板语法,并拒绝构成 . 或 .. 路径段的值。libcurl 在发送请求之前会移除点号路径段,因此对于 /customers/{id}/orders 这样的模板,如果 id 为 ..,请求就会带着该工具的凭据到达运营方主机上的 /orders。

AI 智能体平台应如何限定 API 密钥的作用范围? +

把每个工作空间已保存的密钥放在它自己的加密存储中,并且只从该存储解析引用。然后按用途限定已保存密钥的作用范围:只有获准管理某个密钥的人,才能更改携带该密钥的请求的去向、测试该请求,或开启会发送该密钥的自动调用。

如何让凭据不出现在模型看到的内容中? +

用一个分类和一句固定语句替换传输层错误文本,因为 HTTP 库的错误信息可能引用 URL、主机名和请求头的值。对于工具的响应,按 JSON 允许的每一种写法,对该工具发送过的凭据进行脱敏;嵌套过深、无法解码的 JSON 响应体则不予返回。

模型能选择工具查询哪位客户吗? +

只能通过工具模式所声明的参数来选择。对话的电话号码和联系人的详细信息由平台填入;如果模式声明了一个与其中某个名称同名的参数,该工具在保存时会被拒绝,在为某一轮次构建时还会再次被拒绝。已保存的密钥从来不会从参数中读取。

References

  1. [1] Willison, S. The lethal trifecta for AI agents: private data, untrusted content, and external communication. 16 June 2025.
  2. [2] OWASP Cheat Sheet Series. Server-Side Request Forgery Prevention Cheat Sheet.
  3. [3] IANA. IPv4 Special-Purpose Address Registry.
  4. [4] IANA. IPv6 Special-Purpose Address Registry.
  5. [5] Alibaba Cloud documentation. View instance metadata (metadata service at 100.100.100.200).
  6. [6] OWASP. Top 10 for LLM Applications 2025.
  7. [7] OWASP. API Security Top 10 2023.
  8. [8] Model Context Protocol. Security Best Practices.
  9. [9] IETF RFC 6570. URI Template. Sections 3.2.1, 3.2.3 and 3.2.4.
  10. [10] IETF RFC 3986. Uniform Resource Identifier: Generic Syntax. Section 5.2.4, Remove Dot Segments.
  11. [11] IETF RFC 8259. The JSON Data Interchange Format. Section 7.
  12. [12] IETF RFC 9110. HTTP Semantics. Section 8.6, Content-Length.
  13. [13] Laravel framework source. PendingRequest::send() expands every URL through UriTemplate::expand().
  14. [14] Laravel documentation. HTTP Client, URI Templates.
  15. [15] Laravel documentation. Requests, Input Trimming and Normalization.
  16. [16] Laravel documentation. Validation, Implicit Rules.
  17. [17] Guzzle uri-template source. UriTemplate::expand().
  18. [18] Guzzle source. Client reads HTTP_PROXY, HTTPS_PROXY and NO_PROXY from the environment.
  19. [19] Guzzle documentation. Request options: allow_redirects and timeout.
  20. [20] Requests documentation. Proxies.
  21. [21] Requests documentation. Redirection and History.
  22. [22] Go standard library. net/http: DefaultTransport, ProxyFromEnvironment and Client.CheckRedirect.
  23. [23] Node.js documentation. NODE_USE_ENV_PROXY.
  24. [24] MDN Web Docs. RequestInit: redirect.
  25. [25] curl documentation. CURLOPT_RESOLVE.
  26. [26] curl documentation. CURLOPT_PATH_AS_IS.
  27. [27] curl documentation. CURLOPT_PROXY.
  28. [28] PHPWord source. Word2007 reader loads an external VML image from the URL the document names.
  29. [29] PHP manual. parse_str.
  30. [30] PHP manual. strtr.
  31. [31] PHP manual. PCRE anchors.
  32. [32] Meta for Developers. Onboarding business customers as a Tech Provider (Embedded Signup token exchange).
  33. [33] OpenAI openai-openapi specification, commit 498c71d. Chat Completions tools: "A max of 128 functions are supported."
  34. [34] Google Cloud. Function calling reference, limitations.
  35. [35] Hardy, N. The Confused Deputy (or why capabilities might have been invented). ACM SIGOPS Operating Systems Review 22(4), 1988.
  36. [36] DeMillo, R. A., Lipton, R. J., Sayward, F. G. Hints on Test Data Selection: Help for the Practicing Programmer. IEEE Computer 11(4), 1978.
  37. [37] MITRE. CWE-918: Server-Side Request Forgery (SSRF).
  38. [38] MITRE. CWE-639: Authorization Bypass Through User-Controlled Key.

About the author

Vin Lim

联合创始人 / 首席技术官,StaffOS

Vin Lim 是 StaffOS 的首席技术官。他的工作涵盖智能体编排、业务系统集成,以及决定 AI 智能体能访问什么、能发送什么、能透露什么的控制措施。

Related reading