ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Agent安全加固:文件访问控制、SSRF拦截与响应脱敏实战

Agent安全加固:文件访问控制、SSRF拦截与响应脱敏实战

做 Agent 开发的这两年,我一直觉得 Agent 的能力边界是清晰的,但它的安全边界非常模糊。工具调用、文件读写、网络请求,这些能力组合起来,就是一个能真正"动手"的智能体。可一旦不加约束,它也可能变成一台无意识的攻击机器,对内探测、对外泄露,每一步都有潜在风险。

这篇文章要聊的,是我最近在做一个 Agent 平台时,给它补上的三道"护栏":文件三级访问控制、SSRF 拦截,以及带校验位的响应脱敏。这是从一个真实项目中提炼出来的安全加固方案,适合正在做 Agent 开发、AI 应用集成,或者准备在企业环境里把 Agent 真正落地运转的团队参考。无论你是做触达隐私数据的内部助手,还是面向公网的工具型 Agent,这三层防护的思路基本通用。

1. 项目概述与核心设计思路

1.1 为什么 Agent 必须上护栏

先说我踩过的一个坑。早期版本的 Agent 只接了一些公开 API,我以为不会出什么事。结果有一次测试,让 Agent "帮我看一下本地项目结构",它直接顺着默认路径往系统目录遍历,虽然没有造成实际破坏,但日志里已经能看到它尝试读取非授权文件的行为。后来又发现,Agent 在解析用户提供的 URL 时,如果不去校验目标地址,它就可能在用户诱导下访问内网的管理接口——这不是 Agent 的错,而是我在设计阶段没有给它设定边界。

Agent 和传统后端服务的本质区别在于:它的行为路径是动态的、由模型决策驱动的,不能用静态的接口白名单来限制。你没办法穷举它可能调用的每一个文件路径、每一个网络地址,所以必须建立一套通用的、可拦截的访问控制层。这套护栏不需要理解 Agent 的"意图",只需要在每一次文件访问、每一次网络请求、每一次响应输出经过时,按规则检验放行。

这也是我把这三件事放在一起做的原因:文件访问是 Agent 读取上下文的入口,网络请求是 Agent 获取外部信息的通道,响应输出是 Agent 与外界交互的出口。入口、通道、出口,三处都设卡,才能把风险压制在一定范围内。

1.2 三层护栏的整体架构

我最终实现的架构,是在 Agent 的运行时外面包了一层"网关层",不侵入 Agent 主流程,而是以拦截器的方式串在工具调用链上。每个工具请求都会经过三个模块:

  • 文件访问控制器:负责路径解析、权限校验、内容检测,决定 Agent 能否读写某个路径。
  • 网络请求防火墙:负责 URL 解析、DNS 解析、IP 判定、重定向跟踪,决定 Agent 能否访问某个网络资源。
  • 响应脱敏引擎:负责扫描输出中的敏感数据,进行脱敏,并计算校验位,供下游验证响应是否被篡改。

这三个模块是独立的服务,通过 gRPC 和 Agent 运行时通信。这样做的另一个好处是,策略更新不需要重启 Agent 主进程,下发配置即可生效。后面我会逐个拆开讲实现细节。

2. 文件三级访问控制:从路径到内容的层层设卡

2.1 第一级:路径沙箱与白名单校验

文件访问的第一道关卡,是解决"Agent 能看哪些路径"的问题。我采用的是"沙箱根目录 + 路径规范化"的方案:为每个 Agent 会话分配一个专属的工作目录,Agent 的所有文件读写操作都必须在这个目录范围内完成。

这里最容易踩的坑是路径穿越。用户输入../../etc/passwd这类路径时,如果你只做了简单的字符串前缀匹配,黑客用一个..就能绕过去。我的做法是先调用path.Clean(Go 语言里是filepath.Clean,Python 里是os.path.normpath)将路径规范化,再对 Symlink 做解析,确保最终得到的绝对路径真实存在于工作根目录之下。

提示:Symlink 是一个常见的绕行漏洞。即使路径已经检查过,如果工作目录里有一个指向/etc的软链接,/data/agent1/link_to_etc/passwd依然能读到系统文件。所以我在校验时强制解析所有 Symlink(Go 里用filepath.EvalSymlinks),用解析后的真实路径做判断。

