指纹

栈深度指纹识别:递归限制追踪

JavaScript 栈深度和递归限制如何因浏览器和平台不同而创建指纹,以及如何控制栈行为。

文档中心

想直接看维护中的产品文档?

这篇文章对应的主题已经有文档中心页面。需要规范流程、当前参数和长期参考时,优先看 docs。

简介

每个 JavaScript 引擎都有函数递归深度的限制。当函数自调用次数过多时,引擎抛出 "Maximum call stack size exceeded" 错误。这个限制没有标准化。它在浏览器、浏览器版本、操作系统甚至同一浏览器的主线程和 Web Workers 之间都不同。追踪系统利用这种变化,通过测量栈溢出前可以进行多少次递归调用来精确计算。得到的数字(最大递归深度)成为指纹信号。因为这个值取决于低级引擎内部和平台特定的栈分配,从 JavaScript 很难控制。本文解释栈深度指纹识别的工作原理,以及 BotBrowser 如何通过 --bot-stack-seed 标志提供精确控制。

隐私影响

栈深度指纹识别是较为冷门的指纹技术之一,但正因为大多数隐私工具不解决它,它提供了有用的熵。学术研究表明,递归限制可以区分浏览器版本、操作系统甚至同一浏览器的 32 位与 64 位版本。

该技术有效因为:

  • 它在浏览器间变化。 Chrome、Firefox 和 Safari 各自分配不同的默认栈大小,产生不同的最大递归深度。
  • 它在平台间变化。 同一浏览器版本在 Windows、macOS 和 Linux 上可能因操作系统级栈分配差异而报告不同深度。
  • 它在上下文间变化。 主线程、专用 workers、共享 workers 和 WASM 模块在同一浏览器内各有不同的栈限制。
  • 它是稳定的。 在相同的浏览器/操作系统/硬件组合上,递归限制在会话间保持一致。

Brave 浏览器团队的研究指出,栈深度提供约 2-3 比特的熵。虽然这很适度,但在复合指纹中很有价值,因为它与其他信号不捕获的浏览器引擎内部相关。报告的用户代理和观察到的栈深度之间的不匹配是强不一致信号。

技术背景

JavaScript 栈深度如何工作

当 JavaScript 函数被调用时,引擎将新帧推送到调用栈上。每个帧包含函数的局部变量、参数和返回地址。调用栈占用固定大小的内存区域。当栈满时,引擎抛出 RangeError。

最大深度取决于:

  • 栈大小分配。 操作系统为栈分配一定量的内存。Linux 默认 8 MB(可通过 ulimit -s 配置),macOS 为主线程分配 8 MB,辅助线程 512 KB,Windows 默认 1 MB。
  • 帧大小。 每个调用帧的大小不同,取决于局部变量数量和引擎的内部记账。
  • 引擎优化。 V8(Chrome)、SpiderMonkey(Firefox)和 JavaScriptCore(Safari)各自以不同的头大小、对齐要求和优化实现调用栈。
  • 执行上下文。 主线程通常比 Web Workers 有更大的栈。WASM 模块可能有自己的栈配置。

测量栈深度

追踪脚本通过计数递归调用直到异常发生来测量栈深度:

function measureDepth() {
  let depth = 0;
  function recurse() {
    depth++;
    recurse();
  }
  try {
    recurse();
  } catch (e) {
    return depth;
  }
}

结果取决于函数的帧大小。最小函数(无局部变量、无参数)产生比有许多变量的函数更高的计数,但两个值对给定的浏览器/操作系统组合都是稳定的。

上下文间的变化

栈深度测量在执行上下文间不同:

  • 主线程通常有最大的栈(通常 8 MB 或更多)。
  • 专用 Workers 有较小的栈(通常 1 MB 或 512 KB)。
  • WASM 模块可能有独立于 JavaScript 的栈限制。

这些差异在浏览器/操作系统组合内一致但在组合间变化。主线程深度和 worker 深度之间的比率本身是区分信号。

为什么现有保护无效

