staging 环境的旧价格也被引用了:测试子域名暴露与爬虫封堵的排查复盘

2026-10-02 01:18:51 0 次浏览
GEOAI搜索网站安全robots.txtNginx外贸独立站

适用读者:负责外贸独立站日常维护的后端与运维,手上同时管着一套测试环境。如果你的 staging 域名从公网能直接打开,而最近有客户拿着 AI 回答里的旧报价来对质,这篇复盘的排查路径和封堵方案可以直接对照着做。

客户发来的截图我到现在还留着:他在 AI 助手里问自家主打砂光机的出厂价,回答给的是三年前的价格,比官网挂牌价低了将近两成,连一款两年前就停产的老型号都写得有鼻子有眼。

这是上个月 14 号下午的事。他第一反应是官网被改了,排查到半夜,官网的版本记录干干净净,一个字都没动过。第二天他找到我们这边,我先把这事儿记成了工单,编号 JIRA-4471。

引用源对上了:不是官网,是 staging

第二天上午拉电话会,先不猜原因,照着截图复现。同样的提问换几种问法各问一遍,让 AI 给出引用链接。三轮提问里有两轮的引用列表出现了 staging.mach-ex.com 下的产品页——这是他们给测试环境起的子域名,主站是 www.mach-ex.com。

服务器机房封堵爬虫的主题插画

打开那条引用的快照,价格表、参数表、连「2023 年季节性促销」的横幅都原样躺在那里。也就是说,AI 引擎抓的语料里混进了测试站的内容,回答时把新旧两套数据一起端了出来。

有个细节当时差点漏掉:staging 首页的 title 和主站几乎一样,正文里没有任何「测试环境勿引用」的标记。对生成式引擎来说,这两个站从内容上看不出谁正式谁临时。做生成式引擎优化(Generative Engine Optimization, GEO)时大家盯着官网内容怎么被引用,却很少有人检查测试站会不会被当成第二个官网抓走。

翻访问日志:AI 爬虫的 UA 早就来过了

接下来去翻 staging 的 Nginx 访问日志,按 UA 过滤,专门盯那几家会抓网页语料的爬虫。

结果比预想的严重。GPTBot、ClaudeBot、PerplexityBot、CCBot 这几个 UA,从去年 11 月开始就有稳定的抓取记录,频率不算高,每天几十次,但产品页和价格页全被打过。日志里最早那条是去年 11 月 6 号凌晨 3 点 17 分,抓的正好是价格表页。

简单算了一下量级:八个月里这几家爬虫合计打了 staging 一万四千多次请求,产品相关路径占了六成。测试站不是没人看,是被当作正式内容持续抓了几个月。

整个排查的动作顺序画出来是这样的:

flowchart TD
    A["客户反馈 AI 报价与官网不一致"] --> B["复现提问并查看引用来源"]
    B --> C["确认引用源指向 staging 页面"]
    C --> D["检索访问日志里的爬虫 UA"]
    D --> E["统计抓取量与起始时间"]
    E --> F["部署三层封堵方案"]
    F --> G["DNS 与证书层面收敛暴露面"]

排查过程整理成时间线,方便对照自己的环境检查:

时间点 事件
去年 11 月初 staging 迁到新服务器,签发了公网可信证书
去年 11 月 6 日起 AI 爬虫 UA 开始出现在 staging 访问日志
上个月 14 日 客户反馈 AI 报价与官网不一致
上个月 15 日 对比确认引用源为 staging 页面
上个月 16 日 三层封堵上线,同步做 DNS 与证书收敛

AI 爬虫凭什么找到你的测试域名:发现机制剖析

这是整件事里最值得展开的部分。很多人觉得测试域名没对外宣传过,爬虫怎么会知道。实际上爬虫拿到一个子域名的路子比想象中多。

途径一,外链。 测试链接只要被贴出去过一次——工单系统里贴给客户验收、供应商邮件里的演示链接、甚至官网页脚不小心带上——就有可能被公开页面收录。爬虫顺着链接爬,staging 页面就成了抓取队列里一个普通的 URL,和其他页面没有区别。

