CreepJS 预测报告:聚合逻辑与未知状态
对比 CreepJS 的 getPrediction 查找与 renderPrediction 信号聚合、unknown 状态;两者都不能证明设备身份。
BotBrowser Team
BotBrowser 可以让声明的浏览器配置在浏览器可见表面之间保持一致,但不能认证第三方的群体预测结果。在固定版本的 CreepJS 源码中,getPrediction() 查找群体数据行并选择候选值,而 renderPrediction() 则独立读取 decryptionData、聚合信号字段并显示 review 或 score 状态。这是两条不同的源码路径;任一路径的输出都不能证明个人身份、机器身份或 bot 判定。
TL;DR
getPrediction会在传入数据中查找id与请求 hash 相等的行,也接受把两边转换成字符串后相等。名为decrypted的局部变量保存的是匹配到的对象键;这个函数没有解密数据行。- 系统、GPU 和 GPU 品牌字段只有在候选数组恰好有一个值时才返回。设备字段还可以在多个名称共享一个基础名称时返回该名称。
- 渲染器把 18 个具名信号对象中的值聚合到三个 JavaScript
Set对象,分别保存 GPU 品牌、GPU 和设备。Set去重,代码随后删除undefined;聚合结果不再保留每个值由哪个信号对象提供。 unknownHTML、pendingReview、群体混合评分、预测标记和错误占位符属于不同代码分支。它们都不是标签正确率的校准概率。
Contents
预测标签是聚合结果
“预测”一词容易让人作出超出实现能力的推断。读者可能看到设备名称旁边有分数,就认为页面识别了某台实体设备,或测量了该名称正确的概率。固定版本的 CreepJS 实现并不支持这些结论。源码展示了它如何从输入中选择并呈现特定值,却没有提供校准研究、置信区间或个人级标识符。
两个函数回答的是不同问题。getPrediction 查找一条群体数据行,并从四个候选列表中产生 system、device、gpu 和 gpuBrand 字段。renderPrediction 接收信号对象,把选定字段跨对象组合,再构建预测区域和单独信号行的界面标记。查找结果和信号渲染摘要不应被压成一个不透明分数。
CreepJS 信号组解读文章介绍如何阅读各表面的标签,信号组交叉检查分析则梳理 botHash 背后的有序模式。这两篇都没有追踪群体数据行匹配,也没有覆盖预测面板的候选选择与显示分支。
当报告变化时,这一区别很重要。标签可能消失,因为候选列表现在有两个值、信号值不再共享一个基础名称、某个字段没有提供,或者预测进入了错误分支。这些路径含义不同。没有一个唯一值不代表浏览器没有 GPU;错误占位符也不证明所有信号模块都运行后产生了分歧。
源码范围固定为 CreepJS src/prediction/index.ts 的提交 10aa6724cd33a1015db1574211890518cd04f0cc,不是对当前在线 CreepJS 服务的观察。更广泛的 指纹与隐私威胁文档 介绍浏览器指纹信号,但没有逐分支说明这个预测模块。
先匹配群体数据行,再读字段
getPrediction 接收 hash 和 data。它遍历 data 的键,在每个键所对应的数组中搜索,并将元素的 id 与请求的 hash 比较。比较既接受直接相等,也接受两边转换成字符串后相等。匹配元素所在的键被存进名为 decrypted 的局部变量。
这个变量名本身不能证明代码执行了密码学操作。在此函数中,相关操作是数组搜索和 ID 比较。源码没有在这里解密数据行。准确说法是“按 hash 查找群体数据行”,不是“解密预测”。局部变量的名字不会改变代码实际执行的操作。
找到匹配元素后,函数把四个数组复制到局部变量:devices、systems、gpus 和 gpuBrands。缺失数组会变成空数组。随后,返回对象从每个候选列表中选择一个可见值。因此,找到数据行时,一个或多个返回字段仍可能是 undefined:匹配数据行与取得唯一值是两个不同步骤。
| 源字段 | 查找操作 | 保守解读 |
|---|---|---|
item.id | 与传入的 hash 比较,也接受字符串化后的相等 | 在给定数据对象中找到了匹配记录。 |
item.systems | 缺失时用空列表;只有一个候选时才返回系统 | 零个或多个候选都不会产生唯一 system 值。 |
item.gpus | 缺失时用空列表;只有一个候选时才返回 GPU | 没有返回 GPU 标签不等于浏览器没有 GPU。 |
item.gpuBrands | 缺失时用空列表;只有一个候选时才返回品牌 | 函数不会给多个品牌排序,也不会报告其中一个的置信度。 |
item.devices | 缺失时用空列表;返回单个候选或共同基础名称 | 文本前缀相同是选择规则,不是独立硬件验证。 |
最后一行尤其容易被过度解读。代码注释用 Android 10 说明它如何成为 Android 10 和 Android 10 Blah Blah 共同的基础名称。这只是字符串包含关系的例子,不表示 CreepJS 在真实浏览器中确认了具体操作系统版本。
候选数量决定哪些值会返回
返回规则有意采用不对称设计。三个字段要求恰好一个候选,而 device 还有共享基础名称的后备规则。数组长度解释了为什么群体数据行存在时,界面字段仍可能缺失。
| 字段 | 固定源码中的候选条件 | 条件不满足时的返回值 |
|---|---|---|
system | systems.length == 1 | undefined |
gpu | gpus.length == 1 | undefined |
gpuBrand | gpuBrands.length == 1 | undefined |
device | 一个设备候选,或一个名称被每个候选字符串包含 | 如果没有名称被所有候选共享,基础名称辅助函数返回 undefined。 |
对于系统、GPU 型号和 GPU 品牌,实现不会挑选最常见的候选,也不会按分数排序。它只检查数组是否有一个元素;否则字段就是 undefined。这是明确的歧义边界,不是隐藏的排序算法。
对于设备,getBaseDeviceName 会寻找候选字符串 a,要求每个候选字符串 b 都包含 a。只有一个候选时,代码会直接返回它,不调用辅助函数。有多个候选时,可能返回共同基础名称;如果没有共同名称出现在所有候选中,这个分支就不会给出一个设备名称。此规则基于文本,不应描述成硬件距离计算。
以下源码推导的例子展示代码路径,不是在线报告:
| 候选列表 | 代码路径结果 | 结果不能证明什么 |
|---|---|---|
systems 有一个条目 | 该条目作为 system 返回 | 标签识别了某个人的操作系统。 |
gpus 有两个条目 | 不返回唯一的 gpu 值 | 两个 GPU 都没有被任何信号模块观察到。 |
devices 包含 Android 10 和 Android 10 Blah Blah | 可能返回共同基础名称 Android 10 | 某个具体 Android 版本经过了独立验证。 |
devices 有互不相关的名称,且不存在所有候选共享的基础名称 | 基础名称辅助函数不返回值 | 浏览器无效或正在自动化运行。 |
undefined 是值得保留的状态。将它替换成第一个候选、多数猜测或推断出的设备,会凭空添加函数没有实现的选择规则。比较记录应把“没有返回唯一值”与“源字段不存在”分开记录。
18 条记录进入三个去重集合
聚合字段 gpuSystem 和 gpuModelSystem 不描述 WebGL context 的创建或扩展支持;CreepJS WebGL 报告分析会单独检查渲染器的 context 与能力信号。
renderPrediction 读取具名信号对象,并创建三个 Set 实例。每个集合都从 18 条记录中读取一个属性:jsRuntime、emojiSystem、domRectSystem、svgSystem、mimeTypesSystem、audioSystem、canvasSystem、canvasPaintSystem、canvasTextSystem、canvasEmojiSystem、textMetricsSystem、webglSystem、gpuSystem、gpuModelSystem、fontsSystem、voicesSystem、screenSystem 和 deviceOfTimezone。
三个属性分别投影到不同集合:一个集合收集 gpuBrand,另一个收集 gpu,第三个收集 device。读取属性前,每条记录都会用空对象作后备。集合构建后,代码删除 undefined。相同值会折叠成一个,因为 Set 只保留唯一值。
| 聚合集合 | 从 18 条记录读取的属性 | 源码中直接可见的转换 |
|---|---|---|
gpuBrands | .gpuBrand | 构造集合,然后删除 undefined。 |
gpus | .gpu | 构造集合,然后删除 undefined。 |
devices | .device | 构造集合,然后删除 undefined。 |
该操作将一组具名字段缩减为唯一值,但结果集合不会指出每个值来自哪个信号对象。如果五个模块提供同一个 gpuBrand,集合中只有一个值,不会留下“5 次”的计数。如果两个模块不一致,集合中可能有两个值,但界面仍不显示各自来自哪个 API。上游模块在代码中列出了来源;如果比较需要追溯来源,就要将来源另存到单独记录中。
这个集合也不同于群体数据行的候选列表。getPrediction 的候选数组用于判断某个 hash 是否能返回唯一值。renderPrediction 的信号集合则把各字段值组合成可见摘要。用“共识”一个词描述两条路径,会掩盖这个差异。
18 个来源字段可以按命名表面分组,但这些名称不意味着质量相等或彼此独立。Canvas 图像、绘制、文本和 emoji 在源码中是不同输入;gpuSystem 和 gpuModelSystem 也彼此独立。把它们视为完全独立的投票,会引入实现中不存在的统计假设。
分别看设备选择与评分状态
界面渲染设备标签前,会先计算一个特殊的 Windows Remote Firefox Privacy 选择。源码统计包含“Windows”的设备字符串数量,再除以全部设备字符串数。只有比例大于 0.5 时才进入这条分支,然后按固定偏好顺序查找:Windows 7、Windows 8、Windows 8.1、Windows 11,最后 Windows 10;每个版本都先检查 64 位写法,再检查较短写法。如果比例不超过一半,辅助函数返回 undefined。
最终的 deviceName 是 RFP 辅助函数结果;若结果为 falsy,则使用设备名称的共同基础部分。注释说样本数据会忽略 Firefox RFP,因为它返回 Windows 10;随后说明多个 Windows 版本同时出现时采用的优先顺序。这是该源码特有的显示规则,不证明一般人群遵循同一顺序,也不应被描述为普遍准确的操作系统检测器。
crowdBlendingScoreGrade 把数值区间映射到字母:90 及以上为 A,80 及以上为 B,70 及以上为 C,60 及以上为 D,低于 60 为 F。只有 crowdBlendingScore 的 JavaScript 类型为 number 时,界面才显示百分比和字母。单独的 bot 分支显示 locked。代码定义了字母映射和界面条件,却没有说明分数预测什么,也没有说明它如何校准。
当 pendingReview 为 truthy 时,模块会把它显示为独立提示。它应与评分和预测名称分开记录。没有另一份来源明确定义状态时,固定源码不支持把它改述为“失败”“已批准”或“人工审核已完成”。
可以记录以下界面状态,但不要把它们变成置信度声明:
| 可见状态 | 源码条件 | 有边界的解读 |
|---|---|---|
合并标签前的 ** | deviceName 和 gpuBrandName 都是 truthy | 渲染器有两个显示字符串。星号不是概率。 |
单一标签前的 * | 两个显示字符串中只有一个是 truthy | 此分支只有其中一个字符串可显示。 |
| 空白预测图标 | 两个字符串都不是 truthy | 此分支不会显示合并标签。 |
pending review: … | pendingReview 是 truthy | 界面显示传入字段;固定源码没有定义审核结果。 |
| 群体混合分数及字母 | 分数类型是 number,且没有进入锁定分支 | 界面显示传入的数字和分档;准确度未知。 |
locked | bot 分支为 truthy | 界面进入锁定分支,此处不展示评分。 |
hasValue 辅助函数还需要额外注意。它查找第一个类型不是 undefined 的属性值;调用它的代码会对结果取反,以决定是否显示 unknownHTML。已定义的 false 或 0 本身也是 falsy,因此也可能进入未知状态的显示分支。这意味着 unknown 标签不能证明信号模块没有执行,它也可能反映了辅助函数处理 falsy 值的方式。这个观察只适用于固定实现;后续版本改动该函数后应重新核对。
predictionErrorPatch 是另一条分支。传入错误且预测元素存在时,它会渲染 Prediction Failed,并在许多行显示空白图标。这个占位符描述错误界面,不是候选数量规则,也不是一次成功运行中所有信号都缺失。记录时应把错误分支与正常报告中单独显示为未知的行分开。
保留绑定来源的比较记录
两个报告不同时,先记录问题和源码版本,再解释标签。以下字段能保留实现中的差异,同时不复制原始 hash,也不收集完整浏览器标识:
| 记录字段 | 保留内容 | 原因 |
|---|---|---|
| 来源 | 仓库、commit SHA 和文件路径 | 后续版本可能改变查找或渲染行为。 |
| 查找状态 | 传入数据中是否有 hash 匹配行;不要保存 hash 本身 | 行是否存在与字段是否唯一是不同结果。 |
| 候选数量 | system、设备、GPU 和 GPU 品牌的计数 | 返回规则依赖数组长度和共同设备基础名称。 |
| 聚合状态 | 哪些具名信号字段存在,以及每个集合有零个、一个还是多个唯一值 | 去重集合不会保留原始贡献列表。 |
| 渲染状态 | 预测标记、unknown 分支、pendingReview、数字评分、锁定分支或错误占位符 | 看似相同的空白区域可能来自不同路径。 |
| 限制 | 记录是固定源码分析、当前页面观察,还是其他明确限定的来源 | 源码本身不能证明服务当前输出或准确度。 |
不要只因为查找使用 hash 就保存传入的 hash。通常用布尔值记录“是否存在匹配行”,再记录候选数量,就足以回答问题。保存原始标识符会增加留存数据,却不会改善对代码分支的解释。
这份表用于解释和比较公开来源的行为。它不是复现 CreepJS 后端的步骤,不是 CreepJS 准确率基准,也不是改变第三方评分的方法。要支持这些结论,需要本源码文件没有提供的其他证据。
Sources
- CreepJS prediction 模块,commit
10aa6724cd33a1015db1574211890518cd04f0cc:查找、候选缩减、信号聚合、unknown 状态、评分显示和错误占位符。 - BotBrowser:指纹与隐私威胁:信号和隐私的总体背景,不是 CreepJS 预测实现。
- BotBrowser:自动化一致性:如何整体考虑浏览器可见的配置表面;不决定第三方检测器的结论。
Practical conclusion
应把 CreepJS 预测面板看成一组依赖源码分支的输出。先确认群体数据中是否有匹配行,再分别记录候选数量、信号对象唯一值和界面状态。单一返回值、共同设备基础名称、评分分档、待审核提示和 unknown 标记不能互相替代。固定源码解释的是标签如何被组装,不会因此成为经过验证的设备身份或校准过的 bot 判断。
本文属于指南: 什么是浏览器指纹?原理、风险、信号与隐私边界