# Node 泄露阻断与静默失败排查 本文件用于进入 Node.js 补环境前后,防止目标 JS 误读 Node 环境,并排查“不报错但结果不一致”的问题。 ## Node 泄露阻断原则 目标网页 JS 应看到浏览器环境,而不是 Node.js 环境。探测脚本和最终 runner 都必须先阻断 Node 能力变量,再安装浏览器式环境对象。 基础 Node 能力泄露清单: ```text process, Buffer, require, module, exports, global, __dirname, __filename, setImmediate, clearImmediate, Error.prepareStackTrace, Node 专属堆栈路径 ``` 新版 Node Web API 兼容层也可能成为泄露面,必须删除、隔离或用浏览器真实样本覆盖: ```text navigator, localStorage, sessionStorage, performance, fetch, Headers, Request, Response, FormData, File, Blob, WebSocket, EventSource, BroadcastChannel, MessageChannel, MessagePort, CompressionStream, DecompressionStream, URLPattern, CloseEvent, ErrorEvent ``` 以下对象在浏览器中也常见,但 Node 的实现、原型、属性描述符、`toString`、异常类型、内部状态或网络栈可能与真实浏览器不同;只要目标 JS 会读取或参与签名,就不要盲目透传宿主实现,应按浏览器样本、RuyiTrace 证据或可控桩函数安装: ```text AbortController, AbortSignal, Event, EventTarget, CustomEvent, MessageEvent, DOMException, structuredClone, atob, btoa, URL, URLSearchParams, TextEncoder, TextDecoder, TextEncoderStream, TextDecoderStream, ReadableStream, WritableStream, TransformStream, PerformanceEntry, PerformanceMark, PerformanceMeasure, PerformanceObserver, PerformanceResourceTiming, Crypto, CryptoKey, SubtleCrypto, WebAssembly, queueMicrotask ``` 版本结论以 Node 官方文档为准: - `Navigator` / `navigator` 是 Node v21.0.0 新增,不是 Node 20 官方新增;`navigator.userAgent` 自 v21.1.0 起可能返回 `Node.js/`。 - `navigator.language` / `languages` / `platform` 自 Node v21.2.0 起反映宿主系统或 ICU,`navigator.locks` 自 Node v24.5.0 起可能存在。 - `localStorage` / `sessionStorage` 是 Node v22.4.0 引入的 Web Storage 兼容层;Node v25+ 行为又有变化,不能当作页面级浏览器 Storage 直接复用。 - Node 的 `fetch` 基于 undici,可通过 `process.versions.undici` 看到宿主实现版本;最终真实请求仍应由已确认的 TLS 指纹兼容客户端完成,不要把宿主 `fetch` 当作浏览器网络栈。 - Node 全局 `performance` 是 `perf_hooks.performance`,可能暴露 `nodeTiming`、`eventLoopUtilization`、`timerify`、`markResourceTiming` 等浏览器没有或语义不同的字段。 特别注意:补环境前一旦检测到这些宿主对象,必须先移除、隔离或显式覆盖,再安装浏览器采样值;不得直接复用宿主对象。若目标真实浏览器也存在同名 API,例如 `navigator.locks`、`crypto`、`ReadableStream`,也应按浏览器样本补其值、描述符和原型链,而不是沿用 Node 宿主对象。 ### 运行上下文隔离要求 - 使用 `vm.createContext` 时,不要把宿主函数、宿主数组、宿主类直接塞进目标运行上下文。 - `URL`、`TextEncoder`、`fetch`、`atob`、`console` 等应在目标运行上下文内定义,或确认不会通过 `constructor.constructor` 拿到宿主 `process`。 - 禁止把 `require`、`process`、`Buffer` 作为调试便利变量暴露给目标 JS。 - 禁止直接复用宿主 `navigator`、`performance`、`localStorage`、`sessionStorage`、`fetch`、`WebSocket` 等 Node Web API 兼容层;这些对象必须由 `env.js` 按浏览器样本显式安装。 - 如果最终 runner 无法使用隔离 global,启动后第一步先 `Reflect.deleteProperty(globalThis, "navigator")` 等方式移除宿主 Web API,再用 `Object.defineProperty` 安装浏览器式对象。 - 对 `URL`、`URLSearchParams`、`TextEncoder`、`TextDecoder`、`crypto`、`WebAssembly`、Streams、Events 等“浏览器也有但 Node 也提供”的对象,先判断目标是否会检测原型、描述符、异常或输出;若会检测,使用浏览器采样值或补环境实现,不要直接透传宿主构造器。 - 目标 JS 需要的环境对象要通过 `env.js` 明确安装,不要把 Node 全局对象透传。 - 最终交付前运行 `scripts/check_node_leakage.js`,并在 notes 中记录阻断结论。 ### 快速自检表达式 在目标 JS 所在运行上下文中执行以下表达式,期望均不暴露 Node 能力: ```javascript typeof process === "undefined" typeof Buffer === "undefined" typeof require === "undefined" typeof module === "undefined" typeof global === "undefined" Function("return typeof process")() === "undefined" !/^Node\.js\//.test(String(navigator && navigator.userAgent || "")) !("nodeTiming" in performance) !("eventLoopUtilization" in performance) !("timerify" in performance) typeof process === "undefined" || !process.versions || !process.versions.undici ``` 如果任一表达式暴露 Node 能力,或 `navigator.userAgent` 显示 `Node.js/`,或 `performance` 暴露 `nodeTiming` / `eventLoopUtilization` / `timerify`,先修运行上下文隔离,不要继续补环境。 ### Node 21+ navigator 与 Node Web API 覆盖规则 - 检测到 `globalThis.navigator` 存在时,不要假设它是浏览器 navigator;先检查 `navigator.userAgent` 是否以 `Node.js/` 开头。 - 若宿主 Node 提供 `navigator`,补环境初始化前必须删除或遮蔽宿主对象,再按浏览器 fixture 安装 `Navigator.prototype`、getter、`plugins`、`mimeTypes`、`languages` 等。 - 检测到宿主 `performance` 时,必须确认目标上下文不暴露 Node 专属 `nodeTiming`、`eventLoopUtilization`、`timerify`、`markResourceTiming`。 - 检测到宿主 `localStorage` / `sessionStorage` 时,必须替换为浏览器页面级 Storage 语义,并使用请求样本、RuyiTrace 或 fixture 中的键值。 - 检测到宿主 `fetch` / `WebSocket` / `BroadcastChannel` / `MessageChannel` 等时,不要直接透传到目标 JS;探测模式使用桩函数记录调用,最终请求由已确认 TLS 指纹兼容客户端实现。 ## 静默失败排查清单 当目标 JS 能跑完但 sign/token 不一致,按以下顺序排查: 1. **请求样本一致性**:URL、Query 排序、Body 字符串、Header 大小写、Content-Type、Referer、Origin 是否一致。 2. **SDK 初始化参数**:appId、版本号、平台、渠道、页面路径、nonce、server seed、初始化时机是否一致。 3. **登录态和存储**:Cookie、localStorage、sessionStorage、IndexedDB 摘要是否一致;敏感值不要写入最终报告。 4. **时间和随机数**:`Date.now`、`new Date()`、`performance.now`、`Math.random`、`crypto.getRandomValues` 是否可复现。 5. **浏览器指纹**:UA、language、timezone、screen、devicePixelRatio、plugins、mimeTypes、canvas、WebGL。 6. **加载顺序**:目标 JS 是否在 env 安装前读取环境;入口模块是否需要先执行 runtime chunk。 7. **toString/native-like**:函数、getter、setter、构造函数是否返回浏览器风格字符串。 8. **属性描述符**:enumerable、configurable、writable、getter/setter 是否与浏览器接近。 9. **原型链**:实例 `constructor`、`instanceof`、`Object.prototype.toString` 是否合理。 10. **特殊对象**:`document.all`、`HTMLAllCollection`、`navigator.plugins`、`mimeTypes` 是否被检测。 11. **动态代码**:eval、new Function、setTimeout 字符串、混淆解包结果是否漏执行。 12. **Worker/WASM**:是否实际在 Worker、iframe、WASM 中生成参数,需要单独搬运消息链。 ## Cookie 过期的静默失败排查 Cookie 相关失败先分类,再决定是否让用户补样本: 1. **登录态 / 授权 Cookie**:如果 Cookie 与账号、会话、SSO、Authorization 或权限绑定,不要尝试绕过;让用户在所选取证工具中手动登录,或提供授权样本。 2. **非登录 Cookie**:如果目标不需要登录,或 Cookie 是设备标识、首访标识、风控标识、JS 生成值、challenge 派生值,不要默认要求用户重新提供新 Cookie;应分析生成链路。 3. **来源定位**: - 服务端 `Set-Cookie`:检查是否需要先发起首访 / challenge 请求,并在最终 Node.js / Python 请求客户端中维护 Cookie jar。 - 前端 `document.cookie = ...`:用 Hook / RuyiTrace 查 writer 和调用栈。 - JS 计算:按 `source → entry → builder → writer` 纳入补环境。 - Storage 派生:检查 localStorage / sessionStorage / IndexedDB 摘要是否与浏览器样本一致。 4. **最终交付**:非登录 Cookie 的生成或刷新应进入最终入口脚本;入口运行后先生成 / 刷新 Cookie,再生成加密参数并发送 Node.js / Python 模拟请求。 不要把“重新拿一份新 Cookie”作为默认答案;它只能用于登录态 / 授权态、不可复现的一次性服务端状态,或用户明确只做离线样本复现的场景。 ## 六项纯计算预检 补环境前先证明 Node 与浏览器的基础纯计算差异不会影响结果。若差异明显,先记录,不要直接归咎于环境对象。 | 项目 | 检查内容 | 目的 | |---|---|---| | Math | `Math.imul`、浮点精度、三角函数边界 | 排除 JS 引擎数学差异 | | String/Unicode | UTF-8、emoji、中文、surrogate pair | 排除编码差异 | | Array/Object | sort、JSON.stringify、属性枚举顺序 | 排除序列化差异 | | Date/Timezone | 时区、ISO 字符串、时间戳 | 排除时间格式差异 | | Encoding | atob/btoa、TextEncoder、URLSearchParams | 排除编码和 Query 序列化差异 | | Random | Math.random、crypto.getRandomValues 是否被依赖 | 判断是否要固定随机源 | 使用: ```bash node scripts/precheck_runtime.js --markdown node scripts/precheck_runtime.js --json ``` 浏览器侧也运行同类片段,把结果放入 `notes/runtime-precheck-browser.json`,再与 Node 结果比对。 ## init 参数检查 很多 SDK 签名失败不是缺环境,而是初始化参数不完整。补环境前必须确认: - SDK 初始化函数是否已执行。 - appId / tenantId / clientId / channel / version 是否来自页面配置。 - 初始化是否读取 meta 标签、script 标签、window 全局配置或 localStorage。 - 是否需要先加载 runtime chunk、vendor chunk、polyfill chunk。 - 初始化是否异步等待网络、Worker、WASM 或事件。 - 入口函数调用前是否需要触发页面事件或队列 flush。 输出模板: ```markdown ## SDK / 入口初始化检查 - 初始化函数: - 初始化参数来源: - 必需全局配置: - 必需存储值: - 必需 Cookie: - 必需加载顺序: - 当前缺失: - 是否可以调用入口:是 / 否 ```