途径二,证书透明度日志。 这是这份复盘里最关键的机制。按 CA/浏览器论坛的 Baseline Requirements,凡签发公网可信的 TLS 证书,都必须提交到公开的证书透明度(Certificate Transparency, CT)日志。换句话说,你们给 staging 子域名签一张 Let's Encrypt 证书的那一刻,这个子域名就被永久公示了,任何人去 crt.sh 输入主域名,底下所有子域名一栏无余。爬虫运营方只需要定期拉取 CT log 的新增条目,就能批量拿到全网新出现的子域名。他们 11 月初迁服务器时新签了证书,11 月 6 号爬虫就来了,时间对得严丝合缝。

途径三,DNS 字典爆破。 staging、test、dev、uat、beta、pre 这些前缀是子域名枚举工具字典里的高频词。被动收集不够时,主动把常见前缀拼一遍再验证解析,也能把测试域名翻出来。

flowchart LR
    S["AI 爬虫发现 staging 子域名"] --> P1["公开页面上的测试外链"]
    S --> P2["证书透明度日志里的签发记录"]
    S --> P3["DNS 字典爆破枚举常见前缀"]
    P1 --> T["子域名进入抓取队列"]
    P2 --> T
    P3 --> T

搞清楚发现机制,封堵思路就顺了:外链收干净,证书别再进 CT log,子域名要么挡住要么换个猜不到的名字。

三层封堵:进不来、不敢抓、抓了也没用

光靠一层手段总有漏网的情况,我们上了三层,各管一段。

第一层是 HTTP Basic Auth。测试环境本来就该有访问门槛,整站加认证之后,爬虫没有凭据,请求直接吃 401。

第二层在 Nginx 的 UA 和 Host 上做拦截。主流 AI 爬虫的 UA 直接返回 403;再配一个 default_server 兜底,凡是不带正确 Host 的握手请求一律拒绝。

第三层是 robots.txt 加 noindex 双保险。有人会说前面都 403 了还要这个干嘛——要的。一是部分合规爬虫会先读 robots 再决定抓不抓,提前声明省得浪费双方流量;二是 noindex 头对已经被抓走的存量快照起清理作用,搜索引擎和部分 AI 引擎会逐步把已收录的测试页摘掉。

三层方案各自的分工和局限列一下:

层级 手段 挡住什么 局限
认证层 HTTP Basic Auth 一切无凭据访问,含全部爬虫 覆盖不了已被抓走的存量数据
边缘层 Nginx UA 拦截与 Host 兜底 已知 AI 爬虫 UA、伪造 Host UA 可以伪造,挡君子不挡小人
声明层 robots.txt 与 X-Robots-Tag 合规爬虫的未来抓取与存量收录 对无视声明的爬虫没有强制力

Nginx 配置如下(环境:Nginx 1.24,Ubuntu 22.04,htpasswd 用 apache2-utils 生成):

# 环境:Nginx 1.24,Ubuntu 22.04
# htpasswd 文件用 apache2-utils 的 htpasswd 命令生成
server {
    listen 443 ssl;
    # 该域名仅内网 DNS 能解析,公网已停记录
    server_name staging.mach-ex.com;

    # 第一层:整站 Basic Auth,无凭据一律 401
    auth_basic "staging only";
    auth_basic_user_file /etc/nginx/staging.htpasswd;

    # 第二层:命中主流 AI 爬虫 UA 直接 403
    if ($http_user_agent ~* (GPTBot|ClaudeBot|PerplexityBot|CCBot)) {
        return 403;
    }

    # 第三层:全站响应带 noindex,清理存量收录
    add_header X-Robots-Tag "noindex, nofollow" always;
}

# 兜底:其他 Host 的 TLS 握手直接拒绝
server {
    listen 443 ssl default_server;
    ssl_reject_handshake on;
}

封堵上线后写了个校验脚本,每次改配置跑一遍(环境:Python 3.10+,仅标准库,内网执行):

# 环境:Python 3.10+,仅标准库
# 只跑在内网机器上,不要放到公网执行
import urllib.request, urllib.error

# 伪装 AI 爬虫 UA,期望被 403 拒绝
UA_BOT = "GPTBot"
# 模拟普通浏览器,期望被 Basic Auth 挡成 401
UA_HUMAN = "Mozilla/5.0"

def probe(url, ua):
    # 带自定义 UA 发请求,返回状态码和响应头
    req = urllib.request.Request(url, headers={"User-Agent": ua})
    try:
        r = urllib.request.urlopen(req, timeout=10)
        return r.status, dict(r.headers)
    except urllib.error.HTTPError as e:
        # 4xx 走这里,状态码照样要拿回来
        return e.code, dict(e.headers)