除了防穿越,我还加了一个基于 Globs 的"路径形态校验"。比如明文禁止路径段中出现..、禁止以.ssh、.aws之类的隐藏敏感目录开头,禁止访问配置文件扩展名(.env、.pem、.key)。这一步不是用来兜底的,而是为了尽早拦截明显恶意的请求,降低后续计算的负载。

2.2 第二级:权限矩阵与会话级授权

路径沙箱解决的是"能不能到这个地方"的问题,但还没解决"到了之后能不能读、能不能写"的问题。所以第二级我引入了一个权限矩阵,核心概念是:每个 Agent 会话关联一组授权标签(Scope),每个路径资源声明它需要的标签,请求必须匹配才能放行。

我构建了一张简化的权限表:

资源范围需要的授权标签允许的操作说明
/sessions/{session_id}/workspaceworkspace:read读会话自己的工作区
/sessions/{session_id}/workspaceworkspace:write写仅当任务声明需要
/shared/datasets/{dataset_id}dataset:{dataset_id}:read读共享数据集,按 ID 隔离
/system/config无无全部拒绝
/tmptmp:read读、写临时文件,但有大小限制

这张表看着简单,但它帮我解决了一个隐含问题:Agent 框架(比如 LangChain、Spring AI 这类)通常会自己缓存一些配置或临时文件,如果权限矩阵设得太严,框架本身会崩;设得太松,又等于没设。最后我的策略是:对 Agent 运行时自己的目录给读权限,但写权限必须显式声明。每个任务在启动时,运行时都会声明"这次任务需要写哪些路径",如果有任务结束后还尝试写额外路径,直接被拦截并写入审计日志。

2.3 第三级:落地前的静态与动态检测

前两级管住了"路径"和"权限",第三级管的是"内容"。因为即使 Agent 有权限读某个路径,路径里也可能装着不该被带出沙箱的数据——比如一份包含手机号的订单表、一个存储 API 密钥的配置文件。

内容检测我分两段做。静态检测在文件读取后立即执行,用一组正则和指纹库快速匹配:手机号、身份证号、私人密钥格式(sk-开头、AKIA开头)等。动态检测则交给一个轻量的分类模型,在读取的内容进入模型上下文之前,判断它是否包含高敏数据。如果命中,文件访问控制器会返回"拒绝读取",而不是返回内容。

这里有一个我试过觉得有效的细节:静态检测中的正则不要直接匹配完整的敏感字段,而是匹配"敏感字段的上下文"。比如注册码通常是sk-加 24 位字符,你可以检测(?i)\bsk-[a-z0-9]{20,}\b这种模式,但要小心误杀普通文本,我会在上报时保留命中的上下文片段,方便人工判断是真是假。

3. SSRF 拦截:把内网探测的路堵死

3.1 SSRF 在 Agent 场景里的真实危害

SSRF(服务端请求伪造)很多人以为是 Web 应用的老问题,但在 Agent 场景里危害更大。因为 Agent 通常会有一个"帮用户访问链接、抓取网页"的工具,这个工具天然就是 SSRF 的入口。攻击者只需要诱导 Agent 访问http://127.0.0.1:8080/admin或者云上元数据地址http://169.254.169.254/latest/meta-data/,Agent 就会代替攻击者发起请求,而且因为请求来自内网,很多防火墙不会拦截。

我在设计时参考了主流 WAF 和云厂商的防护规则,梳理出三类必须拦截的目标:

  • 回环地址与本地地址:127.0.0.0/8、::1、localhost等,主要防攻击者探测本机端口。
  • 私有网段与链路本地地址:10.0.0.0/8、172.16.0.0/12、192.168.0.0/16、169.254.0.0/16,防内网探测与云元数据窃取。
  • 保留与特殊地址:0.0.0.0/8、100.64.0.0/10(CGNAT)、多播地址等。

3.2 DNS 解析与重绑定防护

单纯做 IP 黑名单是不够的,最大的坑在 DNS 重绑定。攻击者第一次让 DNS 解析返回一个公网 IP,通过校验后,第二次解析却返回内网 IP。程序如果只做一次 DNS 查询,就可能在"校验通过"之后,访问到真正的内网地址。

