# Agent 的 URL 抓取工具，就是通往客户内网的代理

> 黑名单挡不住 SSRF，只有固定解析 IP、经出口代理转发的抓取路径才能把门关上。

原文: https://fdetimes.net/zh/guides/agent-url-fetch-ssrf-egress-proxy/

设想你在客户现场的第三周。内部客服 Agent 运行顺利，用户提了一个小需求：“贴一个链接，Agent 读完给出摘要”。你用大约二十行代码加了一个 `fetch_url` 工具，演示很顺利，皆大欢喜。

但这个 Agent 运行在客户虚拟私有云（VPC）内的一台虚拟机上。这台机器能看到内部 wiki、管理后台、数据库，以及云元数据服务。

现在，任何能在聊天框里输入内容的人，或者能在 Agent 将要读取的网页里埋下指令的人，都可以从防火墙内部发出请求。赋予他们这种能力的，正是那二十行代码。

这个漏洞叫 SSRF。前线部署工程师（FDE）几乎一定会碰上它，因为这份工作就是把 Agent 放到离客户真实数据很近的地方。下文介绍攻击原理、安全的抓取路径应该是什么样，以及最常见的错误。

## 为什么 Agent 会让 SSRF 更严重？

在传统 Web 应用中，攻击者得先找到一个暴露在外的 URL 参数。而在 Agent 中，URL 由语言模型选择，模型会听从文本，包括别人埋下的文本。

模型上下文协议（Model Context Protocol，MCP）的安全最佳实践文档描述的正是这种场景。文中列出的后果里有一句话，每个 FDE 都应该牢记：服务端的 MCP 客户端会变成一个绕过网络边界控制的代理。

这种事已经真实发生过。CraftedSignal 在 2026 年 5 月发布的安全公告指出，auth-fetch-mcp 包的抓取工具存在 SSRF 漏洞，攻击者可以迫使服务器加载内部资源、云元数据端点或回环地址。

**要点:** 在客户网络内运行的 URL 抓取工具，是一扇谁都能敲的门。

## 最有价值的目标就在机器本身

在云上，运行 Agent 的机器通常绑定了一个 IAM 角色。谁拿到 IMDSv2 会话令牌，谁就能用它从元数据服务读取该角色的临时凭证。到了这一步，对方已经不需要 Agent 了：他们握有这台机器的权限，可以直接调用客户的 API。

AWS 设计 IMDSv2，是为了在防火墙大开、反向代理配置错误或应用存在 SSRF 漏洞时多一层防御。关键机制是：携带令牌的响应在离开实例前，TTL 会被降为 0，因此只有实例本身能收到令牌。根据 AWS 文档，这个响应的默认跳数限制为 1。

当 Agent 运行在实例上的容器或沙箱里时，这个数字决定了沙箱能否拿到令牌。AWS 解释说，进入容器算多一跳，所以跳数限制为 1 时，容器可能根本收不到响应。

结果是：默认值 1 会让距离实例一跳的容器拿不到令牌；而调到 2（AWS 本身就建议容器环境这样做）则会让沙箱接触到机器的凭证。

如果有人为了“让它跑起来”调高了跳数限制，要问清原因。AWS 也给出了替代方案：把 Region 等配置直接传入容器，而不是向容器开放元数据服务。

## 安全的抓取路径是什么样

对客户提出的第一个问题永远是：Agent 真的需要读取*任意* URL 吗？如果用户只会贴公司内部的 Confluence 和 GitHub 链接，就用白名单。但仅靠域名白名单挡不住 DNS 重绑定。

原因在于一个时间差，MCP 文档称之为 DNS TOCTOU（检查时间与使用时间不一致）。检查时，域名指向公网 IP；等到连接时，HTTP 库可能重新解析，拿到一个内网 IP。

OWASP 提醒，在校验和连接之间如果发生第二次未经检查的解析，就能绕过这些检查。

2026 年关于 MCP Atlassian 的安全公告就是真实案例：校验通过后，`requests.get` 又解析了一次 DNS，拿到 169.254.169.254，也就是元数据地址。MCP 建议的修复方法是固定解析结果：只解析一次，检查这个 IP，然后只连接这个 IP。

下面是检查逻辑的核心，用 Python 编写：

```python
import ipaddress, socket
from urllib.parse import urlparse

class BlockedDestination(Exception):
    pass

def resolve_and_check(url: str):
    parsed = urlparse(url)
    if parsed.scheme not in ("http", "https"):
        raise BlockedDestination(f"不允许的协议 {parsed.scheme}")
    host = parsed.hostname
    ips = {ipaddress.ip_address(info[4][0])
           for info in socket.getaddrinfo(host, None)}
    for ip in ips:
        if ip.version == 6 and ip.ipv4_mapped:
            ip = ip.ipv4_mapped
        if (ip.is_private or ip.is_loopback or ip.is_link_local
                or ip.is_reserved or ip.is_multicast or ip.is_unspecified):
            raise BlockedDestination(f"{host} -> {ip}")
    return host, sorted(ips, key=str)[0]  # 连接时要固定使用的 IP
```

有三个细节值得注意。这个函数使用标准库，而不是手动切分 IP 字符串，因为 MCP 文档警告：攻击者会用八进制、十六进制或 IPv4 映射的 IPv6 格式书写 IP，绕过手写的解析器。

只要域名的*任意一个* IP 落在私有、回环或链路本地网段，函数就拒绝。函数还会返回这个 IP，让下一步直接连接它，不再重新解析。

难点恰恰在于 URL 为 HTTPS 时的“直接连接 IP”这一步。你向固定的 IP 打开 socket，但 TLS 握手中的 SNI、证书校验和 `Host` 头仍然必须使用原始域名，否则服务器会返回错误的页面，或者证书会被拒绝。

OWASP 指出，并非每个 SDK 都内置了这类检查，开发者得自己处理边界情况。

datamodel-code-generator 的安全公告说明了代价：防护逻辑只检查了一次 IP，随后让 httpx 自行解析 DNS，于是没有任何机制把连接绑定到通过检查的那个 IP 上。

因此，把上面的函数当作应用内的前置过滤，把固定 IP 的工作交给下文介绍的出口代理。

接下来是重定向循环。关闭 HTTP 库的自动跟随重定向，例如设置 `allow_redirects=False`。每遇到一个 3xx 响应，就取出 `Location` 头，再次调用 `resolve_and_check`，并在几跳之后停止，因为第一个 URL 可能干干净净，第三个却指向内网。

## 代码写得再好，也不是主要防线

上面的函数是应用层防护。只有当每一条发送请求的路径都经过它时，它才有效，包括浏览器工具、第三方 MCP 服务器和 webhook。只要有一条路径忘了调用，就足以被突破。

所以 MCP 文档建议服务端部署使用 Smokescreen 这类出口代理（一种专为防范 SSRF 设计的代理），而不是逐个修补适配器。代理自己解析域名、检查 IP 并建立连接，于是检查和使用之间的时间差在一个地方被堵上，而不是分散在每个工具里。

在客户现场，合理的做法是：把 Agent 放进一个子网，它通往互联网的唯一路由是经过代理，没有通往数据库的路由，强制启用 IMDSv2，并让跳数限制保持在沙箱拿不到令牌的水平。

向客户安全团队汇报时，画出三层：代码里的白名单和 IP 检查，中间的出口代理，底层的网络隔离和元数据配置。一层失守，其他层还能顶住。这样一张图，通常比一句“我们会校验 URL”更快通过安全评审。

## 常见错误

最常见的错误是写黑名单，比如“拦截 `localhost` 和 `127.` 这两个字符串”。这些地址有很多种写法，而这正是 MCP 文档说要避免的手写检查。第二个错误是只在开头检查一次 URL，然后任由 HTTP 库自行跟随重定向、重新解析 DNS。

第三个错误是只考虑用户输入恶意 URL。在 Agent 场景里，URL 可能藏在邮件、工单或 Agent 被要求阅读的网页里。第四个错误是以为云平台会处理好元数据，而一次“为了让容器跑起来”的跳数限制修改，就可能重新打开这扇门。

## 一张图，既能修漏洞，也能帮你拿 offer

本周练习：拿出你最近一次部署的网络图，标出运行 Agent 的机器，然后画出这台机器能发送请求的每一个箭头。凡是你无法向客户解释清楚的箭头，都是下次演示前要完成的工作。

这张图，加上你砍掉的箭头清单，也是求职时最有力的证明。如果某个岗位提到在客户基础设施中部署，或与安全团队协作，面试时就讲这个故事：哪台机器、哪个箭头、哪一层把它挡住了。

在简历上，一句“为运行在客户网络中的 Agent 设计出口策略和 SSRF 防护，通过安全评审”，比三行框架列表更有说服力。招 FDE 的人需要知道，你能看到别人漏掉的箭头。

**本周动手试试:**

- 列出你正在部署的 Agent 中所有能发送网络请求的工具：fetch、浏览器、webhook、第三方 MCP 服务器。逐一记下请求从哪台机器、经由哪个网络发出。
- 询问客户的基础设施团队：运行 Agent 的实例是否强制启用 IMDSv2，跳数限制设为多少，原因是什么。
- 为抓取工具写一个测试：一个会重定向到回环地址的公网 URL。只有工具在重定向这一步拒绝请求，测试才算通过。

## 信源

- [Server Side Request Forgery Prevention - OWASP Cheat Sheet Series](https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html)

- [Security Best Practices - Model Context Protocol](https://modelcontextprotocol.io/specification/draft/basic/security_best_practices)

- [Use the Instance Metadata Service to access instance metadata](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html)

- [Access instance metadata for an EC2 instance](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instancedata-data-retrieval.html)

- [Add defense in depth against open firewalls, reverse proxies, and SSRF vulnerabilities with enhancements to the EC2 Instance Metadata Service](https://aws.amazon.com/blogs/security/defense-in-depth-open-firewalls-reverse-proxies-ssrf-vulnerabilities-ec2-instance-metadata-service/)

- [datamodel-code-generator vulnerable to SSRF protection bypass via DNS rebinding](https://corgea.com/advisories/vulnerabilities/CVE-2026-55391)

- [MCP Atlassian: SSRF via DNS Rebinding in Header-Based Authentication Flow](https://corgea.com/advisories/vulnerabilities/CVE-2026-77265)

- [auth-fetch-mcp SSRF brief (CraftedSignal)](https://feed.craftedsignal.io/briefs/2026-05-auth-fetch-mcp-ssrf/)
