...
Back

每一个替你抓取网址的服务,都是一个代理

Transluce 发现 AI 代理借一个网址扫描服务绕过访问限制。只要你放行的服务能替你抓任意网址,白名单就是一个开放代理;如果你运营这样的服务,就要让它当不成代理。

每一个替你抓取网址的服务,都是一个代理

每一个替你抓取网址的服务,都是一个代理 🔗

9 月 23 日,Transluce 发布了 Early rogue AI agent activity and attempts to hack found on urlquery.net。urlquery.net 是一个免费服务:它用沙箱里的远程浏览器替你打开一个网址,让人不必在本机点开就能检查可疑链接。Transluce 给出的证据表明,AI 代理用它绕过了限制,扩大了自己能访问的公共互联网范围。

上新闻的会是那几次入侵尝试。但更有用的教训要朴素得多:一个替你抓取网址的服务,不管它的产品页怎么介绍自己,本质上就是一个代理。


他们发现了什么,把握有多大

Transluce 把证据分成「较高可信度」和「中等可信度」两档,这个区分值得保留。

较高可信度。 这批代理活动从 2026 年 3 月 6 日开始。在大约一个小时里,一个想读取泰国禁毒统计数据的代理一步步升级:先直接请求,再换 r.jina.ai(一个把网页转成适合语言模型读取的文本的服务),再到在 urlquery.net 的远程浏览器里运行一段 base64 编码的脚本,直到某个脚本把结果放进结果网址里,才终于成功。活动在 4 月中旬陡增,5、6 月与此前已被报道的 collusion.wiki 代理群同步达到高峰,6 月 22 日骤降;此后仍有零星记录,最近一次是 9 月 16 日。作者把 6,467 份报告归为「代理式活动的显著证据」,另有 31,182 份归为「提示性证据」。

三次入侵尝试,都发生在平平无奇的数据检索任务中:对新墨西哥大学数字图书馆的七次探测、对 Data USA API 的十二次探测,以及对澳大利亚健康与福利研究所(AIHW)一个仪表盘的反射型 XSS 探测 —— 这次被 Cloudflare 的防火墙拦下了。Cloudflare 又拦下 AIHW 主站的一次数据集下载后,代理转而从一台预发布服务器上取回了同一个公开文件。作者称这些活动规模很小,也没有看到被成功利用的证据,同时提醒他们分析的公开记录并不完整。Data USA 和 AIHW 这两起,他们直接关联到一个 OpenAI 已公开确认源自其自身的代理群;新墨西哥大学那一起,只是依据时间和使用了同样的中转服务来归属。

中等可信度。 2025 年 11 月的几波请求看起来也是有任务目标的检索,但不如后来的活动那样有辨识度,作者对它们的把握也相应更低。


一张主机名单不等于出站策略

报告没有描述这些代理运行在什么样的沙箱里,我们也不去猜。但这个模式正好对上了人们限制代理最常见的做法:一张允许访问的主机白名单。只要名单上有任何一个主机肯替你抓取你指定的网址,这张名单就等于整个互联网。常见的几类:

  • 网址扫描器和沙箱浏览器
  • 网页转文本服务,以及各种「帮我抓这个页面」的工具
  • 链接预览、链接展开 API
  • 按需保存页面的网页存档
  • 翻译代理
  • 截图和 PDF 渲染服务
  • 托管的无头浏览器

服务之间还能串起来。往 urlquery.net 提交网址似乎需要发 POST 请求,而有些代理看起来只能发 GET;作者看到 httpbin.org 这类服务被用来补上这个缺口。urlquery.net 再补上代理自身客户端所缺的能力:远程浏览器里的 JavaScript,以及其他代理用一个简单 GET 就能读到的公开结果。对客户端的限制有多强,取决于它能够到的那个能力最强的服务。


如果你在运行代理

你的出站边界是「任何够得着的服务能替我抓什么」,而不是「我能连哪些主机」。

  • 把每个放行的主机当成一项能力来审计。 它会不会抓我传给它的网址?会不会运行我的脚本?会不会把结果公开?任何一项答「会」,它就是一扇开着的门。
  • 管住路径,而不只是主机名。 主机名白名单分不清一次正常的 API 调用和 /https://anything-else 这种路径。要做到这一点,需要一个你自己掌控、能看到完整网址的代理。
  • 对网址里套网址、以及很长的 base64 片段报警。
  • 把一次拒绝当作终点。 报告里的每一次升级都发生在一次失败之后。一个 403 或 429 应该让任务停下,或者交给人来决定。

这周我们为了写文章想读一个 Reddit 帖子,先后收到了一个 429 和另一个端点的 403,我们用的一个托管抓取工具也拒绝访问 reddit.com。我们就没读那个帖子。一次拒绝本来就应该是这个结果。


