被冻结的 User-Agent 版本号可能已经不真实。这篇文章说明网站如何用特征检测反推你真实的浏览器引擎版本,以及这种检测方法本身的边界与局限。
你的 User-Agent 字符串写着 Chrome/143.0.0.0。这是真的吗?有两件事让你没法单凭这串文字判断。第一,User-Agent Reduction 把主版本号之后的所有数字都冻结成了写死的 .0.0.0,唯一会随每次更新变化的那部分版本信息就此消失。第二——也是更关键的一点——整串 UA 本来就是浏览器对自己的声明,一个扩展或一套自动化框架用一行代码就能改写它。一个真正需要知道你浏览器引擎版本的网站,必须问一个完全不同的问题:不是"浏览器自称是什么",而是"它实际上具备哪些能力"。这就是基于特征检测的版本推断,也正是 BrowserInsight 自家浏览器内核检测背后所用的技术。
核心要点
- 特征检测版本推断的做法是:测试某个在某个引擎版本中已经上线的能力是否存在,同时测试下一个版本才会上线的能力是否缺失,从而把真实引擎锁定在一个狭窄的区间内。
- 结果永远是一个区间,而不是精确的版本号——功能是分批上线的,有些还带实验开关或被向后移植(backport)到旧版本,所以这项技术能锁定一个版本区间,却锁不到
chrome://version里那串四段式的完整版本号。 - 每一套这样的检测工具都有一个内建的天花板:它只能测试建表时就已经存在的功能,一旦你的浏览器通过了表里的每一项测试,报告就只能说"至少是这个版本"。
- 两个读数的不一致是有方向的:特征检测出的版本低于 UA 声称的版本,通常只说明检测表已经过时;而特征检测出的版本高于 UA 声称的版本,才是真正指向伪造的方向——浏览器可以隐瞒版本,却没法表演出它并不具备的能力。
- 同样的探测方式在版本号出现之前,就已经能把 Blink、Gecko、WebKit 区分开——因为这三种引擎的功能集合本身就是各自独立演化的。
检测机制:用特征探测锁定版本区间
浏览器引擎的每一次发布,都会带来一批可被观测的新 Web 平台能力——一个 CSS 属性、一个 JavaScript 方法、一个 DOM 接口。这些能力中的大多数都可以被页面直接测试,不需要弹权限提示,也不需要用户交互:检查某个属性是否存在、某个构造函数是否已定义、某个 CSS 值是否被接受。
一个最简单的存在性探测大致是这样:
// `Intl.Locale.prototype.variants` 在 Chromium 149 上线。
const has149 = 'variants' in Intl.Locale.prototype;
// CSS 的 `text-fit` 属性在 Chromium 150 上线。
const has150 = CSS.supports('text-fit', 'initial');
// has149 && !has150 -> 这个引擎就是 Chromium 149。
这两行代码都不会下载任何东西,也不会碰网络——它们只是向引擎询问一个关于自身能力的是/否问题。关键在于成对地选择探测项:一个是已知在某个版本上线的功能,另一个是已知在这个版本还不存在、但下一个版本会上线的功能。如果第一项通过、第二项不通过,浏览器就被锁定在"是这个版本,但还不到下一个版本"的区间里。把足够多这样的探测对按已知发布版本表跑一遍,就能沿着阶梯往上走,找到浏览器完全满足的最高版本——这就是真实引擎版本的下限估计,完全不依赖 UA 字符串声称了什么。
这正是 BrowserInsight 自家浏览器内核检测背后的做法:它会走一张 Chromium 版本表(从 Chromium 79 起)和另一张独立的 Firefox 版本表,逐一测试每个版本已知的功能差异,并根据"通过"链条在哪里中断来报告推断出的版本——同时把 User-Agent 声称的版本也一并展示,方便你直接看出两者是否一致。上面那两项探测就是直接取自这张表。
为什么答案是一个区间,而不是一个精确数字
特征检测矩阵永远无法像 chrome://version 那样解析出精确的构建号——它只能锁定一个里程碑版本,即便如此也有几点必须说在前面:
- 功能是成批上线的,而不是一个版本一个,所以单独一项通过的测试很少能单独锁定某一个版本——往往需要几项确认性测试和排除性测试一起,才能收窄区间。
- 部分功能带实验开关,在正式对所有人开放前,会在一个或多个版本里隐藏在实验标志后面,所以处于某个版本、但还没被灰度到该功能的一小部分用户,可能会在"本该通过"的测试上失败。
- 向后移植确实会发生:一个安全相关的功能有时会在正常发布节奏之外,被塞进某个更旧的稳定版渠道,这会让某个旧版本在这一项测试上看起来比实际更新。
这些都不会让这项技术失效——只是意味着诚实的输出应该是"这个浏览器至少是版本 N",而不是"这个浏览器精确等于版本 N.N.N.N"。任何工具,包括本站的内核检测在内,如果把特征检测包装成能解析出精确补丁版本号,那都是在夸大这套方法实际能做到的事。
为什么每一套检测工具都有天花板
一张特征检测表本质上是一份快照:它只能测试建表当时已经存在、且已知的能力。一旦某个浏览器通过了表里的每一项测试——因为它确实比这张表覆盖的最新版本还要新——就没有任何测试项能再拦住它,诚实的报告只能是"这个版本或更新",而不是一个精确数字。这不是某个具体实现特有的 bug,而是这套方法本身结构性的局限,这一点同样适用于 BrowserInsight 自己的内核检测。检测表需要随着引擎不断发布新版本、以及新的可区分特征不断出现而定期更新——Chrome 自己的功能路线图和它的已上线功能列表是官方记录这些信息最主要的公开来源,而这正是维护这类检测表的人必须持续跟踪的内容。caniuse.com 则是核对单个功能在各引擎间支持时间线的一个有用交叉参照,这也是为什么维护者通常不会只依赖某一家浏览器厂商自己发布的更新日志。
不一致该往哪个方向读
有了两个读数,下一步自然是拿来对比。但两种不一致的价值并不对等,把它们当成对称的,正是一个版本检测工具开始把普通用户误判成伪造者的原因。
- 特征检测出的版本低于 UA 声称的版本,通常根本不是谎言。这只是上一节说的天花板在现实中的表现:浏览器比检测表更新,通过了表里的每一项测试,于是被报告在表里最高的一档上。BrowserInsight 的浏览器内核检测有意不为这种情况给出伪造判定——只要检测表没有在新版本发布的同一周就跟上,这条判定就会对每一个把浏览器升到最新的访客误报。
- 特征检测出的版本高于 UA 声称的版本,才是真正可疑的方向。浏览器可以选择不声明自己的版本,却没法表演出它并不具备的能力。只存在于更高版本的功能,配上一个声称更低版本的 UA,意味着那串字符串被改写过,而底下的引擎仍在说实话。
还有第三种模式完全不涉及 UA:看通过/不通过的结果是不是一道干净的阶梯。真实的引擎会通过直到自己版本为止的每一档,并在更高的档位上全部失败,中间不留空缺。低档没通过、高档反而通过了,这根本不是任何一个版本,而是真实构建不会呈现的形状——它指向的是一个被打过补丁或被模拟出来的环境,而不是一个过时的浏览器。由于这项判据只把各个探测结果互相比对,即便 UA 字符串缺失、笼统或纯属乱码,它依然成立。
跨引擎:同一种技术,在版本之前先分辨引擎
特征检测的作用不只是锁定版本号——它同样是页面在版本号出现之前,先把不同引擎区分开的方式。Blink、Gecko、WebKit 各自何时、以何种顺序上线实验性和新标准化的功能,一直存在分歧,这种分歧早在它们各自到达当前版本之前就已经开始。一个存在于 Blink 但不存在于 Gecko 的功能,无论当前跑的是哪一个具体的 Chromium 或 Firefox 版本,都足以把两者区分开来。想了解这三种引擎为何会以这种方式各自分化的更深背景,可以看浏览器引擎详解;想弄清这项技术所探测的渲染引擎,和随之打包的独立 JavaScript 引擎之间的区别,可以看渲染引擎与 JavaScript 引擎。
同样值得说清楚的是,这项技术检测到的到底是什么。Chrome、Edge、Brave、Opera 以及其他所有基于 Chromium 的浏览器,共享同一个底层 Blink 引擎,所以特征检测探测报告的是这个共享引擎的版本——"Chromium 143"——而不是包裹在外层的品牌产品。Chromium 与 Chrome 详细解释了这种区分为何存在、为何不是 bug,为什么浏览器都用 Chromium 则解释了为什么这么多互不相关的浏览器厂商最终都选择共享同一个引擎。
为什么这项技术变得更有用,而不是更没用
特征检测不是一个曾经管用、后来被更好的方案取代的权宜之计——随着其他版本信号的变化,它反而变得更有价值,而不是更没有价值:
- **User-Agent 字符串曾经会随真实更新而变化。**在 User-Agent Reduction 之前,浏览器的 UA 会报告一个接近真实补丁版本的数字,所以那时用特征反推版本更多只是个技术趣闻。现在,UA 里的版本号被设计成固定停在
.0.0.0,两次补丁更新之间根本不会变化——它本来就该看起来是静态的,而且对每一个保持最新的安装来说都确实如此。特征检测是剩下的、唯一没有被清零的版本信号。 - User-Agent Client Hints,具体来说是
Sec-CH-UA-Full-Version-List请求头和与之配套的navigator.userAgentDataAPI,重新带回了精确的版本号——但这只是浏览器对同一个自我声明事实的第三种表述方式,被放在一个需要显式请求才会触发的机制后面,而不是默认广播出去。这确实是一条有用、信息更详细的通道,但它同样可以被伪造 UA 的同一套工具伪造,因为它和 UA 一样,都来自浏览器的自我报告。 - **多个读数之间的不一致,现在是一个真正的信号。**当你有了三个相对独立的来源——冻结的 UA、Client Hints 声称的版本、以及特征检测出的引擎版本——一个在两条自我声明渠道上都宣称同一件事、行为表现却完全是另一个引擎版本的浏览器,正是检测 User-Agent 伪造一文中讨论的那种矛盾。而特征检测是这三者中唯一一个浏览器无法单方面自行宣称的读数。
在自己的浏览器上验证一下
BrowserInsight 的浏览器内核检测会立即在你的浏览器上跑完这套探测:它把特征检测出的引擎版本和 User-Agent 声称的版本并排展示,给出两者是否一致的判定,并逐个版本列出完整的通过/不通过结果,让你看清自己的阶梯停在哪一档。
如果你想先亲眼看看这项技术的效果,可以打开开发者工具运行 CSS.supports('text-fit', 'initial')。返回 true 说明你跑的是 Chromium 150 或更新的版本;返回 false 则说明你要么低于这个版本,要么根本不是这个引擎——无论哪种,这都是直接从引擎上读到的一级台阶,完全不经过 User-Agent。
常见问题
特征检测能查出我 Chrome 的精确构建号吗?
不能。它能把你的引擎锁定到某个里程碑版本——比如"至少是 Chromium 149,但还没到 150"——但 chrome://version 里那些尾部的构建号和补丁号,它始终够不着。功能是按版本发布的,不是按补丁发布的,所以补丁级别的精度从原理上就不是这项技术能提供的。
Firefox 和 Safari 也适用这种方法吗?
区间锁定的逻辑本身与引擎无关,但功能对照表却是针对每种引擎单独构建和维护的。BrowserInsight 的内核检测跑的是一张 Chromium(Blink)版本表和一张 Firefox(Gecko)版本表;WebKit 的版本编号方式噪声更大,本站目前还没有对应的检测矩阵,不过同样的引擎识别步骤仍然适用于它。
如果一个浏览器在表里的每一项测试都没通过,是不是说明它是个很旧的浏览器?
不一定——也可能是这个浏览器比表里已知的最新版本还要新,所以表里没有任何一项能把它和这个上限区分开来。全部通过和全部不通过,在一张过时的表面前看起来是一样的;只有具体的通过/不通过模式(而不是一边倒地全过或全不过),才能告诉你自己究竟处在这张表天花板的哪一侧。
为什么不直接信任 User-Agent Client Hints 给出的版本号?
Client Hints 确实给出了一个精确的版本号,但这依然是浏览器通过另一个渠道对自身的一种陈述——而不是被独立验证过的行为表现。相比之下,特征检测更难被令人信服地伪造,因为要伪造它,浏览器就得真正实现(或足够逼真地模拟)表里直到所声称版本为止的每一项功能,而不只是返回一串不同的字符串。
结语
特征检测探测回答了一个被冻结的 User-Agent 字符串已经无法诚实回答的问题:这个浏览器根据它实际能做到的事、而不是它自称的身份,究竟运行在哪个引擎版本上。答案会是一个有天花板的区间,而不是一个精确的构建号——这是这种方法本身诚实的局限,而不是某个具体实现独有的缺陷。它的价值不在于精确,而在于独立性。在浏览器关于自身的所有陈述里,这是唯一一个浏览器无法单方面自行宣称的信号,也正因如此,它才值得拿来和浏览器说的其他一切互相印证。
推荐阅读:


