尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

AI链接跟随防护:用FastAPI构建RequestGuard出站请求守卫

AI链接跟随防护:用FastAPI构建RequestGuard出站请求守卫 RequestGuard 这个名字很直白它要在 AI 与外部链接之间增加一道请求守卫。大模型本身不会直接上网真正上网的是 Agent、浏览器工具或 HTTP 请求模块只要这些模块允许“跟随链接”攻击者就可以用一条恶意 URL 引着 AI 访问内网地址、云元数据端点或包含提示注入的页面。RequestGuard 解决的不是“AI 能不能上网”而是“AI 该访问什么、不该访问什么”。这篇文章不会去照搬项目源码而是围绕 RequestGuard 的防护目标拆解 AI 链接跟随背后的攻击面、请求检查链路以及一个可运行的 FastAPI 最小守卫服务。读完你可以把类似机制接进 LangChain、Spring AI 或自研 Agent也能直接用 curl 验证防护规则是否生效。1. 为什么 AI 跟随链接会成为新的攻击面1.1 AI 的“链接跟随”和人类点击不是一回事人类点击链接时通常会先看域名、看页面标题再决定是否进入。AI Agent 的“点击”则是一次程序化调用模型决定调用某个网页读取工具把 URL 交给代码去请求然后代码默认可能跟随重定向、接收任意响应体、甚至把页面内容当作后续决策的上下文。这个链路和浏览器的最大区别在于权限和自动化。AI 所在环境往往持有云厂商凭证、内网 API key、对象存储权限。如果它被诱导访问了http://169.254.169.254/latest/meta-data/云服务商元数据服务可能把临时密钥直接返回给页面读取逻辑。攻击者不需要突破防火墙只需要让 AI 帮自己发一个请求。AI Agent 让模型从对话系统变成了能操作外部世界的执行器。执行器越强出站请求越需要防护。传统的 WAF 主要防护入站攻击DLP 主要检测数据外发但 RequestGuard 这类机制卡在“AI 即将发起外部请求”这个时间点这是过去 Web 安全工具很少覆盖的位置。1.2 攻击者放一条链接能造成哪些后果下面这些场景在真实的 AI 应用里并不少见只是很多团队在产品原型阶段还没有遇到。攻击类型典型链接后果SSRF 探测内网http://169.254.169.254/latest/meta-data/云元数据泄露、临时凭证被读取SSRF 访问内网服务http://10.0.0.5:9200/_cluster/health内网管理接口暴露提示注入页面内隐藏!-- ignore previous --改变 AI 后续行为诱导输出上下文秘密数据外带攻击者服务器上带 JS 的页面让 AI 把会话信息拼入后续请求资源滥用长文本页面 循环重定向消耗 token、带宽和推理时间恶意文件分析指向二进制或钓鱼页面AI 摘要工具处理有毒内容下游链路被污染这里有一个共性攻击者并不需要攻破 AI 应用本身只需要让 AI 主动去访问一个不该访问的地址。链接本身就是攻击载荷。1.3 RequestGuard 作为出站防护层RequestGuard 更像一个“出站请求仲裁器”。它接收 AI 应用发来的请求信息包括 method、url、header 片段然后根据策略判断是否允许。允许AI 继续抓取拒绝AI 给用户返回安全提示。它的核心价值不是提高 AI 对页面的理解能力而是降低 AI 被外部内容操纵的风险。在 RAG 场景里它还可以限制检索来源禁止 Agent 读取不受信任的域名在工具调用场景里它可以关闭重定向、解析 DNS、过滤内网 IP。2. RequestGuard 的核心防护模型2.1 决策 API 而不是透明代理RequestGuard 有两种常见接入方式。第一种是透明代理方式AI 应用的出站流量全部经过 RequestGuard。由代理统一处理 CONNECT、GET、POST在流量层面过滤目标地址。优点是应用无感知缺点是代理需要承载大流量实现复杂度高。第二种是决策 API 方式。AI 应用在发起 HTTP 请求之前先调用 RequestGuard 的/check接口拿到allow或deny结果后再决定是否继续。这个模式安全可控便于在 LangChain、Spring AI 或自研代码里嵌入。下文的最小实现采用决策 API 方式。它不代理实际响应内容只负责回答“这个请求能不能出去”。2.2 一条请求要经过几道检查检查链路从快到慢排列顺序很重要。先做成本最低的规则匹配再做 DNS 解析和 IP 校验避免每个请求都触发外部 DNS 查询。一条请求通常要经历以下节点解析 URL检查 scheme 是否只允许http和https。检查 hostname 是否命中黑名单域名。检查 hostname 是否命中白名单域名。解析 DNS拿到 IP 列表。检查 IP 是否属于内网、环回、链路本地或策略中的 CIDR。根据策略决定allow、block并返回原因。这个顺序可以避免明显不合规的请求进入解析阶段。比如file:///etc/passwd根本不需要 DNS 解析第一步直接拒绝即可。用伪代码表示如下def decide(url, policy): parsed parse_url(url) if parsed.scheme not in policy.allowed_schemes: return deny(scheme_not_allowed) if matches(parsed.hostname, policy.blocked_domains): return deny(blocked_domain) ips resolve_dns(parsed.hostname) if any(private(ip) or in_blocked_cidr(ip) for ip in ips): return deny(private_ip) return allow(ok)2.3 策略对象默认拒绝白名单放行一个完整的请求策略应该包含 scheme 白名单、域名黑名单、域名白名单、CIDR 黑名单、重定向开关。下面是一份 YAML 示例default_action: block scheme: allow: - http - https domain: blocked: - localhost - 127.0.0.1 - 169.254.169.254 - *.local allow: [] cidr: blocked: - 10.0.0.0/8 - 172.16.0.0/12 - 192.168.0.0/16 - 169.254.0.0/16 - ::1/128 - fc00::/7 redirect: allow: false这里的关键是default_action默认为block。只有明确命中白名单或没有命中任何黑名单的请求才能放行。cidr.blocked用于兜底即使域名黑名单没有覆盖到某个内网地址IP 校验也能拦住。3. 用 FastAPI 编写一个最小 RequestGuard 服务3.1 项目结构与依赖最小实现只需要四个文件requestguard-demo/ ├── requirements.txt ├── policies.yaml ├── app.py └── client_demo.py依赖文件内容fastapi uvicorn pydantic pyyaml安装时不要直接锁死版本而是根据当前环境选择兼容版本。实际部署前建议确认 FastAPI、Pydantic 和 Python 版本之间的匹配关系。3.2 策略加载policies.yaml会被加载成 Python 对象。为了便于扩展策略类集中管理域名、CIDR 和重定向配置。import ipaddress import socket from urllib.parse import urlparse import yaml from fastapi import FastAPI from pydantic import BaseModel class Policy: def __init__(self, raw: dict): self.default_action raw.get(default_action, block) self.allowed_schemes raw.get(scheme, {}).get(allow, [http, https]) self.blocked_domains raw.get(domain, {}).get(blocked, []) self.allowed_domains raw.get(domain, {}).get(allow, []) self.blocked_cidrs [ ipaddress.ip_network(cidr) for cidr in raw.get(cidr, {}).get(blocked, []) ] self.follow_redirects raw.get(redirect, {}).get(allow, False) def load_policy(path: str policies.yaml) - Policy: with open(path, r, encodingutf-8) as f: return Policy(yaml.safe_load(f) or {})这里注意一点CIDR 在初始化时就要转换成ipaddress.ip_network对象后续判断成员关系会很快不需要每个请求都做字符串解析。3.3 域名和 IP 校验函数域名匹配需要支持完整域名和后缀通配。IP 校验则需要同时依赖 Python 内置的is_private和策略里的 CIDR 列表。def domain_matches(hostname: str, patterns: list[str]) - bool: hostname hostname.lower() for pattern in patterns: pattern pattern.lower().strip() if pattern.startswith(*.): suffix pattern[1:] if hostname.endswith(suffix): return True elif hostname pattern: return True return False def resolve_hostname(hostname: str) - list[str]: try: infos socket.getaddrinfo(hostname, None) except socket.gaierror: return [] ips: list[str] [] for info in infos: ip info[4][0] if ip not in ips: ips.append(ip) return ips def is_forbidden_ip(ip_str: str, policy: Policy) - bool: ip ipaddress.ip_address(ip_str) if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved: return True for net in policy.blocked_cidrs: if ip in net: return True return False在常见实现中is_private已经能覆盖 RFC1918 内网地址但保留显式的blocked_cidrs仍然有必要。比如某些云厂商元数据 IP 在不同网络环境下不一定被判定为 private需要策略层单独封禁。3.4 检查接口/check接口接收 URL 和 method返回决策结果和解析到的 IP。这样调用方不仅能知道是否被拦截还能看到被拦截的原因。app FastAPI(titleRequestGuard Demo) class CheckRequest(BaseModel): url: str method: str GET class Decision(BaseModel): allowed: bool reason: str url: str resolved_ips: list[str] [] app.get(/health) def health(): return {status: ok} app.post(/check, response_modelDecision) def check(req: CheckRequest): policy load_policy() # 生产环境建议启动时加载到内存 parsed urlparse(req.url) if parsed.scheme not in policy.allowed_schemes: return Decision(allowedFalse, reasonscheme_not_allowed, urlreq.url) if not parsed.hostname: return Decision(allowedFalse, reasonmissing_hostname, urlreq.url) hostname parsed.hostname if domain_matches(hostname, policy.blocked_domains): return Decision(allowedFalse, reasonblocked_domain, urlreq.url) if policy.allowed_domains and not domain_matches(hostname, policy.allowed_domains): return Decision(allowedFalse, reasondomain_not_allowed, urlreq.url) ips resolve_hostname(hostname) if not ips: return Decision(allowedFalse, reasondns_failed, urlreq.url) if any(is_forbidden_ip(ip, policy) for ip in ips): return Decision(allowedFalse, reasonprivate_ip, urlreq.url, resolved_ipsips) return Decision(allowedTrue, reasonok, urlreq.url, resolved_ipsips)这段代码刻意简化了策略加载频率。生产环境应该把 Policy 对象放到应用启动阶段避免每个请求都读 YAML。这里直接写在函数里是为了让最小示例更集中。3.5 调用端封装client_demo.py模拟 AI Agent 调用守卫服务的逻辑。请求到达/check后如果allowed为真再发起真正的 HTTP 抓取。import requests GUARD_ENDPOINT http://127.0.0.1:8090/check def guard_check(url: str, method: str GET) - dict: resp requests.post( GUARD_ENDPOINT, json{url: url, method: method}, timeout3, ) resp.raise_for_status() return resp.json() def safe_http_get(url: str, max_hops: int 3): current url for _ in range(max_hops): decision guard_check(current) if not decision[allowed]: return None, decision resp requests.get(current, allow_redirectsFalse, timeout5) if resp.is_redirect: location resp.headers.get(Location) if not location: return resp, decision current requests.compat.urljoin(current, location) continue return resp, decision raise RuntimeError(redirect too many)这里最关键的一行是allow_redirectsFalse。如果允许自动重定向那么守卫只检查了第一个 URL后续 302 跳转到内网地址时AI 应用不会再次请求守卫检查。4. 集成到 AI Agent 调用链路4.1 在 LangChain 工具中接入LangChain 的工具定义可以用装饰器包一层守卫逻辑。模型看到的是一个fetch_safe工具实际执行时每次都会先过 RequestGuard。from langchain.tools import tool tool def fetch_safe(url: str) - str: Fetch a URL content after RequestGuard check. response, decision safe_http_get(url) if response is None: return fblocked: {decision[reason]} return response.text[:2000]在 Spring AI 中思路也一样。你可以实现一个ToolCallback在真正的WebClient调用前先请求守卫服务。Spring AI 的生态里已经支持自定义工具回调关键是把守卫逻辑放在网络调用之前而不是放在模型生成之后。4.2 按上下文传递策略标签生产环境里不同 Agent 可能有不同权限。有的 Agent 只允许访问企业内网文档有的 Agent 可以访问外网。RequestGuard 的/check接口可以增加context字段用来携带策略标签。{ url: https://example.com/data, method: GET, context: customer-support-agent }策略服务根据context选择不同的规则集。这样能避免“一个白名单管所有 Agent”的粗粒度问题。4.3 部署位置与网络隔离RequestGuard 服务应该部署在 AI 应用的内部网络只对内网暴露不直接对公网开放。AI 应用访问外部网络必须经过受控出口而 RequestGuard 只提供决策不承担大流量代理因此它对带宽的要求不高。AI Agent 应用 | (内部 HTTP) v RequestGuard 服务 | (受控 DNS / 受控出口) v 外部网站 / 白名单内网服务如果团队已经使用了服务网格可以把 RequestGuard 注册为一个内部服务通过服务名调用。5. 测试与验证让防护效果可观测5.1 启动服务安装依赖后启动 FastAPIcd requestguard-demo pip install -r requirements.txt uvicorn app:app --host 127.0.0.1 --port 8090先检查健康状态curl -s http://127.0.0.1:8090/health正常输出{status:ok}5.2 用 curl 验证拦截结果通过公网域名测试curl -s -X POST http://127.0.0.1:8090/check \ -H Content-Type: application/json \ -d {url: https://example.com/notes}通过内网 IP 测试curl -s -X POST http://127.0.0.1:8090/check \ -H Content-Type: application/json \ -d {url: http://127.0.0.1:8080/admin}通过云元数据地址测试curl -s -X POST http://127.0.0.1:8090/check \ -H Content-Type: application/json \ -d {url: http://169.254.169.254/latest/meta-data/}5.3 用例矩阵测试 URL期望决策期望 reasonhttps://example.comallowokhttp://127.0.0.1:8080/adminblockprivate_iphttp://169.254.169.254/latest/meta-data/blockprivate_ipfile:///etc/passwdblockscheme_not_allowedhttp://192.168.1.1/blockprivate_iphttps://definitely.invalidblockdns_failed验证时不能只看allowed字段还要看reason是否和预期一致。dns_failed可能意味着测试环境 DNS 异常而不是规则生效。另外要验证重定向场景。用一段短 URL 或自己的跳转服务做测试确保allow_redirectsFalse生效AI 应用在收到 302 后会重新调用 RequestGuard 检查新的 Location。6. 常见问题、绕过方式和排查链路6.1 公网域名被误判为 private_ip现象是访问一个看起来正常的公网域名/check返回private_ip。先看响应里的resolved_ips。如果解析到10.x.x.x或192.168.x.x说明所在的 DNS 环境返回了内网记录。这可能是内网 DNS 策略导致的也可能是测试机器/etc/hosts里写入了该域名。检查方式nslookup example.com dig example.com处理方式调整 RequestGuard 所在环境的 DNS 配置或者在策略里临时排除该域名。但注意不要让排除范围过大否则会绕过 IP 校验。6.2 重定向绕过这是最常见的绕过方式。请求先通过https://public.example.com校验但该地址返回 302 跳转到http://169.254.169.254AI 应用默认跟随重定向请求就进入了内网。处理方案是在客户端强制allow_redirectsFalse收到 3xx 后重新解析 Location 并再次调用/check。RequestGuard 策略里也应该把redirect.allow保持为false提醒调用方不要在未检查跳转目标的情况下跟随重定向。6.3 DNS rebindingDNS rebinding 比普通内网 IP 更隐蔽。第一次解析返回公网 IP守卫检查通过等到真正的请求发出时DNS 再次解析返回内网 IP。由于守卫和抓取动作不是原子操作存在时间窗口。缓解方式包括在 RequestGuard 中缓存解析结果并要求 AI 应用使用同一个 IP 发起连接由守卫服务代理实际连接而不是把 URL 交给应用层自行解析对短 TTL 的域名提高防护等级。6.4 允许规则太宽如果白名单配置成*.example.com那么evil.example.com也会放行。攻击者如果能注册该子域名就绕过了域名限制。建议只放行必要域名并区分域名白名单和路径白名单。Domain 只控制主机路径应该交给后续的业务权限判断。推荐排查链路先看/check返回的reason。再看resolved_ips是否包含异常 IP。核对策略 YAML 中域名、CIDR 是否按预期加载。看 AI 应用端是否关闭了重定向。看 DNS 解析环境是否和 AI 运行环境一致。看同一个 URL 多次决策是否一致不一致优先怀疑 DNS 变化或策略未刷新。7. 生产环境落地清单7.1 学习环境和生产环境的差异维度原型实现生产环境策略存储本地 YAML配置中心或数据库DNS 解析系统默认可控 DNS 缓存防护边界单接口多实例 内部网关日志print / FastAPI 日志结构化日志 审计安全内网明文mTLS 或签名高可用单点多副本 限流策略刷新重启生效动态刷新7.2 生产环境建议默认拒绝白名单放行。没有明确需要的域名不要放行。禁止在 AI 应用侧跟随重定向所有跳转目标必须重新过守卫。对云元数据 IP 单独封禁即使is_private判断失败也要有一层兜底。决策日志至少包含请求 ID、URL、resolved_ips、reason、策略版本。对 DNS 解析做短 TTL 缓存避免每个请求都触发递归解析。规则变化要能回滚不然一次误配置会导致 AI Agent 大面积断网。上线前压测/check的 QPS确认 DNS 解析不会成为瓶颈。定期更新威胁情报域名和 IP 库。接入 LangChain、Spring AI 时不要把守卫逻辑放在模型 prompt 里而是放在工具执行层。7.3 可复用的发布前检查清单是否设置了allow_redirectsFalse。是否配置了至少一层 CIDR 黑名单。云元数据 IP 是否在拦截范围内。策略是默认拒绝还是默认允许。日志里能否看到每个请求的最终决策。是否有 DNS 缓存和失败降级策略。配置变更是否有审批和回滚步骤。重定向后的 URL 是否会被再次检查。是否对 AI Agent 按context应用不同规则。RequestGuard 的价值不在于把 AI 关进笼子而在于让 AI 的每一步外部访问都可以解释、可控制、可审计。在 Agent 越来常见的今天出站请求防护会和身份认证、数据脱敏一样成为 AI 应用上线前必须考虑的一环。如果项目还处于原型阶段先按这篇文章的最小链路跑通再逐步加入策略中心、DNS 固定和动态规则会比一开始就追求“全自动安全”更稳妥。
返回列表