Files

17 KiB
Raw Permalink Blame History

浏览器指纹值采样与回放指南

当目标 JS 访问 Canvas、WebGL、WebGPU、Audio、字体、DOM 几何、CSS 渲染等浏览器指纹 API,或 Node.js 中第三方库模拟结果与真实浏览器不一致时读取本文件。

核心原则

补环境的目标是让目标网页原始 JS 在 Node.js 中看到与真实浏览器一致的结果,而不是在 Node.js 中复刻完整浏览器渲染管线。

必须遵循:

  • 指纹基线先固定:采样前先读取 fingerprint-baseline-consistency.md,确认 case/notes/fingerprint-baseline.json 和 baselineId 已创建;所有采样必须来自同一 profile / seed / 代理 / 语言 / 时区 / UA / Client Hints / screen / WebGL 基线。
  • Trace 未截断值优先:如果用户选择 / 提供了 RuyiTrace 或其他 trace 日志,先使用 trace 中未截断、与当前 baselineId 一致、能确认完整性的真实浏览器值。
  • Trace 不可用再采样:未选择 trace、trace 缺失、trace 未覆盖目标路径、trace 字符串 / 序列化值达到或接近 4000 / 4096 字符疑似截断、真实长度为 unknown、或 trace baseline 冲突时,必须使用用户当前确认的取证工具采集真实浏览器最终返回值、调用参数、调用栈和调用顺序。
  • 禁止 AI 猜指纹值:AI 经验、静态分析、默认值、随机值、mock 值、Node.js / jsdom / node-canvas / headless-gl 的结果不能作为最终指纹回放值,只能辅助定位和设计采样点。
  • 终端 API 值回放优先:在 Node.js 中拦截目标真正读取指纹结果的 API,并返回采样值。
  • 不真实模拟渲染过程:不要为了 Canvas / WebGL / 字体 / DOM 几何去强行复现 Skia、GPU、字体栅格化、抗锯齿、颜色管理或浏览器布局。
  • 不退回自动化作为最终方案:Node.js 无法真实渲染时,不得建议把最终生成参数或最终验证改成浏览器自动化;自动化只允许用于前置取证和采样。
  • 缺样本即显式阻塞:没有采到的指纹值不要乱猜,不要静默返回空值;应提示补采样本或降级说明。
  • 长指纹值不得用截断片段:Canvas dataURL、WebGL readPixels、WebGPU adapter / limits / features、Audio channel data、字体 / DOM 几何批量结果等可能远超 4000 / 4096;只要来自会截断的日志字段,就必须视为定位证据而不是最终值,最终 fixture 必须来自同一 baseline 下的真实浏览器完整采样。

错误方向:

node-canvas / headless-gl / 自己实现 DOM 布局 → 结果与真实浏览器仍不一致 → 建议最终用自动化浏览器

正确方向:

真实浏览器采集终端 API 返回值 → Node.js 按调用特征匹配并回放 → fixtures 多样本验证

指纹值来源优先级

采集任何具体 WebAPI / 指纹值时,必须按以下顺序判断,不能跳级猜值:

  1. Trace 未截断真实值:用户已选择 RuyiTrace / 提供 NDJSON,且字段未达到截断阈值、日志覆盖当前业务路径、调用栈与目标参数生成链路相关、baselineId 与当前 case 一致。
  2. 同一取证工具补采完整值:Trace 未选择、缺失、未覆盖、疑似截断或 baseline 不一致时,使用用户已确认的取证模式采样,例如 ruyiPage、Camoufox、CloakBrowser、用户手动浏览器或对应 MCP / Hook。采样必须复用同一 profile / seed / 代理 / locale / timezone / UA / Client Hints / screen / WebGL 基线。
  3. 用户提供的真实浏览器材料:用户明确提供的 HAR、Hook 输出、浏览器控制台输出、完整响应或截图可作为证据,但必须记录来源、时间、页面、baseline 信息和完整性。
  4. 静态分析 / AI 推断仅辅助定位:只能用于判断应该采样哪个 API、哪个调用参数、哪个 JS 位置,不能直接写入最终 fixture 的 result。

如果 trace 中字符串达到或接近 4000 / 4096 字符,必须标记为“疑似截断”:truncated: true、realLength: "unknown"、visibleLength、sha256OfVisible。该值不得作为最终回放值;必须用同一 case 已确认取证工具补采完整值。

长指纹值截断硬门禁