# 校验一:AI 爬虫 UA 必须被边缘层拒绝
code, _ = probe("https://staging.mach-ex.com/", UA_BOT)
assert code == 403, f"AI 爬虫未拦截 {code}"

# 校验二:浏览器请求被 401,且带 noindex 头
code, h = probe("https://staging.mach-ex.com/", UA_HUMAN)
assert code == 401 and "noindex" in h.get("X-Robots-Tag", "")
print("三层封堵校验全部通过")

DNS 和证书层面的收敛

三层封堵解决的是「进来了怎么办」,暴露面本身还得收,这部分分四步做。

先停掉 staging 的公网 DNS 记录,改成内网解析,办公室和 VPN 用户走内部 DNS 才能解析到测试站。公网上查不到这个域名,DNS 爆破这条途径就断了。

证书这块,不再给 staging 签公网可信证书,改用内部 CA 自签,浏览器手动信任一次即可。代价是有警告提示,但测试环境能接受。不进 CT log,等于从公示名单上把名字划掉了。

防火墙加白名单,443 端口只放行办公室出口 IP 和 VPN 网段,跟 Basic Auth 双保险。

旧的 staging 域名已经进过 CT log,删不掉,那就让它废掉:解析到主站做 301,或者干脆停止解析。配合 noindex 头,存量的收录和快照在之后几周陆续清掉了。顺手把 robots.txt 也补上,虽然此时更像是一种姿态:

# staging 全站对爬虫关闭,配合 noindex 头使用
User-agent: *
Disallow: /

封堵前后的对照:

观测项 封堵前 封堵后
公网可达性 无需凭据直接打开 401 拦截,仅内网可访问
AI 爬虫抓取 多家 UA 稳定抓取八个月 UA 层 403,日志归零
证书透明度 子域名在 CT log 公示 改自签,不再新增公示记录
搜索与语料收录 测试页可被收录引用 noindex 生效,存量逐步清理
报价泄露风险 新旧价格混入 AI 回答 引用源仅剩正式官网

复盘完想提醒的几个误区

误区一是把 robots.txt 当封堵手段。它只是单方面声明,没有强制力,无视它的爬虫照样抓。它的正确位置是和认证、拦截配合,声明「这里不该来」,而不是指望它自己挡人。

误区二是觉得测试站没做过外链就没人知道。CT log 和 DNS 爆破都不需要你主动留链接,只要签过公网证书、用过常见前缀,被翻出来是迟早的事。做完这单之后我给自己定了个习惯:新项目初始化时就去 crt.sh 搜一遍自家主域名,看看有哪些子域名已经躺在公示名单里。

往前看,GEO 这个方向被越来越多外贸团队重视,大家琢磨的是怎么让官网内容更容易被 AI 引擎引用。但 GEO 有个前提常被忽略:引用源得干净。测试站、过期落地页、废弃域名里的旧数据一样会被卷进语料,而且它们往往连基本的防护都没有,被引用的概率反而比主站高。做 GEO 之前先把自家的门关好,不然优化得再用心,AI 回答里照样可能蹦出一条三年前的价格。

你们的环境里还有多少个能从公网直接打开的测试域名?建议今天就查一遍,CT log 上搜一下自家主域名,结果可能会吓你一跳。踩过类似坑的欢迎评论区聊聊,尤其是 noindex 清理存量快照花了多久。

参考与延伸

  • Google 搜索中心关于阻止索引与 X-Robots-Tag 的官方文档:https://developers.google.com/search/docs/crawling-indexing/block-indexing
  • robots.txt 协议官方说明:https://developers.google.com/search/docs/crawling-indexing/robots/intro
  • Nginx ngx_http_auth_basic_module 模块文档:https://nginx.org/en/docs/http/ngx_http_auth_basic_module.html
  • 证书透明度日志公开查询入口:https://crt.sh/

staging 环境、测试子域名、AI 爬虫封堵、robots.txt、Nginx 配置、外贸独立站、GEO

🤖
本内容由 AI 辅助生成,经人工校对审核;部分素材、资料来源于公开网络,仅作个人观点分享与交流使用,无任何商业侵权意图。若内容、图片、文字涉及您的合法著作权、版权权益,请联系本人,核实后将第一时间删除、修改相关内容。