Client Hints 指纹识别:HTTP 头部与身份暴露
sec-ch-ua 等 Client Hints 头部在每次请求中描述浏览器品牌、版本和平台。了解不一致从何而来,以及如何把请求头与 JavaScript 取值对照。
基于 Chromium 的浏览器发出的每个请求,都可以带上一段简短的结构化描述,说明发出请求的是什么浏览器。这些描述叫作 Client Hints,其中三项默认发送:Sec-CH-UA、Sec-CH-UA-Mobile 和 Sec-CH-UA-Platform。它们以请求头的形式出现,早于页面上任何脚本的运行,所以服务器从第一次导航起就能读到浏览器品牌、主版本、设备类别和操作系统系列。
Client Hints 的初衷,是用更窄、更易控制的结构化信息取代冗长且格式自由的 User-Agent 字符串。实际上,它们也让浏览器多了一个描述自己的位置。如果请求头说一种情况,navigator.userAgentData 说另一种,User-Agent 字符串又说第三种,这种分歧本身就是指纹审查会注意到的信号。下面各节依次说明每一层报告什么、不一致从何而来、如何在一次会话中对照各层,以及 BotBrowser 在这个主题上记录了什么、没有记录什么。
浏览器在任何脚本运行之前发送什么
Client Hints 分为两组。低熵提示默认发送。Sec-CH-UA 列出浏览器品牌及其主版本,Sec-CH-UA-Mobile 说明浏览器是否请求移动版体验,Sec-CH-UA-Platform 给出操作系统系列。这三项以相似的方式描述了很大一部分浏览器,因此规范把它们视为低风险,允许浏览器在无人请求时发送。
高熵提示是更详细的那一组:完整版本列表、平台版本、CPU 架构、位数、设备型号,以及 32 位浏览器是否运行在 64 位系统上。服务器必须先用 Accept-CH 响应头列出想要的提示表示同意,浏览器才会在之后发往该来源的请求中附带它们。如果服务器在第一次请求时就需要某项提示,还可以使用 Critical-CH,它会要求浏览器带上该提示重试请求。第三方框架只有在嵌入它的页面通过 Permissions-Policy 头部委托后,才会收到提示。
JavaScript 有自己通向同一批数据的路径。navigator.userAgentData 会立即给出品牌、移动标记和平台,页面请求具体名称时,getHighEntropyValues() 会以 Promise 形式返回详细取值。两条路径本应描述同一个浏览器,所以相互对照是最直接的一致性检查。
桌面版 Chrome 系浏览器的默认请求,携带的头部形如下面几行。具体的 GREASE 条目及其位置会随 Chrome 版本变化,因此这些取值展示的是数据形态,不是用来照抄的内容。
Sec-CH-UA: "Chromium";v="136", "Google Chrome";v="136", "Not.A/Brand";v="99"
Sec-CH-UA-Mobile: ?0
Sec-CH-UA-Platform: "Windows"
User-Agent 头部仍然存在。Chrome 缩减了其中的细节,精确的版本和平台信息转移到了 Client Hints。因此服务器手里有同一个浏览器的两份描述,可以核对它们是否一致。关于字符串与结构化提示的更细对比,请参阅 Custom User Agent。
品牌列表中还有一个 GREASE 条目,它是刻意设成怪异形式的品牌,例如 Not.A/Brand,其文本、版本和位置会随时间变化。它的目的,是防止服务器把固定列表写死。对配置浏览器身份的人来说,GREASE 提醒我们:品牌列表遵循与版本相关的规则,手写的列表即使每个品牌名都是真的,也可能显得不对。
不一致从何而来
浏览器身份有多个表面,页面或服务器的不同部分各自读取其中一个。两个表面描述了不同的浏览器,就会出现不一致。最常见的来源如下:
- User-Agent 字符串写的是一个浏览器版本,而
Sec-CH-UA列出的是另一个主版本。 Sec-CH-UA-Platform中的平台与navigator.userAgentData中的平台不同,或移动标记与平台相矛盾。- 请求头中的品牌列表与 JavaScript 返回的品牌不同,例如一处是 Edge,另一处只是 Chrome。
- Worker 报告的身份与主线程不同,因为覆盖只作用于页面。
- 两种不同工具各自施加覆盖,例如 Profile 加上框架层或 CDP 层的 User-Agent 修改,两种工具覆盖的表面子集并不相同。
- 配置在编写时是正确的,但现在落后于生成浏览器二进制文件的那个 Chrome 版本。
请求头一侧的项目可以在服务器上读取,无需运行页面脚本:默认请求头随第一次请求到达,请求高熵提示的服务器会在下一个请求周期收到它们,并能与 User-Agent 对照。navigator.userAgentData 和 Worker 的对照则需要脚本。从隐私角度看,Client Hints 增加了一组取值,它们与其他信号结合后可以缩小浏览器的范围。EFF 的 Cover Your Tracks 展示了普通取值的组合如何变得有区分度,而 Client Hints 是这种组合中的又一个成分。一组一致的取值也比东拼西凑的取值更容易审查,因为你看到的每个差异都是真实的问题,而不是噪声。
设想一个团队加载了描述 Windows 桌面的 Profile,随后又通过测试框架设置了另一种操作系统的 User-Agent 字符串。框架覆盖的是字符串,也许还有部分元数据,Profile 覆盖的是它自己的表面,两个工具互不知情。结果是一次会话中,请求头和脚本可见的取值描述了两台不同的机器。解决办法不是更好的覆盖。解决办法是为每个取值选定一个来源,并去掉另一个。
执行上下文与请求类型同样重要。专用 Worker、共享 Worker 和服务 Worker 各有自己的全局作用域,NavigatorUAData 接口在 Worker 作用域和窗口中都会暴露。因此页面可以在 Worker 内读取品牌和平台取值,并与自己的取值对照。只修改页面的覆盖,会让这种对照暴露出来。
与平台相关的取值需要特别留意,因为它们彼此关联。平台名称、平台版本、架构、位数和移动标记描述的是同一台设备。对某个操作系统正常的平台版本,放在另一个系统的名称旁边就显得不对;移动标记为真却搭配桌面架构,会立刻引出疑问。修改其中一个取值时,要把它们放在一起复查。
在一次会话中对照请求头与 JavaScript 取值
对照的目标很窄:确认一次会话在你能读取的所有位置都报告同一个身份。为此不需要第三方评分页面。一个你能控制的页面和服务器就够了,它们还能让你有意请求高熵提示。
- 从你能控制的服务器通过 HTTPS 提供一个测试页面,并记录它在第一次导航时收到的请求头。
- 添加
Accept-CH响应头,列出你想对照的高熵提示,例如Sec-CH-UA-Full-Version-List、Sec-CH-UA-Platform-Version和Sec-CH-UA-Arch,然后再加载一次页面,让浏览器带上它们。 - 在页面中读取
navigator.userAgentData,并用相同的名称调用getHighEntropyValues(),把结果记录在请求头旁边。 - 在专用 Worker 中重复这些 JavaScript 读取,如果你的站点使用服务 Worker,也在其中重复,并记下与主线程的任何差异。
- 对照品牌名称、主版本、平台、平台版本、架构、位数、型号和移动标记,再加上 User-Agent 字符串,并写下每个差异以及产生它的请求或上下文。
公开页面可以作为补充。EFF 的 Cover Your Tracks 展示你的浏览器在典型跟踪者眼中的样子,可作为整体区分度的第二意见。这类页面不太适合逐个请求头的对照,因为它们不会告诉你哪个请求产生了哪个取值。这件事请用你自己的测试页面,把公开工具当作背景。更完整的检查流程请参阅 Verify Browser Fingerprint。
把结果记在一个简单的表格里,每个取值一行,每个表面一列:请求头、主线程、Worker 和 User-Agent 字符串。各列不同的行是一个发现。各列一致的行只是该取值的证据,并不说明某个网站会如何对待这次会话。
发现差异时,从出错的表面倒推。问清楚是哪个工具或参数负责该表面,检查是否还有第二个工具也在写入它,然后去掉重复的来源。之后重新运行整个对照,而不只是失败的那一行,因为改动一个取值可能让相邻取值的不一致暴露出来。
用 BotBrowser 保持唯一的 Client Hints 来源
BotBrowser 文档说明,配置身份参数(包括 --bot-browser-brand 以及 User-Agent 和平台参数)会生成相互匹配的 Client Hints 品牌、GREASE 令牌、高熵取值和 Client Hints HTTP 头部,并在主线程、Worker 和 HTTP 请求中与 navigator.userAgentData 保持一致。对于上面描述的对照,这意味着你可以把一次会话的请求头和 JavaScript 取值,与同一套配置身份进行核对,而不必手工协调多个来源。BotBrowser 不能控制网站如何归类 Client Hints 或其他指纹信号,不保证任何检测或访问结果,也不能协调在 BotBrowser 之外施加的 CDP 层或框架层 User-Agent 覆盖。自定义 User-Agent 和完整的 userAgentData 控制需要文档所述的 ENT Tier3 许可。
文档中的路径很短。加载一个 Profile,只有在需要不同于 Profile 的品牌或平台时,才添加身份参数。参数写在启动参数中,不要写在框架选项里。BotBrowser 会根据这些输入生成依赖的取值,所以你不必自己编写品牌列表或 GREASE 条目。
chromium-browser \
--bot-profile="path/to/profile.enc" \
--bot-browser-brand=edge \
--bot-ua-full-version=142.0.7444.60 \
--bot-brand-full-version=142.0.3595.65
文档中的品牌取值是 chrome、edge、brave、opera、chromium 和 webview。每个品牌有自己的版本节奏,所以即使主版本相同,Edge 和 Opera 的完整版本也与 Chromium 版本不同。用 --bot-ua-full-version 设置 Chromium 版本,用 --bot-brand-full-version 设置品牌自身的版本,并让两者保持相同的主版本。品牌切换记录在 ENT Tier2 许可下,而 WebView 品牌和自定义 User-Agent 记录在 ENT Tier3 下。
把 Profile 和身份参数当作唯一来源。如果你还通过 CDP 或框架选项设置了 User-Agent,这个覆盖是在 BotBrowser 之外施加的,BotBrowser 不会把它与生成的 Client Hints 协调起来。为每个取值选择一个来源。传入 --user-agent 时,使用文档中的占位符,例如 {ua-full-version},让字符串与提示遵循相同的参数。
每次 Chrome 发布新版本后,检查 --bot-ua-full-version 的 Chromium 主版本是否仍与你的 BotBrowser 二进制文件一致。当二进制文件升级到新的主版本时,请使用针对该版本的 Profile 和参数。文档中记录的不一致症状是 Client Hints 与二进制文件不符,而过期的版本本身就是配置与运行它的浏览器之间的不一致。
关于 Client Hints 的常见问题
禁用 JavaScript 时还会发送 Client Hints 吗?
默认的低熵提示是请求头,不依赖页面脚本。高熵提示取决于服务器通过 Accept-CH 表示同意。JavaScript 接口当然需要脚本运行。这种划分说明了为什么请求头检查和脚本检查回答的是不同的问题,也说明了为什么两者都属于一致性检查。
服务器能收到它从未请求过的高熵提示吗?
通过请求头不能。浏览器只在得到同意之后才附带它们。页面上运行的脚本仍然可以调用 getHighEntropyValues() 并自行转发结果,这是另一条有自己控制方式的路径。审查一个站点的行为时,两条路径都要看。
测试框架可以设置 Client Hints 吗?
测试框架和 DevTools 协议可以覆盖 User-Agent 字符串,在某些情况下还能覆盖相关元数据。覆盖能涵盖什么取决于工具,不同工具的覆盖还可能相互矛盾。请用上面的对照检查每个表面。BotBrowser 不会协调在 BotBrowser 之外施加的覆盖,所以请把身份取值放在一个地方。
Sec-CH-UA 与 Sec-CH-UA-Full-Version-List 有什么区别?
Sec-CH-UA 携带品牌及主版本,默认发送。Sec-CH-UA-Full-Version-List 携带每个品牌的完整版本字符串,是需要事先同意的高熵提示。在 JavaScript 中,同样的划分表现为:brands 对应主版本,fullVersionList 对应详细版本。两者应当列出相同的品牌和相同的主版本。
Client Hints 一致就能决定网站如何对待会话吗?
不能。Client Hints 只是众多指纹信号之一。一致性消除了一类不一致,但网站还会结合许多其他输入,BotBrowser 也不能保证任何分类、跟踪结果或访问结果。把干净的对照结果当作对你自己配置的质量检查即可,仅此而已。
哪些 BotBrowser 参数会影响 Client Hints?
文档中的身份参数是 --user-agent、--bot-platform、--bot-platform-version、--bot-model、--bot-architecture、--bot-bitness 和 --bot-mobile,另有辅助参数 --bot-browser-brand、--bot-ua-full-version 和 --bot-brand-full-version。--user-agent 参数支持从其他参数读取取值的占位符,因此字符串和提示取自相同的取值。
信赖某个配置之前的检查清单
一份简短的清单能让审查可以重复。每次修改 Profile、品牌、平台参数或浏览器二进制文件时都运行一遍。
- 每个取值只有一个来源:Profile、身份参数,或者别无其他。
- 品牌、平台和移动标记在请求头、主线程和 Worker 之间一致。
- 通过
Accept-CH请求的高熵取值与getHighEntropyValues()的结果相符。 - User-Agent 字符串和 Client Hints 指向相同的 Chromium 主版本。
- 每次发布后,
--bot-ua-full-version的主版本与 BotBrowser 二进制文件一致。 - 许可级别覆盖了所使用的参数。
延伸阅读:Navigator Properties Fingerprinting 介绍了与 navigator.userAgentData 相邻的、脚本可见的 navigator 取值,What Is Browser Fingerprinting 则提供了信号如何组合的更广背景。