我的解法是"双解析 + 二次校验":先解析一次拿到 IP 列表,逐个校验;然后在对目标发起连接时,强制使用同一 IP,而不是重新走系统 DNS。这一步要求 HTTP 客户端必须支持自定义 DialContext(Go 的标准库自带这个能力,Python 的urllib3可以通过socket.create_connection包装实现)。

如果 Agent 运行环境是容器或者 Kubernetes,我更推荐直接用网络策略卡死出网方向:让 Agent Pod 只能访问白名单网段,其他全部拒绝。这样即使代码层有漏洞,网络层还能兜住。

注意:URL 解析时要处理各种绕行写法。http://2130706433/这样的十进制 IP,http://0x7f000001/这样的十六进制写法,还有http://[::ffff:127.0.0.1]/这种 IPv4 映射的 IPv6 写法,都必须统一归一化成标准 IP 再做拦截。我踩过这个坑,早期只拦了127.0.0.1,结果测试用0x7f000001轻松绕过。

3.3 重定向与协议过滤的细节处理

SSRF 拦截里很容易被忽略的还有重定向。一个公网 URL 可能 302 跳转到内网地址,如果拦截器只检查了第一个请求的 URL,后面跟重定向时不做检查,那这条链路就被打穿了。我实现了一个"跟随重定向但每次跳转都重新走完整校验"的逻辑,重定向次数限制在 5 次以内,超过直接拒绝。

协议过滤同样重要。Agent 的网络工具应该只允许http和https,file、gopher、dict、ftp这类协议必须禁用。gopher协议尤其危险,因为攻击者可以用它在非 HTTP 端口上伪造任意字节流——经典的打法就是用 gopher 打 Redis 未授权访问,通过 Agent 发起一条写计划任务的命令,从而拿下服务器。虽然现在很多 Redis 默认配置已经加了保护,但这个风险在 Agent 这个"好人"身上依然存在:Agent 会用高权限身份去访问内网,一旦被诱导到 Redis 6379 端口,危害远大于普通攻击者手动探测。

接着是端口限制。我给网络请求工具设了默认只允许80、443、8080、8443这类标准 Web 端口,其他端口默认拒绝,除非管理员显式加白。这一步能有效压缩攻击面,至少不会让 Agent 去访问内网任意的 Redis、MySQL、Elasticsearch 端口。

4. 带校验位的响应脱敏:防泄漏还得防篡改

4.1 为什么要给脱敏结果加校验位

响应脱敏听起来不复杂:把 Agent 输出里的电话号码、身份证、密钥等敏感数据打码就行。但在真实场景里,我遇到一个进阶问题:脱敏引擎在网关层运行,而下游消费方(比如调用 Agent API 的业务系统)并不知道该信多少。如果响应在脱敏后被中间环节篡改,比如把脱敏后的138****1234还原成另一个真实号码,下游该怎么发现?

这就是"校验位"存在的意义。我在脱敏后的响应体里附带一个基于原始内容的 HMAC 值,叫做"校验位"。下游拿到响应后,用它自己的密钥重新计算脱敏后内容的 HMAC,如果与校验位不一致,说明内容在传输或处理过程中被改过,应当丢弃或告警。

这里有一个关键设计决策:校验位也是对脱敏后的内容算的,而不是对原始内容算的。因为一旦对原始内容计算 HMAC,校验位本身就变成了泄露通道——攻击者可以拿着响应和校验位做离线字典攻击,验证他猜的原始值对不对,脱敏就形同虚设了。

4.2 脱敏规则与校验位的协同设计

脱敏规则我按数据类别划分,每一类都有对应的掩码策略和上下文保留策略:

数据类型掩码策略上下文保留比例示例
手机号保留前 3 后 4,中间打码高138****1234
身份证号保留前 4 后 4低1101**********1234
邮箱保留首字符和域名中a***@example.com
API 密钥全部打码,仅显示类型无[REDACTED_API_KEY]
地址保留省市级,隐藏门牌中北京市朝阳区***

这样一个设计能保持业务可用性:下游如果只是做展示,掩码后的号码完全够了;如果需要核验,可以比对掩码后的部分是否匹配,而不是拿完整原文。

