资讯动态

Agent 工具调用安全加固:文件访问、SSRF 拦截与响应脱敏实践

发布时间:2026/10/18 20:22:20 来源:尧图企业网站定制
花了不少时间调 Agent 的工具调用权限然后发现真正麻烦的不是模型而是模型能碰到的那些东西。一个能帮你读文件、拉网页、调内部 API 的 Agent本质上就是一个没有安全意识但拥有大量凭证的实习生。你让它去查一下日志它可能顺手把日志里某个用户的 token 回给了调用方你让它抓取一篇文章它可能因为提示注入去请求云元数据地址把临时凭证吐出来。最近我把一个内部 Agent 项目重做了一遍安全加固核心就是三件事文件访问做了三级控制、外发请求加了 SSRF 拦截、响应出口统一做脱敏并附带校验位。这套方案不依赖特定框架只要你的 Agent 有工具调用Function Calling / Tool Use能力就能套用。1. Agent 安全的第一性问题你没法控制模型的“意图”传统应用安全的核心是控制用户的输入。Agent 场景反过来了危险的是模型根据输入产生的“工具动作”。你很难在 Prompt 层杜绝模型被诱导所以必须在模型和真实资源之间加一层强制约束。1.1 为什么 Prompt 防护不可靠早期我试过在 system prompt 里写“不得访问内网地址”“不得读取非授权文件”实测效果约等于零。举一个真实经过某次测试用户输入一篇包含 Markdown 图片链接的文章模型识别出“这是一张远程图片”自动调用 fetch 工具去获取图片。如果在 Prompt 里做了图片链接拼接攻击者把链接写成http://169.254.169.254/latest/meta-data/模型很可能就把云厂商的元数据抓回来了。Prompt 注入是目前 Agent 领域公认的难题业界最好的做法也不是靠模型自觉而是靠“工具边界”。你的工具能访问什么资源、能发出什么请求、能返回什么数据必须在模型不可控制的层面写死。1.2 三项护栏的职责划分我给这套护栏定了三个目标对应标题里的三项能力文件访问控制 Agent 能读哪些文件、写哪些文件。这里最容易出问题的是路径穿越../../etc/passwd和符号链接指向敏感目录。外发请求控制 Agent 能访问哪些网络目标。不仅要防误访问内网 IP还要防 DNS 重绑定、IPv6 映射、重定向跟随这类绕过。响应出口控制 Agent 生成的内容里不包含敏感数据。要意识到即便工具层面做了访问控制LLM 也可能把上下文里的信息拼接进回答所以需要在出口统一脱敏。所谓三级文件访问就是纵向把“能接触文件”的能力拆成三个可独立配置的层次任何一层不通过就不放行。层级控制粒度典型手段系统级进程能访问哪些路径OS 账号权限、chroot、容器只读层Agent 级具体某个工具能读哪些目录工具注册时声明资源范围指令级单次调用的路径是否合法路径规范化 白名单前缀校验2. 文件三级访问从操作系统到单次调用的逐层拦截这层我踩过不少坑最关键的一点是不要用一个拦截函数打天下。单层校验一旦有漏洞整个文件系统就暴露了。三级各有各的防线哪怕前两级被绕过第三级也能兜住。2.1 第一级系统级最小权限这一级很多人会忽略因为觉得后面有路径校验就够了。但路径校验函数难免有 bug最稳妥的做法是让 Agent 进程本身就碰不到敏感目录。我在生产环境用的是容器隔离Agent 执行代码跑在独立容器里文件系统只有/workspace可写整个/etc、/root、/home都是只读或者干脆不可见。几个细节容器以非 root 用户运行UID 必须显式指定不能省略/workspace挂载为独立数据卷宿主机其他目录都隔离敏感文件如果必须让 Agent 访问用单独挂载而不是放进 workspace# docker-compose 片断注意 user 和 read_only services: agent-worker: image: agent-runner:latest user: 10001:10001 read_only: true tmpfs: - /tmp volumes: - agent-workspace:/workspace:rw这层最大的意义是“失陷兜底”。假设 Agent 被完全攻破攻击者也只能在这个容器里折腾拿不到宿主机和整个内网的控制权。2.2 第二级工具注册表到了 Agent 级你要做的是在工具注册时声明它的资源边界。每个文件类工具都应该带上一个allowed_paths清单路径加载器只能从这个清单里选目录。我用的工具声明格式类似FileToolSet { read_file: { name: read_file, allowed_paths: [/workspace/data, /workspace/config], blocked_paths: [/workspace/data/private], }, write_file: { name: write_file, allowed_paths: [/workspace/output], }, }工具装载时如果发现某个工具有资源请求但没有allowed_paths直接拒绝注册。宁可少一个功能也要保证每个工具都能说清楚自己能碰哪里。2.3 第三级指令级路径校验第三级是真正干活的地方也是绕过手法最多的环节。这里别自己写路径解析一定用标准库的规范化函数。核心逻辑先将路径转换为绝对路径对软链接执行 realpath 解析防止软链接指向白名单外校验解析后的路径是否以允许的目录前缀开头对不存在的文件需要先校验父目录import os from pathlib import Path def check_path_access(tool_path: str, allowed_prefixes: list[str]) - bool: # 1. 转绝对路径并规范化 candidate Path(tool_path).expanduser().resolve() # 2. 如果路径存在解析所有软链接 if candidate.exists() or candidate.parent.exists(): candidate candidate.resolve(strictFalse) # 3. 前缀校验 for prefix in map(Path, allowed_prefixes): resolved_prefix prefix.resolve(strictFalse) try: candidate.relative_to(resolved_prefix) return True except ValueError: continue return False一个很容易漏掉的细节校验前缀时不能只判断“路径字符串以/workspace/data/开头”因为resolve()已经做了规范化能防掉绝大多数路径穿越。但如果想偷懒用字符串前缀判断我建议也别拦着只是会收到一堆奇怪的 bug 报告。2.4 soft link 和 hard link 的坑这是三级校验里最阴间的环节。某个目录下存在一个软链接比如/workspace/data/link_to_logs - /var/log如果不做 realpath 解析你校验的是/workspace/data/link_to_logs/security.log看似在白名单前缀内实际读的却是/var/log/security.log。更阴间的是 hard link。在同一个文件系统内你可以让一个目录里的硬链接直接指向/etc/passwd从路径上看完全正常。防 hard link 的办法是系统级配置加protected_hardlinks或者校验 inode 是否是常规文件而不是在应用层死磕。3. SSRF 拦截Agent 发起的请求同样需要目标校验SSRFServer Side Request Forgery服务端请求伪造的核心问题是服务端发起了攻击者期望的请求。Agent 场景里攻击者通过提示注入让模型发起请求本质和传统 SSRF 一样但因为模型行为不可预知拦截难度更高。3.1 目标校验的第一步URL 标准化拿到一个 URL 之后别急着发起请求。先要把它拆开看清楚真正要连接的主机是谁。http://2130706433/ - 127.0.0.1十进制 IP http://0x7f000001/ - 127.0.0.1十六进制 IP http://0177.0.0.1/ - 127.0.0.1八进制 IP http://[::ffff:127.0.0.1]/ - 127.0.0.1IPv4 映射地址 http://localtest.me/ - 127.0.0.1DNS 解析这些变形看起来低级但实际自测的时候能拦住大半因为很多第三方 SSRF 库本身就不处理这些。推荐的写法是先解析 URL再提取 host/port最后把各种格式统一转成标准 IP 格式去判断。import ipaddress from urllib.parse import urlparse def validate_url_target(url: str) - tuple[bool, str]: parsed urlparse(url) host parsed.hostname port parsed.port or (443 if parsed.scheme https else 80) if not host: return False, missing-host return validate_host(host, port)算法的核心如果 host 本身是 IP 字面量直接解析成标准 IP 判断私网/保留地址。如果 host 是域名先做 DNS A/AAAA 解析拿到所有解析结果后逐个判断任何一个解析结果是内网地址就拒绝。重点是解析结果要转化成标准 IP 形式比如::ffff:10.0.0.1要识别出真实 IP 是 IPv4 的10.0.0.1。3.2 禁止用域名直接建连聊聊 DNS Rebinding这是我这一节最想强调的一点。校验完解析结果是公网 IP不等于安全因为你校验时解析的 IP 和实际建连时解析的 IP 可能不一样。这就是 DNS Rebinding。攻击者控制一个域名第一次解析返回公网 IP比如你自己的博客服务器校验通过随后攻击者切换 DNS 解析到内网地址比如 172.16.0.1。如果你的代码是“校验解析结果-放行-再次用域名发请求”第二次解析就可能拿到内网 IP。正确做法是校验通过后用解析出来的 IP 作为连接目标而不是再用域名去建连。也就是校验和建连必须使用同一次解析结果并且建连时禁用自动 DNS 解析。如果把安全策略放到 HTTP 客户端层要注意重定向跟随。默认 Pythonrequests会自动跟随重定向攻击者可以利用一个公网重定向跳转到内网地址。所以禁掉自动跟随手动检查每个跳转身的 URL或者干脆不允许跨域跳转。3.3 网络层的兜底出口限制应用层校验再严格也怕有遗漏。我的建议是网络层加一层兜底Agent 容器的网络出口只允许访问固定的公网 IP 段或者干脆走 HTTP 代理由正向代理层做黑白名单。做法参考Agent 容器只允许访问指定的 HTTP 代理地址不允许直连外网代理层维护一份“允许访问的公网域名/网段”名单代理层对远程地址再做一次目标校验防止应用层被绕过这样即使应用层出现了一个逻辑漏洞让请求发出去了网络层也会兜住。4. 响应脱敏与带校验位的防篡改机制文件访问和 SSRF 拦的是“Agent 能看到什么、能触达哪里”响应脱敏管的是“Agent 返回的答案里有没有夹带私货”。4.1 为什么要在出口统一脱敏有人觉得文件访问都拦干净了Agent 返回的内容自然是安全的。这个想法太乐观。LLM 的回答不一定是某个工具的直接输出它可能把工具返回内容、系统提示、历史会话拼接在一起重新组织语言。模型不可控所以出口必须有一道独立防线。脱敏要在“Agent 返回内容”这个出口统一处理不能在工具层做。工具层脱敏会导致模型在上下文里拿到的是脱敏后的内容影响推理但出口脱敏不会。4.2 脱敏位点与敏感数据扫描我实现的脱敏位点工具结果返回给模型之前可选防止敏感数据进上下文模型最终响应返回给调用方之前必须做敏感数据的类型要覆盖常见的高价值泄露项手机号、邮箱、身份证号、各类 API Key / Token、数据库连接串、内网 IP 地址、云厂商临时凭证、带 Metadata 的 URL 链接。逻辑比较简单本质是正则匹配 替换。区分度依赖于正则质量下面给一个例子import re SENSITIVE_RULES [ # 云厂商 AK 前缀示例 (re.compile(rAKID[0-9A-Za-z]{16,}), AK/), (re.compile(rsk-[A-Za-z0-9]{20,}), SK/), # 典型 access_token 形式 (re.compile(rBearer\s[A-Za-z0-9\-_.~/]*), Bearer TOKEN), # 内网 IP (re.compile(r\b(?:10|127|172\.(?:1[6-9]|2\d|3[01])|192\.168)\.\d{1,3}\.\d{1,3}\b), PRIVATE_IP/), # 邮箱 (re.compile(r[A-Za-z0-9._%-][A-Za-z0-9.-]\.[A-Za-z]{2,}), EMAIL/), ] def sanitize_text(text: str) - str: for pattern, repl in SENSITIVE_RULES: text pattern.sub(repl, text) return text这里有个区分——规则不能一刀切把所有 IP 都盖掉否则 Agent 想返回“服务部署在 10.10.10.10”这类正常信息也被拦影响功能。所以“内网 IP”的正则里只匹配私网段公网 IP 不管。4.3 校验位让调用方确认“这份响应是脱敏过的”只做替换有个问题调用方比如前端、另一个服务如何确认自己拿到的响应确实经过了脱敏网关如果攻击者绕过网关直接调用后端接口拿到的就是未脱敏响应。如果网关和调用方之间被中间人篡改调用方也无从察觉。我引入了校验位机制做法比较直接对脱敏后的响应内容计算一个 HMAC附加在响应头里。import hashlib import hmac SANITIZE_SECRET bset-a-different-secret-in-production def compute_checksum(body: bytes) - str: return hmac.new(SANITIZE_SECRET, body, hashlib.sha256).hexdigest()调用方拿到响应后先取X-Sanitize-Checksum头对响应体做同样的 HMAC 计算并比对。如果一致说明这份响应在网关脱敏之后没有被篡改。这里校验位的意义防绕过调用方可以通过校验位判断响应是否真的经过网关。防篡改一旦响应被中间人替换校验位就对不上。审计依据网关可以在日志里记录每个响应的校验位事后排查非常方便。个人建议校验位只对文本类响应application/json、text/plain 等生效二进制文件流不要去算 HMAC因为性能开销大而且文件类响应通常走独立接口不属于模型生成的文本响应。4.4 脱敏失败时的降级策略正则替换不是百分百可靠总有漏网之鱼。我的做法是加一道“疑似敏感内容”检测如果某个响应在替换后仍包含大量疑似密钥结构比如连续 32 位以上的 base64 字符串网关直接返回一个标准的 refuse 报文而不是冒险把未脱敏内容放出去。HTTP/1.1 200 OK Content-Type: application/json X-Sanitize-Checksum: 7f9c...比对失败时客户端应扔掉整个响应 {code: 403, message: response blocked: potential sensitive data}注意这时校验位也要照常计算。如果调用方发现校验失败/报文内容是 refuse就知道这次请求有问题可以把 payload 存下来给安全组排查。5. 落地的完整链路与排错笔记三个模块单独实现都不难真正花时间的是把它们串进 Agent 框架里并且在真实场景里不自相矛盾。5.1 编排请求在护栏间的流转顺序以一个带工具调用的 Agent 为例完整链路如下用户输入先到达 Agent API 网关。Agent 框架启动会话LLM 生成工具调用请求。工具调用请求进入工具网关先做鉴权和参数白名单校验。文件类工具走文件三级访问HTTP 类工具走 SSRF 拦截数据库类工具走是否允许该会话调用的校验。工具执行完成后响应回到工具网关如果包含敏感信息则在工具层打脱敏标记可用。LLM 根据工具结果生成最终回复回复在出口网关做强制脱敏 HMAC 签名。调用方拿到响应校验X-Sanitize-Checksum一致则接受不一致则丢弃。5.2 常见坑清单这套系统上线以后我记录了几个高频踩坑点写在这里供参考1. 路径校验的绝对路径和软链接问题很多同学只判断Path(tool_path).resolve()之后的前缀而忘了校验“父目录是否存在”。一个路径resolve(strictFalse)和strictTrue的行为不同对不存在的文件会抛出异常导致工具调用直接失败。建议所有文件访问统一走一个safe_open函数在函数内部做完整校验而不是靠每个工具自己拼路径。2. SSRF 校验里对“重定向跟随”的处理HTTP 客户端库默认跟随重定向攻击者可以把http://your-public-server配成 302 跳转到http://172.16.0.1。我一开始没管这个到内网扫描测试时才发现。正确做法是禁用跟随或者像前面说的手动逐个校验跳转目标。requests.get(url, allow_redirectsFalse)3. 脱敏的顺序问题脱敏规则必须放在最后执行不能在 LLM 生成回复之前就全局替换一遍否则模型看到的脱敏内容可能影响语义。工具层可以打标记但最终面向调用方之前必须做最后一遍脱敏。4. HMAC 校验位的 payload 类型判断如果响应是二进制比如 Agent 下载文件HMAC 算出来调用方也很难校验反而成为性能瓶颈。我的策略是只在文本响应上启用校验位二进制接口单独走访问控制。5.3 验证方法用攻击样例做回归测试护栏做完了不能只靠想象我强烈建议建一套回归测试集包含了攻击样例攻击样例清单 - 路径穿越read_file(../../etc/passwd) - 软链接绕过/workspace/data/link_to_etc/passwd - 混合场景write_file 到 /workspace/../workspace2/evil.txt - SSRF 私网直连http://169.254.169.254/、http://10.0.0.1/ - SSRF 变形十进制 IP、IPv6 映射、DNS Rebinding - 提示注入 外发让模型抓取公网恶意 URLURL 里重定向到内网 - 敏感数据透传让模型在回答中返回自定义 token确认出口被盖掉 - 校验位防篡改模拟中间人替换 json 内容确认 checksum 失败每次更新 Agent 框架或某个工具实现都跑一遍这个清单。实测下来这是性价比最高的安全投入花半天时间建好后面能省下大量沟通成本。6. 收尾安全护栏和 Agent 能力的平衡做完这套方案之后我的体会是Agent 安全最难的其实不是某一个技术点而是别让安全把 Agent 变傻。三级文件访问如果配置太死Agent 经常读不到它应该读的配置SSRF 如果只允许少数白名单域名Agent 拉资料的能力就废了一半。我现在的调参思路是文件访问默认从严只有在工具声明里明确写清楚“读 /workspace/public”这类需求才放开SSRF 默认放行公网 IP内网一律拦截特殊情况走独立的内网专用工具这样便于审计脱敏规则保持“宁可错杀不放过密钥”。另外建议给每个 Agent 实例分配独立凭证和独立审计 ID。这样一旦某个 Agent 出了安全问题你能快速定位到是哪个实例、哪个配置、哪个时间点出的问题。最后分享一个小技巧安全网关的日志别只记“拦截了什么”要把“放行了什么”也记录下来特别是针对 SSRF 和文件访问的放行记录。很多时候你排查一个 Agent 的诡异行为最后发现是某个被放行的请求在搞鬼。这套护栏方案我在内部跑了两个多月从最初每天都有告警到后面基本稳定。它能拦住的不是所有攻击但可以把最常见的、危害最大的几类风险堵住。如果你的 Agent 也接了文件工具和 HTTP 工具尽早把它加上别等到上线了再补。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