栈深度由 JavaScript 引擎的内部栈管理决定。它无法从 JavaScript 修改,因为:

  • 异常由引擎抛出,而非 JavaScript 代码。
  • 栈大小在线程创建时由操作系统分配。
  • 拦截 RangeError 不会改变它发生的深度。
  • 浏览器扩展在同一引擎中运行,无法访问栈配置。

常见保护方法及其局限性

JavaScript 级别基本没有保护栈深度指纹识别。值由引擎和操作系统决定,没有扩展或脚本可以改变它。

用户代理伪装改变报告的浏览器但不改变实际引擎行为。声称是 Firefox 但在 Chrome 中运行不改变 Chrome 特定的栈深度,创建可检测的不一致。

虚拟机可能改变栈深度(由于不同的操作系统配置)但引入其他指纹信号。

编译自定义浏览器构建并修改栈大小是可能的,但对大多数用户来说不切实际。它还要求匹配所有其他引擎级信号以保持一致性。

随机化测量(通过提前捕获异常)没有帮助。追踪脚本可以多次调用测量函数并取最大值,或使用不同的函数形状来三角定位实际限制。

唯一有效的方法是控制引擎的实际栈行为,这需要在浏览器级别修改。

BotBrowser 的引擎级方法

BotBrowser 通过 --bot-stack-seed 标志提供对 JavaScript 栈深度行为的直接控制。

三种控制模式

--bot-stack-seed 标志接受三种类型的值:

profile - 匹配配置文件的精确栈深度。递归限制匹配配置文件设备会产生的。这是维护一致设备身份的最准确选项。

real - 使用原生栈深度。不应用修改。递归限制反映你的实际硬件和操作系统配置。

整数种子(1-UINT32_MAX) - 生成确定性的每会话栈深度变化。每个种子产生不同但稳定的深度值。

多上下文覆盖

BotBrowser 的栈深度控制适用于所有 JavaScript 执行上下文:

  • 主线程递归限制受控。
  • Web Workers(专用和共享)的栈深度独立但与配置文件一致地受控。
  • WASM 模块栈行为也受控。

主线程和 worker 深度之间的比率匹配配置文件设备会表现的,确保跨上下文测量产生一致结果。

与其他信号的一致性

受控栈深度与配置文件的用户代理、平台和浏览器版本对齐。如果配置文件指定 Windows 10 上的 Chrome 120,栈深度就匹配 Chrome 120 在 Windows 10 上实际产生的值。用户代理声明和可观察的引擎行为之间不会有不匹配。

基于种子的变化

使用整数种子时,栈深度从种子值确定性地派生。相同的种子始终产生相同的深度。不同的种子在配置文件的浏览器/操作系统组合的真实范围内产生不同的深度。这允许你创建多个不同的身份,每个都有合理的栈深度。

配置和使用

配置文件匹配的栈深度

chrome --bot-profile="/path/to/profile.enc" \
       --bot-stack-seed=profile \
       --user-data-dir="$(mktemp -d)"

基于种子的变化

# 从种子的确定性栈深度
chrome --bot-profile="/path/to/profile.enc" \
       --bot-stack-seed=42 \
       --user-data-dir="$(mktemp -d)"

# 不同种子产生不同深度
chrome --bot-profile="/path/to/profile.enc" \
       --bot-stack-seed=43 \
       --user-data-dir="$(mktemp -d)"

原生栈深度

chrome --bot-profile="/path/to/profile.enc" \
       --bot-stack-seed=real \
       --user-data-dir="$(mktemp -d)"

与其他确定性标志结合

chrome --bot-profile="/path/to/profile.enc" \
       --bot-stack-seed=profile \
       --bot-noise-seed=42 \
       --bot-time-seed=42 \
       --user-data-dir="$(mktemp -d)"

Playwright 集成

const { chromium } = require('playwright');

const browser = await chromium.launch({
  executablePath: '/path/to/botbrowser/chrome',
  args: [
    '--bot-profile=/path/to/profile.enc',
    '--bot-stack-seed=profile'
  ]
});

const page = await browser.newPage();
await page.goto('https://example.com');