校验位的计算我用的是 HMAC-SHA256,密钥由配置中心下发,按会话粒度轮换。校验位不放在 HTTP 头里,而是放在响应体的一个固定字段中,方便不同语言的客户端统一处理。每次脱敏操作都会记录日志,包含本次命中的敏感类型数量、命中字段分类,以及校验位的前 8 位,方便排查时快速定位。

4.3 校验位与日志审计的联动

这里想分享一个容易被忽略的经验:脱敏引擎必须保持"状态无关"。它对每一次响应都独立处理,不要把上一次的结果缓存起来,因为 Agent 的响应可能是流式的,逐片段输出。我一开始为了实现简单,用了全局缓存记录"这次会话脱敏过哪些内容",后来发现两个问题:一是 Agent 响应较长时会漏掉后半段,二是并发请求会串数据。

改成状态无关后,逻辑变成了:每个响应片段独立扫描、独立脱敏、独立计算校验位,片段与片段之间用序号关联。下游聚合时,把所有片段的校验位合并成最终的校验位。这个方案在流式输出的场景下实测稳定,代价是会多出一点点计算量,但对安全性是值得的。

5. 常见问题与排查技巧实录

5.1 误杀与漏放并存怎么平衡

脱敏最怕的就是误杀太多,正常业务全被拦截;同时也怕漏放,敏感数据堂而皇之流出去。我踩过几次坑之后,总结出一条策略:脱敏按"层级"逐步放开,而不是一次性全拦。

具体做法是:第一轮先用最严格的正则,只放行完全不含任何敏感模式的响应;第二轮对命中敏感模式的内容做掩码,而不是直接拒绝整个响应;第三轮再根据命中类型,决定是掩码还是阻断。这样业务方拿到的响应基本可用,只是敏感字段被处理了。如果某类内容频繁误杀,可以在配置中心单独加白,而不是下调全局阈值。

5.2 性能开销与延迟优化

加上这三层护栏之后,一个工具调用的耗时大约增加了 20 到 40 毫秒,在大多数场景下可接受。但如果遇到大文件读取或超大响应体,检测耗时可能会翻倍。我的优化思路是"分层提前退出":路径检查、权限检查、内容检查、网络校验,每一层都是独立模块,能提前拒绝就直接返回,不再往下走。实际测试中,大多数恶意请求被第一层路径沙箱就拦截了,根本走不到后面的高开销检测。

网络请求部分我加了响应体大小的上限(默认 2MB),防止攻击者用超大响应拖垮 Agent 进程。文件读取同样有大小限制,超过 10MB 的文件会被截断或者拒绝读取,具体看任务类型。

5.3 联动测试:真实攻击模拟的几条经验

最后分享一个我觉得很有价值的测试方法:不要只测正向功能,要把这三层护栏当成一个整体,做"攻击链"模拟。

  • 攻击者上传一个包含软链接的压缩包,诱导 Agent 解压后读取链接指向的系统文件。这时候第一层路径沙箱必须通过EvalSymlinks拦下来。
  • 攻击者给一个看似正常的公网 URL,但 URL 本身会 302 跳转到内网地址。这时候网络请求防火墙的"重定向重新校验"机制必须生效。
  • 攻击者诱导 Agent 访问云元数据接口,拿到临时密钥后写入工作区,再诱导 Agent 在回复里带上密钥。这时候响应脱敏引擎要能在输出前把密钥特征扫出来并打码,同时校验位确保下游拿到的响应是完整的、未被篡改的。

这三条链路我分别压测过,最终的结论是:单层防护一定有漏洞,但三层叠加之后,攻击者想要绕过的成本会大幅上升。每一层都只做自己的事,不依赖前一层的结果,这样即使某一层被绕过,后面还有兜底。

给 Agent 上护栏这件事,本质上是在"能力"和"信任"之间划一条线。Agent 的能力越强,这一步就越不能省。我现在的体会是,安全设计要在 Agent 架构的早期就介入,等业务跑起来了再回头补,改造成本会翻好几倍。你可以在自己的项目里先拿文件访问和 SSRF 这两层起步,它们相对独立、见效快,跑顺了再补响应脱敏和校验位,一步一步把边界扎紧。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进