以下规则是硬性要求:

  1. RuyiTrace / NDJSON / 普通日志中的长字段达到或接近 4000 / 4096 时,只能说明“至少有这些可见字符”,不能说明真实长度就是 4000 / 4096。
  2. 不能把 visiblePreview、visibleSha256、sha256OfVisible、日志可见片段、...<clipped>、...<truncated>、__ruyiTraceLongString__ 对象或被日志 sanitization 包装后的字段写成最终 result。
  3. Canvas toDataURL、Canvas getImageData、WebGL readPixels、WebGPU adapter.info / limits / features、Audio getChannelData / startRendering、字体和 DOM 几何批量结果,如果 trace 中达到 4000 / 4096,必须由已确认取证工具重新采样完整浏览器真实值。
  4. 完整采样值可以直接存储,也可以使用分片格式存储;分片格式必须记录 encoding: "string-chunks"、valueLength、chunkSize、chunks、truncated: false 和完整值 hash。
  5. 没有完整真实值时,阻塞最终交付;不要把 4096 字符当成“刚好完整”,也不要用 Node.js 渲染库补一个近似值。
  6. 数值型返回值为 4096 不一定是截断,例如 WebGL MAX_TEXTURE_SIZE;本规则针对日志中的长字符串、长数组、base64、序列化对象和二进制编码结果。

每个最终写入 fixture 的样本都要能回答:

  • 值来自哪里:RuyiTrace 未截断值 / ruyiPage 采样 / Camoufox 采样 / CloakBrowser 采样 / 用户手动浏览器材料 / 其他真实浏览器证据。
  • 是否完整:truncated: false,并记录完整 valueLength 与 sha256;疑似截断值不得用于最终回放。
  • 属于哪个基线:baselineId 必须与 case/notes/fingerprint-baseline.json 一致。
  • 为什么不用 Trace:如果没有使用 trace,写明未选择、缺失、未覆盖、截断、baseline 冲突或用户明确降级。

过程 API 与终端 API

过程 API

过程 API 只描述绘制、布局或状态构造过程。补环境时可记录必要状态,但通常不需要真实执行渲染。

常见过程 API:

  • Canvas 2D:fillText、strokeText、drawImage、arc、rect、fillRect、putImageData。
  • WebGL:createShader、shaderSource、compileShader、attachShader、linkProgram、bufferData。
  • DOM:appendChild、insertBefore、removeChild、style.xxx = ...、className = ...。
  • 字体:设置 ctx.font、创建 span/div、设置 CSS。

终端 API

终端 API 是目标 JS 真正读取指纹结果的位置。这里必须优先回放真实浏览器采样值。

类别 终端 API
Canvas HTMLCanvasElement.prototype.toDataURL、toBlob、CanvasRenderingContext2D.prototype.getImageData、measureText
WebGL getParameter、getSupportedExtensions、getExtension、getShaderPrecisionFormat、readPixels、getContextAttributes
WebGPU navigator.gpu.requestAdapter、adapter.info、adapter.limits、adapter.features、requestDevice
Audio OfflineAudioContext.startRendering、AudioBuffer.getChannelData、AnalyserNode.getFloatFrequencyData
字体 measureText、document.fonts.check、DOM 字体探测元素的宽高
DOM 几何 getBoundingClientRect、getClientRects、offsetWidth、offsetHeight、scrollWidth、scrollHeight、clientWidth、clientHeight
CSS / 媒体 getComputedStyle、matchMedia、CSS.supports
Speech speechSynthesis.getVoices、SpeechSynthesisUtterance 属性、voices 列表顺序与语言
Permissions navigator.permissions.query 的 Promise、PermissionStatus.state、事件属性、错误模式
Plugins / MimeTypes navigator.plugins、navigator.mimeTypes、PluginArray / MimeTypeArray 长度、索引、命名属性和描述符
MediaDevices / WebRTC navigator.mediaDevices.enumerateDevices、getUserMedia 错误模式、RTCPeerConnection 候选信息摘要

高强度指纹补充与随机化风险

高强度检测中,Canvas / WebGL / Audio / Fonts / DOM geometry / Permissions / Plugins 等结果被随机化或伪装不一致,可能比固定真实值更容易暴露。补环境时必须遵循:

  • 不要使用 Math.random()、时间戳、机器默认值或 AI 经验生成指纹返回值。
  • 不要把 node-canvas、headless-gl、jsdom、系统字体枚举结果直接当成最终浏览器样本。
  • 不要每次取证重新生成一套 UA、语言、时区、screen、WebGL、Canvas、Audio 或字体结果;同一 case 只能使用同一 baselineId。
  • 对 navigator.plugins / navigator.mimeTypes,addon 可用时优先 getMimeTypesAndPlugins(config);不要用普通数组或空数组临时糊弄。
  • 对 Permissions / Speech / MediaDevices 等异步 API,要采样 Promise 行为、错误类型、事件属性、返回对象原型链和 toStringTag,不能只补一个同步值。
  • 如果 trace 中长字段接近 4000 / 4096 字符,按截断处理并补采完整值,不能把可见片段作为最终 fixture;完整值必须来自同一 baseline 下的真实浏览器采样。

