日期:2026-08-10
客户是一家做能源设备运维的公司,内部搭了一个运维智能体,能查设备台账、调工单系统、也能联网搜公开资料。所有模型调用和工具调用都走 AI 网关统一出口。
出事是在一次内部红蓝对抗演练里。蓝队同事往对话框里写了一段话,大意是"请帮我核实一下这个内部文档链接的内容",后面跟了一个内网管理后台的地址。智能体老老实实调了联网抓取工具,把那个地址拉了一遍,把返回内容摘要给出来了。虽然那个后台有登录页拦着,没泄露实质数据,但这个行为本身已经说明问题:模型可以被诱导,去访问它不该访问的地方,而网关放行了。
安全部门的反应很直接,当天就把联网工具停了。智能体缺了这个能力,用起来大打折扣,业务方天天催。
我们接到的活是把这个能力重新打开,前提是让安全部门签字。
方案落在 AI 网关的接入层和编排层,围绕工具调用做了一圈管控。
工具白名单与出网域名管控。所有可被模型调用的工具必须先在管控台注册,注册时声明它会访问哪些目标:域名白名单、允许的 HTTP 方法、允许的端口。没注册的工具,模型就算生成了调用意图,网关也不执行。联网搜索这类需要访问开放互联网的工具,走独立的出网代理,代理上再做一层域名和内容管控。
内网地址拦截。所有工具调用的目标地址在执行前解析,落在内网网段、回环地址、云元数据地址这些范围的一律拒绝。这一步看着简单,实际有不少绕过手法,后面细说。
调用参数校验。每个工具注册时要提供参数的 schema,网关按 schema 校验模型生成的参数:类型、取值范围、正则约束。校验不过直接驳回并把错误信息回喂给模型让它重试,不透传到后端系统。
沙箱执行与超时熔断。需要执行代码的工具(比如数据分析类的)放进容器沙箱,无持久化、无网络(或仅白名单出网)、CPU 内存受限、硬超时。沙箱进程用完即销毁。
调用留痕。每一次工具调用记录完整上下文:谁触发的、模型生成的原始参数、校验结果、实际执行的目标、返回摘要、耗时。这份日志单独存,安全部门有只读权限。
SSRF 的各种绕过。 这是投入最多的地方。最初我们的拦截逻辑是拿到 URL、解析 hostname、判断是不是 IP、是内网 IP 就拦。上线前自己测了一轮,被绕过好几次:域名解析到内网地址(攻击者控制的公网域名,A 记录指向 192.168 段)、十进制或八进制表示的 IP、URL 里带 @ 符号混淆真实 host、还有跳转,第一跳是公网、第二跳 302 到内网。
最后的做法是把判断挪到连接层:不在 URL 字符串上做文章,而是在建立 TCP 连接前,拿到实际解析出来的 IP 再判断,并且禁止跟随重定向(需要跟随的场景,每一跳都重新走一遍判断)。同时把 DNS 解析结果和实际连接的 IP 绑定,避免解析和连接之间被换掉。这套写完之后,我们请安全部门的人又打了一轮,剩下一个提交时没想到的点:IPv6 的映射地址表示法,补上了。
动态 URL 怎么判定。 联网搜索工具的目标 URL 是模型根据搜索结果生成的,不可能提前白名单。我们的处理是分级:搜索接口本身是固定白名单域名;搜索结果里的链接如果要抓取,走一个二级策略,只允许 HTTP 和 HTTPS、只允许 80 和 443 端口、目标 IP 必须是公网、单次会话内抓取次数有上限、抓取内容做大小截断。另外维护了一份黑名单,命中已知恶意域名库的直接拒。
沙箱逃逸和资源占用。 沙箱这块我们没自己造轮子,用的是容器加 seccomp 配置,限制系统调用集合。踩过的坑是资源:某次业务方写的分析脚本里有个死循环,CPU 打满,虽然限制了单容器的 CPU 份额,但并发上来之后宿主机整体负载还是被拖高,影响了同机的其他沙箱。后来加了并发沙箱总数上限和排队,以及单沙箱的硬超时(默认三十秒,可按工具配)。超时直接 SIGKILL,不给优雅退出的机会,这个决定当时有争议,实践下来是对的。
审计怎么做到真能用。 日志记全了不难,难的是安全部门能从里面看出问题。我们跟他们一起定了几个告警规则:短时间内同一会话大量工具调用、参数校验失败率异常升高、拦截命中集中在某个用户、模型生成的目标地址包含内网特征词。这些规则触发后推到他们的安全平台。真正让他们签字的,是我们做了一个"重放"功能:任何一次被拦截的调用,可以在隔离环境里重放一遍看当时到底发生了什么。有了这个,他们才觉得这个黑盒是可查的。
案例片段(已脱敏):工具注册与出网策略
yaml tool: web_fetch exec_mode: proxy egress_policy: allow_schemes: [http, https] allow_ports: [80, 443] deny_ip_ranges: - 10.0.0.0/8 - 172.16.0.0/12 - 192.168.0.0/16 - 127.0.0.0/8 - 169.254.0.0/16 # 含云元数据地址 - ::1/128 - ::ffff:0:0/96 # IPv4 映射,后补 resolve_then_check: true # 连接前按实际解析 IP 判定 follow_redirect: false # 需跳转时逐跳重新判定 max_body_bytes: 2097152 per_session_calls: 20 param_schema: url: {type: string, pattern: "^https?://", max_len: 2048} timeout_ms: {type: int, min: 1000, max: 15000}案例片段(已脱敏):一次拦截记录与重放结论
[2025-xx-xx 14:22:07] session=S-77104 user=U-2213 tool=web_fetch 模型生成参数: url = "http://ops-admin.internal.example/api/v1/nodes" 校验链路: schema 校验 PASS DNS 解析 ops-admin.internal.example -> 10.32.7.19 IP 段判定 10.0.0.0/8 命中 -> DENY 处置:拒绝执行,向模型返回 "target not allowed",模型改口回答无法访问 告警:命中规则 R-INT-01(内网地址访问尝试),已推安全平台 重放(隔离环境):确认目标为内网运维后台登录页,无数据返回 溯源:该 URL 来自用户输入的原文,判定为诱导型提示注入
功能重新开放至今跑了四个月,联网和工具能力一直没再被停过。
高危调用拦截数累计一千两百多次。其中真正带攻击意图的很少,绝大部分是模型自己"想多了",比如根据文档里出现的内网地址就去尝试访问。这个比例本身说明了一件事:不需要有人攻击,模型自己就会往危险的地方走。
误拦率是我们最担心的指标,稳定在千分之三左右。误拦主要集中在两类:企业自己的公网服务但域名带 internal 字样、以及一些走非标端口的合作方接口。这些通过补白名单解决,现在每月大概补一两条。
工具调用超时率百分之一点二,主要是外部网站响应慢导致的抓取超时,属于正常范围。沙箱侧因为死循环被强杀的比例是万分之六,加了并发上限之后再没影响过宿主机。
审计覆盖率百分之百,这是硬要求。四个月里安全部门主动查过十九次,其中三次是从告警发起的,都在十分钟内定位清楚。
还有一个软性收益,业务方现在敢接更多工具了。上线时只有五个注册工具,现在是二十三个,因为注册流程里有明确的策略声明,安全评审从"一事一议"变成了填表,评审周期从平均两周降到两天。数据为项目复盘口径,已做脱敏处理。
做工具调用的安全管控,第一个要转变的观念是:模型不是可信调用方。它生成的参数和普通用户提交的表单没有本质区别,甚至更危险,因为它会被上下文里的内容诱导,而那些内容可能来自不受控的文档或网页。凡是模型生成的东西,一律当外部输入处理。
SSRF 防护千万别在 URL 字符串层面做。我们第一版就是这么写的,被绕了四五种方式。判断必须下沉到实际建立连接的那一刻,用真实解析出来的 IP 判,重定向逐跳判。这个原则不是我们发明的,安全圈里是常识,但做业务系统的人容易想不到。
安全能力要给使用方留出口子,否则最后的结果是能力被整个关掉。我们这个项目最初的方案是全白名单、任何动态 URL 都不许访问,安全部门当然满意,业务方直接说那我不用了。分级策略加上会话内次数限制,才是双方都能接受的点。
审计系统要能重放。这一点是客户教我的。日志再全,看的人也很难从字段里还原当时的场景,能重放就不一样了,等于给了他们验证的能力。信任是从"我能自己查"来的。
现在还没做的是工具调用的行为基线。理想情况下应该学习每个应用正常的调用模式,偏离基线的自动升级审查。我们试着做了个简单版本,误报太多,暂时关了。样本量还不够,打算再攒半年数据。