const depth = await page.evaluate(() => {
  let d = 0;
  function r() { d++; r(); }
  try { r(); } catch(e) {}
  return d;
});
console.log(`Stack depth: ${depth}`);

Puppeteer 集成

const puppeteer = require('puppeteer');

const browser = await puppeteer.launch({
  executablePath: '/path/to/botbrowser/chrome',
  defaultViewport: null,
  args: [
    '--bot-profile=/path/to/profile.enc',
    '--bot-stack-seed=profile'
  ]
});

const page = await browser.newPage();
await page.goto('https://example.com');

验证

深度测量。 在浏览器控制台中运行递归函数并记录深度。使用相同配置文件和栈种子跨会话比较。深度应完全相同。

Worker 深度测量。 在 Web Worker 中运行相同的递归测试。深度应与主线程不同(workers 有更小的栈)但跨会话一致。

跨机器验证。 在不同机器上使用相同配置文件和种子运行相同测试。栈深度应匹配。

种子变化。 改变种子并验证深度变化。这确认种子正在积极控制行为。

最佳实践

  • 使用 --bot-stack-seed=profile 获得最大准确性。 这匹配配置文件设备的精确栈深度,确保与用户代理和平台一致。
  • --bot-noise-seed--bot-time-seed 结合。 栈深度、渲染噪声和时间行为都是整体浏览器指纹的一部分。控制所有三者以获得全面保护。
  • 不要设置不切实际的深度值。 如果使用种子,生成的深度会落在配置文件浏览器的真实范围内。手动指定极端值可能与其他信号不一致。
  • 在 worker 上下文中测试。 栈深度在主线程和 workers 间不同。验证两者。
  • 理解范围。 栈深度是众多信号之一。它对整体指纹有贡献但本身不够充分。始终使用完整配置文件。

常见问题

Q: 栈深度提供多少熵? A: 大约 2-3 比特。该值区分浏览器系列、平台类型,有时还有特定版本。其价值在其他信号已被控制的复合指纹中增加。

Q: 栈深度会随浏览器更新而变化吗? A: 有时会。主要的 V8 引擎更改可以改变帧大小或栈分配,这改变最大递归深度。这是配置文件应更新以匹配当前浏览器版本的原因之一。

Q: 网站能在我不知情的情况下测量我的栈深度吗? A: 可以。测量是一个简单的递归函数调用,在毫秒内执行且不产生可见效果。不需要权限。

Q: 栈深度影响正常 JavaScript 执行吗? A: 受控栈深度只适用于测量的限制。正常的 Web 应用代码很少接近递归限制。

Q: WASM 栈深度是否单独控制? A: 是的。WASM 模块有自己的栈配置。BotBrowser 的 --bot-stack-seed 标志同时控制 WASM 和 JavaScript 栈行为。

Q: 如果我需要运行深度递归算法怎么办? A: 栈深度控制设置的是测量限制,而非人为上限。如果你的应用需要深度递归,使用 --bot-stack-seed=real 可以获得原生栈深度。

Q: headless 和 headed 模式下栈深度不同吗? A: 在标准 Chromium 中,栈深度无论显示模式如何都相同。BotBrowser 用其受控值维持这种一致性。

总结

JavaScript 栈深度是一个冷门但有效的指纹信号,在浏览器、平台和执行上下文间变化。因为它由引擎内部和操作系统级栈分配决定,无法从 JavaScript 控制。BotBrowser 的 --bot-stack-seed 标志在引擎级别提供对递归限制的直接控制,有匹配配置文件精确深度、使用原生深度或从种子生成确定性变化的选项。结合噪声和时间种子,栈深度控制完善了 BotBrowser 对确定性浏览器行为的全面方法。

相关主题请参阅什么是浏览器指纹识别确定性浏览器行为Navigator 属性保护性能时间指纹识别

#Stack-Depth#浏览器指纹识别#Recursion#Javascript#Privacy

让 BotBrowser 从研究走向生产

先用这些指南理解模型,再进入跨平台验证、隔离上下文和面向规模化的浏览器部署。