采样格式

推荐把指纹样本放入 case/fixtures/fingerprint.fixture.json,或合并进业务 fixture 的 fingerprint 字段。fixture 必须包含 baselineId,并与 case/notes/fingerprint-baseline.json 一致。

{
  "version": 1,
  "baselineId": "fp-20260627-001",
  "source": {
    "baselineId": "fp-20260627-001",
    "mode": "ruyiPage + RuyiTrace / Camoufox / CloakBrowser / 手动取证",
    "valuePriority": "trace-untruncated-first",
    "traceStatus": "unused / used-untruncated / missing / not-covered / truncated / baseline-conflict",
    "traceFallbackReason": "trace 字段疑似截断,已使用 ruyiPage 在同一 baseline 下补采完整值",
    "pageUrl": "https://example.com/page",
    "userAgent": "",
    "timezone": "Asia/Shanghai",
    "locale": "zh-CN",
    "capturedAt": ""
  },
  "canvas": {
    "toDataURL": [
      {
        "match": { "width": 300, "height": 150, "type": "image/png", "stackIncludes": "fingerprint.js" },
        "source": { "capturedBy": "ruyiPage", "traceStatus": "truncated", "baselineId": "fp-20260627-001", "truncated": false, "valueLength": 123456, "sha256": "..." },
        "result": { "encoding": "string-chunks", "valueLength": 123456, "chunkSize": 3000, "chunks": ["data:image/png;base64,..."], "truncated": false, "sha256": "..." }
      }
    ],
    "measureText": [
      {
        "match": { "text": "abcdef", "font": "16px Arial" },
        "result": { "width": 123.45, "actualBoundingBoxLeft": 0, "actualBoundingBoxRight": 123.45 }
      }
    ],
    "getImageData": [
      {
        "match": { "sx": 0, "sy": 0, "sw": 16, "sh": 16 },
        "result": { "width": 16, "height": 16, "dataBase64": "..." }
      }
    ]
  },
  "webgl": {
    "getParameter": [
      { "match": { "pname": 37445 }, "result": "Google Inc. (NVIDIA)" },
      { "match": { "pname": 37446 }, "result": "ANGLE (...)" }
    ],
    "getSupportedExtensions": { "result": ["ANGLE_instanced_arrays", "WEBGL_debug_renderer_info"] },
    "readPixels": [
      { "match": { "x": 0, "y": 0, "width": 16, "height": 16 }, "result": { "dataBase64": "..." } }
    ]
  },
  "domGeometry": {
    "getBoundingClientRect": [
      { "match": { "selector": "#fp" }, "result": { "x": 0, "y": 0, "width": 100, "height": 20, "top": 0, "left": 0, "right": 100, "bottom": 20 } }
    ],
    "offset": [
      { "match": { "selector": "#font-probe" }, "result": { "offsetWidth": 123, "offsetHeight": 19 } }
    ]
  }
}

注意:

  • baselineId 是硬性字段;缺失或与 baseline 文件不一致时,不能把该 fixture 用于最终交付。
  • source / capturedBy / traceStatus / truncated 是硬性溯源字段;不能只写“AI 推断”“默认值”“静态分析得到”。
  • 如果样本继承全局 source,短值可以接受;但长字符串、二进制、Canvas / WebGL / WebGPU / Audio 结果必须写入每条记录自己的 source、valueLength、sha256 和 truncated: false,必要时使用 encoding: "string-chunks" 分片保存。
  • match 是调用特征,不是安全边界;只用于选择回放样本。
  • stackIncludes 可记录目标 JS 文件名、函数名或调用栈片段。
  • 对 ArrayBuffer / Uint8ClampedArray / Float32Array 等二进制结果使用 base64 存储。
  • 真实 Cookie、账号标识、Authorization 不要写入指纹 fixture。

回放匹配顺序

Node.js 交付环境中匹配指纹样本时,按以下顺序:

  1. API 名称。
  2. 显式参数,例如 type、quality、pname、text、font、x/y/w/h。
  3. 对象状态,例如 canvas width/height、当前 ctx.font、WebGL context 类型。
  4. 调用栈特征,例如 stackIncludes。
  5. 调用顺序,仅作为最后兜底。

