Intl API 暴露了浏览器内置的 ICU 语言环境数据库。本文讲解哪些 Intl 调用会泄露指纹、为何能推断浏览器版本,以及为什么难以一致伪造。
问大多数人 JavaScript 的 Intl API 里哪一项被用来做指纹识别,答案几乎都是时区。时区确实最出名,我们在时区与语言泄露一文里已有详细讲解。但 Intl 并不是单一信号,它是一扇通向浏览器内置语言环境数据库的窗口,而这个数据库体量大、带版本号,并且随引擎、平台和构建版本而不同。你能向它提出的问题,携带的信息量远比一个时区字符串多。
核心要点
Intl暴露的是编译进浏览器或操作系统的 ICU 类语言环境数据,因此它的回答取决于引擎、操作系统和构建版本,而不仅仅是你设置的语言。- 三个语言环境值可能互相矛盾:
navigator.languages(对外声明)、Accept-Language请求头(实际发送)、Intl.DateTimeFormat().resolvedOptions().locale(运行时协商的结果)。这种矛盾本身就是信号。 Intl.supportedValuesOf()会返回日历、排序规则、货币、数字系统、时区和单位的长列表。整份列表就是指纹,同时还能标出你的构建年代。Intl.NumberFormat、Intl.DateTimeFormat和Intl.Collator的格式化输出会因数据版本而异;在遵循操作系统区域设置的浏览器上,即使界面语言是英语,默认值也可能暴露你所在的地区。- 数据存在于二进制文件内部,很难一致地伪造。Tor Browser 和 Firefox 的
privacy.resistFingerprinting可以把它统一为en-US,代价是使用体验。
时区:一段话带过
没错,Intl.DateTimeFormat().resolvedOptions().timeZone 是最知名的 Intl 泄露点,它与 IP 所在国家不符是经典的 VPN 特征。这里不再重复,这个话题由时区与语言泄露负责。下面讲的是 Intl 攻击面的其余部分,它们远没有那么多人讨论。
三个语言环境值,三个来源
浏览器内部对“你的语言”有好几种说法,它们来自不同的地方:
navigator.languages是浏览器向脚本声明的有序语言列表。Accept-Language是浏览器在每个请求中实际发送的内容。resolvedOptions().locale(见Intl.DateTimeFormat及其同类对象)是运行时把你的请求与它真正拥有的语言数据匹配之后协商出的结果。
在未经改动的浏览器里,这三者按可预期的方式对应。如果某个工具改了前两项却忘了第三项,页面看到的就是一个运行时自己都不认可的“声称身份”。这与指纹一致性和 User-Agent 伪造检测是同一套逻辑:单个值不足以定罪,矛盾本身才是证据。
熵最高的一项:Intl.supportedValuesOf()
Intl.supportedValuesOf(key) 接受 calendar、collation、currency、numberingSystem、timeZone 或 unit 之一,返回运行时支持的该类值的排序列表。这些列表很长,并且与构建内置的语言数据版本同步变化。
关键在于:指纹是整份列表,而不是其中任何一项。新增一种货币、一个被重命名或新收录的时区、多出一个单位,都会改变列表。由于语言数据版本随浏览器和操作系统发布而更新,这份列表还相当于构建日期戳:不读取 User-Agent 字符串,也能把你定位到某一段发布区间。把六份列表做个哈希,就得到一个紧凑的标识符,只要浏览器数据不变它就稳定,并且与使用相同数据的所有人共享。
把格式化输出当作探针
另一种做法是不再列举,而是直接格式化:给运行时一个固定输入,比较它返回的结果。
Intl.NumberFormat决定分组分隔符、小数点、货币符号位置和紧凑记数法。Intl.DateTimeFormat.formatToParts()把日期拆成带标签的部分,各部分的顺序以及它们之间的字面字符串都能被看到。Intl.Collator决定带重音、大小写混合和中日韩字符串的排序方式。
不同的数据版本和不同的平台数据,对同一输入可能给出可观察到的不同输出。我们刻意不列出特定版本的具体输出字符串,因为它们会变化,且我们没有逐一验证;重点是差异的类别。追踪者不需要知道正确答案是什么,只需要对许多答案取一个稳定的哈希。
与操作系统地区而非语言绑定的默认值
视浏览器和平台而定,有些 Intl 默认值跟随操作系统的区域设置,而不是你在浏览器里选的语言。Apple 设备上的 Safari 最为典型;Chrome 和 Firefox 则大多依据浏览器自身的语言设置来决定默认值,这本身就是引擎之间的一个差异。可能受影响的值包括:
- 默认日历(公历、佛历、日本年号历等);
- 默认数字系统(拉丁数字、阿拉伯-印度数字、天城文数字);
- 小时制(12 小时或 24 小时)和每周第一天。
一个人可以在界面语言为英语的浏览器里阅读英文页面,而操作系统却设成了泰国或沙特地区。在遵循该设置的浏览器上,navigator.language 显示的是英语,resolvedOptions() 却悄悄透露了地区。这与语言请求头是不同的信号,可用来推测设备是在哪里完成初始设置的。
为什么难以一致地伪造
反检测浏览器可以改写 User-Agent、navigator.languages 和时区字符串,因为这些只是 JavaScript 对象上的值。而语言环境数据本身是固化在二进制文件或操作系统里的。要保持自洽,伪造层必须拦截每一个 Intl 构造函数、每一个 supportedValuesOf 键、每一次 formatToParts 调用,还有数字、日期和字符串上的 toLocaleString,并给出与声称的平台和版本一致的答案。漏掉一处,声称的身份就与运行时自相矛盾。想了解这类缺口在实践中如何被发现,可参阅反检测浏览器与真实浏览器的对比。
我们介绍这些信号,是为了让你能理解和检测它们,而不是提供伪造它们的方法。
缓解措施及其代价
Tor Browser,以及启用了 privacy.resistFingerprinting 的 Firefox,通过标准化来应对这一攻击面:时区一律报告为 UTC;如果你接受“请求英文版网页”的提示,语言环境也会对所有网站统一报告为 en-US。人群里每个人看起来都一样,这正是目的所在。
代价是真实存在的:日期、数字和排序会按一个并非你本人的语言环境来格式化,而信任该报告值的网站会给德国或日本的用户展示错误的习惯格式。标准化是用便利换匿名,这也是大多数浏览器默认不这么做的原因。
这个信号有多大?
客观衡量,Intl 属于中等熵的贡献者,达不到 Canvas 那个量级。当前版本 Chrome 上的 en-US 这类常见语言环境,其 Intl 特征与极多的人完全相同。对冷门语言环境、操作系统与浏览器地区混搭,以及旧版或定制构建,这个信号的区分度会更高;它最大的价值在于与其他信号交叉验证。如何看待这一点,请参阅熵与匿名集。你可以用指纹检测查看自己的浏览器报告了什么,它完全在浏览器本地运行,不会向服务器发送任何数据。

