#!/usr/bin/env node 'use strict'; const NODE_CAPABILITY_GLOBALS = [ 'process', 'Buffer', 'global', 'setImmediate', 'clearImmediate', ]; const NODE_WEB_API_GLOBALS = [ 'navigator', 'localStorage', 'sessionStorage', 'performance', 'crypto', 'Crypto', 'SubtleCrypto', 'fetch', 'Headers', 'Request', 'Response', 'FormData', 'File', 'Blob', 'WebSocket', 'EventSource', 'BroadcastChannel', 'MessageChannel', 'MessagePort', 'CompressionStream', 'DecompressionStream', 'URLPattern', 'CloseEvent', 'ErrorEvent', 'AbortController', 'AbortSignal', 'Event', 'EventTarget', 'CustomEvent', 'MessageEvent', 'DOMException', 'structuredClone', 'atob', 'btoa', 'URL', 'URLSearchParams', 'TextEncoder', 'TextDecoder', 'TextEncoderStream', 'TextDecoderStream', 'ReadableStream', 'WritableStream', 'TransformStream', 'PerformanceEntry', 'PerformanceMark', 'PerformanceMeasure', 'PerformanceObserver', 'PerformanceResourceTiming', 'CryptoKey', 'WebAssembly', 'queueMicrotask', ]; const MODULE_SCOPE_NAMES = ['require', 'module', 'exports', '__dirname', '__filename']; const DENY_LIST = [ 'process', 'Buffer', 'require', 'module', 'exports', 'global', '__dirname', '__filename', 'setImmediate', 'clearImmediate', 'Error.prepareStackTrace', 'Node 专属堆栈路径', '宿主 navigator', 'navigator.userAgent=Node.js/', 'navigator.locks', '宿主 localStorage', '宿主 sessionStorage', 'performance.nodeTiming', 'performance.eventLoopUtilization', 'performance.timerify', 'performance.markResourceTiming', '宿主 fetch / undici', '宿主 WebSocket', '宿主 BroadcastChannel / MessageChannel', '宿主 URL / TextEncoder / Streams / Events / crypto 等浏览器兼容层直接透传', ]; function parseArgs(argv) { const args = { json: false, markdown: false }; for (let i = 2; i < argv.length; i++) { const a = argv[i]; if (a === '--json') args.json = true; else if (a === '--markdown') args.markdown = true; else if (a === '--help' || a === '-h') args.help = true; else throw new Error(`未知参数:${a}`); } if (!args.json && !args.markdown) args.markdown = true; return args; } function usage() { return `用法: node scripts/check_node_leakage.js --markdown node scripts/check_node_leakage.js --json 说明:检查当前 Node 宿主中常见泄露变量,并给出目标 JS 运行上下文的阻断清单。该上下文可为 vm、独立 Node 进程或显式隔离的 global。`; } function safeGet(fn, fallback = undefined) { try { return fn(); } catch (err) { return fallback === undefined ? `访问失败:${err.message || String(err)}` : fallback; } } function descriptorInfo(name) { const d = safeGet(() => Object.getOwnPropertyDescriptor(globalThis, name), null); if (!d) return null; return { enumerable: !!d.enumerable, configurable: !!d.configurable, writable: Object.prototype.hasOwnProperty.call(d, 'writable') ? !!d.writable : undefined, hasGetter: typeof d.get === 'function', hasSetter: typeof d.set === 'function', valueType: Object.prototype.hasOwnProperty.call(d, 'value') ? typeof d.value : undefined, }; } function globalStatus(name) { const type = safeGet(() => typeof globalThis[name], '访问失败'); const accessFailed = String(type).startsWith('访问失败'); return { name, visibleOnGlobalThis: type !== 'undefined' && !accessFailed, type, descriptor: descriptorInfo(name), }; } function collectNavigatorSnapshot() { if (typeof globalThis.navigator === 'undefined') return { visible: false }; const nav = globalThis.navigator; const userAgent = safeGet(() => nav.userAgent, '访问失败'); return { visible: true, constructorName: safeGet(() => nav.constructor && nav.constructor.name, ''), userAgent, isNodeUserAgent: /^Node\.js\//.test(String(userAgent || '')), platform: safeGet(() => nav.platform, ''), language: safeGet(() => nav.language, ''), languages: safeGet(() => Array.isArray(nav.languages) ? Array.from(nav.languages) : nav.languages, ''), hardwareConcurrency: safeGet(() => nav.hardwareConcurrency, ''), locksType: safeGet(() => typeof nav.locks, 'undefined'), prototypeKeys: safeGet(() => Object.getOwnPropertyNames(Object.getPrototypeOf(nav)).sort(), []), }; } function collectPerformanceSnapshot() { if (typeof globalThis.performance === 'undefined') return { visible: false }; const perf = globalThis.performance; return { visible: true, constructorName: safeGet(() => perf.constructor && perf.constructor.name, ''), hasNodeTiming: safeGet(() => 'nodeTiming' in perf, false), hasEventLoopUtilization: safeGet(() => 'eventLoopUtilization' in perf, false), hasTimerify: safeGet(() => 'timerify' in perf, false), hasMarkResourceTiming: safeGet(() => 'markResourceTiming' in perf, false), prototypeKeys: safeGet(() => Object.getOwnPropertyNames(Object.getPrototypeOf(perf)).sort(), []), }; } function collectStorageSnapshot(name) { const type = safeGet(() => typeof globalThis[name], '访问失败'); if (type === 'undefined' || String(type).startsWith('访问失败')) return { visible: false, name, type }; const storage = safeGet(() => globalThis[name], null); return { visible: true, name, type, constructorName: safeGet(() => storage && storage.constructor && storage.constructor.name, ''), descriptor: descriptorInfo(name), }; } function collect() { const moduleScope = Object.fromEntries(MODULE_SCOPE_NAMES.map(name => [name, safeGet(() => eval(`typeof ${name}`) !== 'undefined', false)])); const capabilityGlobals = NODE_CAPABILITY_GLOBALS.map(globalStatus); const webApiGlobals = NODE_WEB_API_GLOBALS.map(globalStatus); const navigatorSnapshot = collectNavigatorSnapshot(); const performanceSnapshot = collectPerformanceSnapshot(); const storageSnapshots = [collectStorageSnapshot('localStorage'), collectStorageSnapshot('sessionStorage')]; const hostSignals = []; if (navigatorSnapshot.visible && navigatorSnapshot.isNodeUserAgent) hostSignals.push(`navigator.userAgent=${navigatorSnapshot.userAgent}`); if (navigatorSnapshot.visible && navigatorSnapshot.locksType !== 'undefined') hostSignals.push('navigator.locks 可见'); if (performanceSnapshot.hasNodeTiming) hostSignals.push('performance.nodeTiming 可见'); if (performanceSnapshot.hasEventLoopUtilization) hostSignals.push('performance.eventLoopUtilization 可见'); if (performanceSnapshot.hasTimerify) hostSignals.push('performance.timerify 可见'); if (typeof process !== 'undefined' && process.versions && process.versions.undici) hostSignals.push(`process.versions.undici=${process.versions.undici}`); return { note: '当前脚本运行在 Node 宿主中,存在这些变量是正常现象;目标网页 JS 所在运行上下文中不应暴露 Node 能力,也不应直接复用宿主 Node Web API 兼容层。', host: { node: process.version, platform: process.platform, arch: process.arch, v8: process.versions.v8, undici: process.versions.undici || '' }, capabilityGlobals, webApiGlobals, moduleScope, navigator: navigatorSnapshot, performance: performanceSnapshot, storage: storageSnapshots, hostSignals, denyList: DENY_LIST, contextExpectations: { process: 'undefined', Buffer: 'undefined', require: 'undefined', module: 'undefined', global: 'undefined', functionProcess: 'Function("return typeof process")() 应为 "undefined"', navigatorUserAgent: '不能以 Node.js/ 开头,必须来自浏览器采样或 fixture', navigatorLocks: '不应暴露 Node 的 navigator.locks,除非已按浏览器样本明确实现', performanceNodeTiming: 'performance.nodeTiming 应不存在', performanceEventLoopUtilization: 'performance.eventLoopUtilization 应不存在', performanceTimerify: 'performance.timerify 应不存在', storage: 'localStorage/sessionStorage 必须是浏览器页面级 Storage 实现,不得复用 Node 宿主 Storage', network: 'fetch/WebSocket 等不得透传宿主实现,探测模式用桩函数,最终请求用已确认 TLS 指纹客户端', webCompatGlobals: 'URL/TextEncoder/Streams/Events/crypto/WebAssembly 等浏览器同名 API 如参与检测,也必须按浏览器样本或补环境实现安装,不得盲目透传宿主构造器', }, recommendations: [ '不要把宿主函数、宿主数组、宿主 URL/TextEncoder 构造器直接塞进目标 JS 运行上下文。', 'Node 官方文档显示 navigator 是 v21.0.0 新增,不是 Node 20;如存在宿主 navigator,先删除或隔离,再安装浏览器式 Navigator。', 'Node 22.4+ 如存在宿主 localStorage/sessionStorage,先删除或隔离,再安装浏览器式 Storage。', '不要透传宿主 performance;需移除 nodeTiming、eventLoopUtilization、timerify 等 Node 专属能力。', 'Web Streams、Events、URL、TextEncoder、crypto、WebAssembly、queueMicrotask 等同名 API 也要按目标浏览器样本决定是否覆盖,不能默认复用 Node 宿主对象。', '在目标 JS 运行上下文内部定义 fetch、XHR、atob、console 等函数,避免 constructor.constructor 泄露。', '目标 JS 运行前检查 Function("return typeof process")()。', '最终 runner 中不要为了调试暴露 require、process、Buffer。', ], }; } function renderGlobalList(lines, title, items) { lines.push('', title); for (const g of items) lines.push(`- ${g.name}:${g.visibleOnGlobalThis ? '宿主可见' : '宿主不可见'}(${g.type})`); } function renderMarkdown(result) { const lines = ['# Node 泄露阻断检查', '', `- Node.js:${result.host.node}`, `- 平台:${result.host.platform}-${result.host.arch}`, `- undici:${result.host.undici || '未暴露'}`, '', `说明:${result.note}`]; renderGlobalList(lines, '## 宿主 Node 能力变量状态', result.capabilityGlobals); renderGlobalList(lines, '## 宿主 Node Web API 兼容层状态', result.webApiGlobals); lines.push('', '## 模块作用域变量'); for (const [k, v] of Object.entries(result.moduleScope)) lines.push(`- ${k}:${v ? '存在' : '不存在'}`); lines.push('', '## navigator 宿主信号'); if (!result.navigator.visible) lines.push('- 宿主 navigator 不可见'); else { lines.push(`- userAgent:${result.navigator.userAgent}`); lines.push(`- 是否 Node userAgent:${result.navigator.isNodeUserAgent ? '是' : '否'}`); lines.push(`- platform:${result.navigator.platform}`); lines.push(`- language:${result.navigator.language}`); lines.push(`- hardwareConcurrency:${result.navigator.hardwareConcurrency}`); lines.push(`- locks:${result.navigator.locksType}`); } lines.push('', '## performance 宿主信号'); if (!result.performance.visible) lines.push('- 宿主 performance 不可见'); else { lines.push(`- nodeTiming:${result.performance.hasNodeTiming ? '可见' : '不可见'}`); lines.push(`- eventLoopUtilization:${result.performance.hasEventLoopUtilization ? '可见' : '不可见'}`); lines.push(`- timerify:${result.performance.hasTimerify ? '可见' : '不可见'}`); lines.push(`- markResourceTiming:${result.performance.hasMarkResourceTiming ? '可见' : '不可见'}`); } if (result.hostSignals.length) { lines.push('', '## 已发现宿主特征信号'); for (const item of result.hostSignals) lines.push(`- ${item}`); } lines.push('', '## 目标运行上下文阻断清单'); for (const name of result.denyList) lines.push(`- ${name}`); lines.push('', '## 建议'); for (const item of result.recommendations) lines.push(`- ${item}`); return lines.join('\n') + '\n'; } try { const args = parseArgs(process.argv); if (args.help) { console.log(usage()); process.exit(0); } const result = collect(); if (args.json) console.log(JSON.stringify(result, null, 2)); if (args.markdown) process.stdout.write(renderMarkdown(result)); } catch (err) { console.error(err.message || String(err)); console.error(usage()); process.exit(1); }