Files
hello_js_reverse_skill/references/environment-patch.md
T
白夜 b5cf8b121a feat(v2.4.0): JSVMP 双路径攻克体系 + jsdom 环境补丁知识库
基于实战经验重构 JSVMP 分析框架:

- SKILL.md: 新增路径选择决策树(路径 A 算法追踪 vs 路径 B 环境伪装)
- SKILL.md: 路径 B「环境伪装六步法」完整方法论(采集→对比→分级→补丁→验证→端到端)
- SKILL.md: 新增场景 10「JSVMP + jsdom 环境伪装」、解法模式 E
- SKILL.md: 标注三板斧适用边界与局限,经验法则扩充至 22 条
- references/jsdom-env-patches.md: 新增 jsdom 环境补丁知识库(Function.toString 三层防御 / plugins 完整模拟 / DOM 布局属性 / 30+ API 存根)
- cases/: 新增经验库系统(模板 + 首个 JSVMP 实战案例含 58 项环境检测差异全表)
- references/jsvmp-analysis.md: JSVMP 专项分析详细指南
- references/troubleshooting.md: 常见问题排查指南
- templates/python-request/: Python 项目模板
- 各 references 文档增强、Python 技术栈支持、MCP v1.1 适配(44→52 工具)

Made-with: Cursor
2026-04-02 18:10:05 +08:00

11 KiB
Raw Blame History

环境补全指南

概述

将浏览器端 JS 代码移植到 Node.js 执行时,需要模拟浏览器环境。本指南提供各种场景下的 环境补全方案,从最小补全到完整模拟。

原则

  1. 最小补全优先:只补目标代码实际访问的 API,不要一次性补全所有浏览器 API
  2. 按需扩展:运行报错时再补对应的属性/方法
  3. 真实值优先:尽量使用真实浏览器采集的值,而非随意伪造

1. Node.js vm 沙箱最小环境

基础模板

const vm = require('vm');

function createMinimalSandbox(options = {}) {
    const sandbox = {
        // 全局对象
        window: null,
        self: null,
        globalThis: null,
        
        // DOM 最小模拟
        document: {
            cookie: options.cookie || '',
            createElement: (tag) => ({
                tagName: tag.toUpperCase(),
                style: {},
                setAttribute: () => {},
                getAttribute: () => null,
                appendChild: () => {},
                innerHTML: '',
                src: '',
            }),
            getElementById: () => null,
            getElementsByTagName: () => [],
            getElementsByClassName: () => [],
            querySelector: () => null,
            querySelectorAll: () => [],
            head: { appendChild: () => {} },
            body: { appendChild: () => {} },
            location: { href: options.url || 'https://example.com', hostname: 'example.com' },
            referrer: options.referrer || '',
            title: '',
            readyState: 'complete',
        },
        
        // Navigator
        navigator: {
            userAgent: options.userAgent || 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
            appCodeName: 'Mozilla',
            appName: 'Netscape',
            appVersion: '5.0',
            platform: 'MacIntel',
            language: 'zh-CN',
            languages: ['zh-CN', 'zh', 'en'],
            cookieEnabled: true,
            onLine: true,
            plugins: { length: 3 },
            mimeTypes: { length: 4 },
            webdriver: false,
            hardwareConcurrency: 8,
            maxTouchPoints: 0,
        },
        
        // Location
        location: {
            href: options.url || 'https://example.com',
            protocol: 'https:',
            host: 'example.com',
            hostname: 'example.com',
            port: '',
            pathname: '/',
            search: '',
            hash: '',
            origin: 'https://example.com',
        },
        
        // Screen
        screen: {
            width: 1920,
            height: 1080,
            availWidth: 1920,
            availHeight: 1055,
            colorDepth: 24,
            pixelDepth: 24,
        },
        
        // 定时器
        setTimeout: setTimeout,
        setInterval: setInterval,
        clearTimeout: clearTimeout,
        clearInterval: clearInterval,
        
        // 内置对象
        String, Array, Object, Math, Date, RegExp, JSON, Map, Set, WeakMap, WeakSet,
        parseInt, parseFloat, isNaN, isFinite, NaN, Infinity, undefined,
        encodeURIComponent, decodeURIComponent,
        encodeURI, decodeURI,
        escape, unescape,
        Error, TypeError, RangeError, SyntaxError, ReferenceError,
        ArrayBuffer, Uint8Array, Int32Array, Float64Array, DataView,
        Promise,
        Proxy, Reflect,
        Symbol,
        
        // Base64
        btoa: (str) => Buffer.from(str, 'binary').toString('base64'),
        atob: (b64) => Buffer.from(b64, 'base64').toString('binary'),
        
        // Console(用于调试)
        console: console,
    };
    
    // 循环引用
    sandbox.window = sandbox;
    sandbox.self = sandbox;
    sandbox.globalThis = sandbox;
    sandbox.top = sandbox;
    sandbox.parent = sandbox;
    sandbox.frames = sandbox;
    
    return sandbox;
}

// 使用示例
function runInSandbox(code, options = {}) {
    const sandbox = createMinimalSandbox(options);
    vm.createContext(sandbox);
    
    try {
        vm.runInContext(code, sandbox, {
            timeout: options.timeout || 5000,
            filename: options.filename || 'sandbox.js',
        });
    } catch (e) {
        console.error('沙箱执行错误:', e.message);
        throw e;
    }
    
    return sandbox;
}
function createCookieTrapSandbox(options = {}) {
    const sandbox = createMinimalSandbox(options);
    const cookies = {};
    
    Object.defineProperty(sandbox.document, 'cookie', {
        get() {
            return Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join('; ');
        },
        set(val) {
            const parts = val.split(';')[0].split('=');
            const name = parts[0].trim();
            const value = parts.slice(1).join('=').trim();
            cookies[name] = value;
            console.log(`[Sandbox] Cookie Set: ${name}=${value}`);
        }
    });
    
    sandbox._cookies = cookies;
    return sandbox;
}