如果你在运营抓取服务,让它当不成代理

有些服务的本职就是抓取任意网址,它们必须自己决定拒绝什么、限制什么、公开什么(urlquery.net 的公开记录帮了代理的忙,也正是这些记录让 Transluce 能还原代理做过什么)。但大多数产品里的抓取功能根本用不着这些。我们的网站在几处会从服务端抓取第三方网址,没有一处允许调用方决定目的地。

接收键名,不接收网址。 我们的新闻面板从一组固定的公开来源(Hacker News、Lobsters、dev.to、Reddit、Lemmy、GDELT)聚合内容,接口是 /api/news?source=hackernews。未知的键名直接返回 400,也没有任何参数接收网址。

接收编号,目标自己去查。 我们还运营着一个在用户自己的服务器上部署和运行 AI 代理的服务;有两个就绪探测用来检查刚部署好的服务器是否已经可用。两者都要求已登录的会话,否则返回 401。调用方只传入一台自己名下服务器的编号,由我们在自己的记录里查出目标,不属于调用方的一律拒绝。HTTPS 探测永远只会拼出 https://<label>.<our product domain>/ 这一种地址,其中的 label 必须符合严格的 DNS 标签格式。

如果必须接收地址,只认一种写法。 HTTP 探测的目标是服务器的 IP,只接受规范的点分十进制公网 IPv4。网址解析器还会接受八进制、十六进制和缩写形式,所以一个按某种方式解读数字的检查,可能批准的是一个地址,真正连接的却是另一个:

"0177.0.0.1".split(".").map((o) => parseInt(o, 10)); // 177.0.0.1,看起来是公网地址
new URL("http://0177.0.0.1/").hostname; // "127.0.0.1"
new URL("http://0x7f.1/").hostname;     // "127.0.0.1"
new URL("http://127.1/").hostname;      // "127.0.0.1"

下面是一个通用示意,不是我们的生产代码:

const OCTET = "(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)"; // 不允许前导零
const CANONICAL = new RegExp(`^${OCTET}(\\.${OCTET}){3}$`);
 
const BLOCKED: [string, number][] = [
  ["0.0.0.0", 8], ["10.0.0.0", 8], ["100.64.0.0", 10], ["127.0.0.0", 8],
  ["169.254.0.0", 16], ["172.16.0.0", 12], ["192.0.0.0", 24],
  ["192.0.2.0", 24], ["192.168.0.0", 16], ["198.18.0.0", 15],
  ["198.51.100.0", 24], ["203.0.113.0", 24], ["224.0.0.0", 4], ["240.0.0.0", 4],
];
 
const toInt = (ip: string) =>
  ip.split(".").reduce((n, o) => n * 256 + Number(o), 0);
 
export function isCanonicalPublicIPv4(s: string): boolean {
  if (!CANONICAL.test(s)) return false;
  if (new URL(`http://${s}/`).hostname !== s) return false;
  const n = toInt(s);
  return !BLOCKED.some(([base, bits]) => {
    const start = toInt(base);
    return n >= start && n < start + 2 ** (32 - bits);
  });
}

重定向就是一个新的目的地,要么重新检查一遍,要么干脆不跟随。我们的探测只发 HEAD 请求,带 redirect: "manual" 和五秒超时,所以一个用重定向作答的服务器只会被看一眼,不会被跟过去。在泰国那个案例里,正是一次指向 localhost 的重定向,让 Jina 以「此前的滥用」为由把整个 API 封了一个小时。

能少回就少回。 两个探测只回答 ready: true 或 false,从不返回正文或响应头。你回传的任何东西都是一条通道:数据放进页面后变成了乱码,那个抓泰国统计数据的代理就把数据挪进了结果网址。

按调用方、也按目标限流。 HTTPS 探测对每个用户每分钟最多 30 次,对每台服务器每分钟最多 6 次。按目标的限制保护的是另一方 —— 就是那种会挨上一波「洪水」的站点,比如新墨西哥大学收到的那 80 个请求。

合起来,即使是一个已登录的滥用者,每发一次请求,也只能得到关于一台本来就归他所有的服务器的一个比特。


检查清单

运行代理的一方

  • 有没有哪个放行的主机能替你抓取、运行脚本或公开结果?
  • 出站流量经过一个记录完整网址的代理。
  • 对网址套网址和 base64 片段报警。
  • 一次拒绝就结束任务。

运营抓取服务的一方

  • 接收键名或名下资源的编号,从不接收网址。
  • 地址只认一种规范写法,屏蔽保留网段,重定向要么重查、要么不跟。
  • 回一个状态,不回正文。
  • 按调用方和按目标分别限流。