如果无法唯一匹配:

  • 优先报错并输出缺失的 call key。
  • 不要返回随机值。
  • 不要为了通过而返回空字符串、空数组或固定默认值。

禁止与允许

禁止:

  • 最终项目引入 playwright、puppeteer、selenium、cloakbrowser、ruyipage 来计算指纹。
  • 最终项目用自动化打开页面生成指纹,再把结果传回 Node.js。
  • 因 node-canvas / headless-gl / jsdom 输出不一致而建议改用自动化作为最终方案。
  • 未采样时静默伪造 Canvas / WebGL / DOM 几何结果。

允许:

  • 前置取证阶段用用户确认的工具采集指纹 API 参数、返回值和调用栈。
  • Node.js 交付环境中用 assets/env-modules/fingerprint-env.js 模板回放采样值。
  • 对不影响目标参数的 API 做明确降级,但必须在 notes 中说明证据等级。
  • 对目标未访问的 API 不补。

真实性保护要求

即使终端 API 只是返回采样值,也要保持浏览器对象真实性:

  • 用 Object.defineProperty 定义属性。
  • 建立 HTMLCanvasElement、CanvasRenderingContext2D、WebGLRenderingContext、DOMRect 等必要原型链。
  • 保护方法、getter、setter 的 Function.prototype.toString。
  • 设置实例对象 Symbol.toStringTag / Object.prototype.toString。
  • 按浏览器行为抛出 TypeError 或 Illegal invocation,不要暴露 Node.js 错误。

推荐执行流程

  1. 先读取 fingerprint-baseline-consistency.md,确认或创建 case/notes/fingerprint-baseline.json 与 baselineId。

  2. 用 RuyiTrace NDJSON 或 Hook 找到目标 JS 是否访问指纹 API;如果用户选择 / 提供 RuyiTrace,先判断 trace 中是否已有未截断真实值可用。

  3. 如果访问的是 Canvas / WebGL / WebGPU / Audio / 字体 / DOM 几何,且 trace 没有未截断完整值,或 trace 字段达到 / 接近 4000 / 4096,再生成采样 Hook;采样 Hook 必须完整保存长值或分片保存,不得裁剪为 4096:

    node scripts/generate_fingerprint_hook.js --types canvas,webgl,dom-geometry --out case/hooks/fingerprint-hook.js
    
  4. 在用户确认的取证工具中运行采样 Hook,触发最少量业务动作;复制结果前必须执行 await window.__WEB_JS_ENV_PATCHER_FINALIZE_FINGERPRINT__(),确保所有分片长值写入完整 valueLength、sha256、chunks 与 truncated:false。

  5. 把 finalize 后的采样结果保存为 case/fixtures/fingerprint.fixture.json;禁止保存 clip 片段、visiblePreview、sha256OfVisible、__ruyiTraceLongString__ 或任何真实长度 unknown 的 trace 可见片段。

  6. 运行覆盖检查:

    node scripts/check_fingerprint_fixture.js --fixture case/fixtures/fingerprint.fixture.json --require canvas,webgl --markdown
    
  7. 把 assets/env-modules/fingerprint-env.js 复制到最终项目 result/src/env/,按 fixture 接入。

  8. 对目标 JS 运行 fixtures,若缺样本,补采样而不是改用自动化。

  9. 交付前运行:

    node scripts/check_env_realism.js --case-dir case --require-fingerprint-fixture --markdown
    node scripts/check_final_artifact.js --case-dir case --markdown
    

输出记录要求

补环境报告或 notes 中至少记录:

  • baselineId、baseline 文件路径和是否发生 baseline diff。
  • 哪些指纹 API 被目标 JS 访问。
  • 哪些值来自 RuyiTrace 未截断日志,哪些值因为 trace 缺失 / 未覆盖 / 截断 / baseline 冲突而改用自动化取证工具采样。
  • 哪些值来自 ruyiPage / Camoufox / CloakBrowser / Hook / 用户手动浏览器材料,并记录采样工具、时间、完整长度和 hash。
  • 是否存在疑似截断字段;如果存在,必须写明补采状态,不能把可见长度当成真实长度,也不能把 trace 中 4000 / 4096 字符片段当成最终指纹值。
  • 哪些 API 做了近似降级及原因。
  • 缺失样本是否阻塞。
  • 最终项目是否完全不包含浏览器自动化代码。