2. WASM 环境补全

基础 WASM 加载

const fs = require('fs');

async function loadWasm(wasmPath, importObject = {}) {
    const wasmBuffer = fs.readFileSync(wasmPath);
    
    const defaultImports = {
        env: {
            memory: new WebAssembly.Memory({ initial: 256 }),
            table: new WebAssembly.Table({ initial: 0, element: 'anyfunc' }),
            abort: () => { throw new Error('WASM abort'); },
            ...importObject.env,
        },
        wasi_snapshot_preview1: {
            fd_write: () => 0,
            fd_close: () => 0,
            fd_seek: () => 0,
            proc_exit: () => {},
            ...importObject.wasi_snapshot_preview1,
        },
        ...importObject,
    };
    
    const result = await WebAssembly.instantiate(wasmBuffer, defaultImports);
    return result.instance;
}

wasm-bindgen (Rust) 环境补全

class Window {
    constructor() {
        this.document = {
            body: {},
            createElement: () => ({}),
            getElementById: () => null,
        };
    }
}

function patchWasmBindgenEnv() {
    const win = new Window();
    win.window = win;
    win.self = win;
    
    globalThis.Window = Window;
    globalThis.window = win;
    globalThis.self = win;
    globalThis.document = win.document;
    
    // wasm-bindgen 可能检查 instanceof
    // 确保 win instanceof Window === true
}

Emscripten 环境补全

const Module = {
    print: console.log,
    printErr: console.error,
    TOTAL_MEMORY: 16777216,
    noInitialRun: true,
    onRuntimeInitialized: function() {
        console.log('WASM Runtime Ready');
    }
};

3. jQuery 模拟

function createjQueryStub() {
    const $ = function(selector) {
        return {
            length: 1,
            val: () => '',
            text: () => '',
            html: () => '',
            attr: () => '',
            css: () => ({}),
            find: () => $(''),
            each: (fn) => { fn(0, {}); },
            click: () => {},
            on: () => {},
            ajax: $.ajax,
        };
    };
    
    $.ajax = function(options) {
        console.log('[jQuery] $.ajax:', options.url, options.data);
        return { done: (fn) => ({ fail: () => ({}) }) };
    };
    
    $.get = $.post = $.ajax;
    $.fn = $.prototype = {};
    $.extend = Object.assign;
    
    return $;
}

4. XMLHttpRequest 模拟

function createXHRStub(interceptor) {
    return class XMLHttpRequest {
        constructor() {
            this.readyState = 0;
            this.status = 0;
            this.responseText = '';
            this.response = '';
        }
        
        open(method, url) {
            this._method = method;
            this._url = url;
            this.readyState = 1;
        }
        
        setRequestHeader(name, value) {
            this._headers = this._headers || {};
            this._headers[name] = value;
        }
        
        send(body) {
            if (interceptor) {
                interceptor({
                    method: this._method,
                    url: this._url,
                    headers: this._headers,
                    body: body,
                });
            }
            this.readyState = 4;
            this.status = 200;
            if (this.onreadystatechange) this.onreadystatechange();
            if (this.onload) this.onload();
        }
        
        addEventListener(event, handler) {
            this['on' + event] = handler;
        }
    };
}

5. 环境检测绕过清单

检测项 Node.js 默认 需要补全的值
typeof window undefined object
typeof document undefined object
typeof navigator undefined object
typeof process object (暴露) 需要删除
typeof module object (暴露) 需要删除
typeof global object (暴露) 视情况删除
navigator.webdriver N/A false / undefined
window.chrome N/A { runtime: {} }
navigator.plugins.length N/A > 0

清除 Node.js 特征

function hideNodeFeatures(sandbox) {
    delete sandbox.process;
    delete sandbox.module;
    delete sandbox.exports;
    delete sandbox.require;
    delete sandbox.global;
    delete sandbox.__filename;
    delete sandbox.__dirname;
    delete sandbox.Buffer;
}

6. 完整环境补全模板(jsdom)

当最小补全不够时,使用 jsdom 提供完整 DOM 环境:

const { JSDOM } = require('jsdom');

function createFullBrowserEnv(options = {}) {
    const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
        url: options.url || 'https://example.com',
        referrer: options.referrer || '',
        contentType: 'text/html',
        pretendToBeVisual: true,
        runScripts: 'dangerously',
        resources: 'usable',
    });
    
    const { window } = dom;
    
    // 补充 jsdom 缺少的 API
    if (!window.btoa) {
        window.btoa = (str) => Buffer.from(str, 'binary').toString('base64');
    }
    if (!window.atob) {
        window.atob = (b64) => Buffer.from(b64, 'base64').toString('binary');
    }
    
    return { dom, window, document: window.document };
}

MCP 辅助确定环境需求

1. [camoufox-reverse] search_code(keyword="window|document|navigator|location|screen")
   → 先在已加载脚本中识别代码访问了哪些浏览器 API

2. [camoufox-reverse] add_init_script(script="记录被访问的全局变量和属性")
   + reload()
   + get_console_logs
   → 确定运行时真正访问了哪些环境字段

3. [camoufox-reverse] set_breakpoint_via_hook(target_function="环境检测函数路径")
   + get_breakpoint_data
   → 在环境检测函数入口捕获入参、返回值和调用栈,确认需要补全的关键分支