Puppeteer 中的 CDP 泄露:反欺诈系统如何通过 Chrome DevTools Protocol 检测自动化

Puppeteer 中的 CDP 泄露:反欺诈系统如何通过 Chrome DevTools Protocol 检测自动化
Markus_automation
Markus_automation

Expert in data parsing and automation

在浏览器自动化工具中,Puppeteer 长期以来一直占有特殊的地位。与笨重的 Selenium 生态系统不同,它在 Node.js 环境中开箱即用地为开发人员提供了对 Chromium 原生、高性能的控制。

由于其庞大的开箱即用插件生态系统以及与 V8 引擎的深度集成,Puppeteer 已成为行业标准。您可以使用 Puppeteer,连接 puppeteer-extra-plugin-stealth,购买优质代理,仔细伪装 Canvas 和 WebGL 指纹,对于许多任务来说,这就足够了。

然而,在保护严密的网站上,此类爬虫可能会遇到困难。Cloudflare、Akamai 或 DataDome 等保护系统可能会开始拦截您的会话。为什么会这样?

问题不在于您的代码逻辑或代理的质量。原因恰恰在于让 Puppeteer 如此强大且便捷的基石:Chrome 开发者工具协议 (CDP)。这种底层控制协议会留下特定的数字痕迹,现代反欺诈算法可以检测到这些痕迹并用其识别机器人。

让我们来看看这些泄露是如何发生的,以及为什么针对标准 Puppeteer 设置的表面伪装技术不再有效。

内容

使用Octo Browser维护您的在线匿名性。您真实的数字指纹无法被追踪。

什么是 CDP,它为什么会留下数字痕迹?

Chrome DevTools Protocol 提供了对浏览器架构的底层访问。它最初是为调试、检测和性能剖析设计的,而不是为了隐秘爬取。

该协议通过与 V8 引擎直接交互的 WebSocket 连接起作用。当您的脚本发送命令(例如点击或页面导航)时,浏览器会打开一个本地套接字进行双向通信。这个过程不可避免地会生成内部日志,影响内存分配,并在隔离的浏览器环境中留下痕迹。安全系统可以分析这些微小变化和时间模式以检测自动化行为。因此,仅由于连接的存在就可能暴露出浏览器自动化的事实。

CDP 泄漏与传统指纹识别有何不同

必须明白,CDP 泄漏和浏览器指纹泄漏代表了两个根本不同的检测方向。

  • 传统指纹识别 (Canvas, WebGL, 字体) 用于识别独特的硬件特征和渲染行为。指纹不一致或伪造错误会使反爬虫系统检测到操纵行为,并将用户归类为可疑用户。

  • CDP 泄漏暴露了代码执行的行为和结构指标。它们暴露了浏览器正在被远程控制的事实。

您可能拥有完美且独特的 WebGL 指纹,但自动化标记可以瞬间使这一优势失效。检测浏览器指纹需要相当多的资源和处理时间,而检测环境变量或调用栈则几乎不需要成本。这正是反欺诈系统更青睐基于 CDP 检测的原因。它使他们能够在基础的协议级检测期间过滤掉机器人,从而节省本应花在更昂贵的深度行为分析上的服务器资源。

Chrome DevTools Protocol 泄漏的构造解析

执行上下文标记 (Execution Context markers)

Puppeteer 的主要弱点之一在于它在浏览器内执行代码的具体方式。在底层,page.evaluate() 方法依赖于 CDP 命令 Runtime.evaluate

向浏览器注入脚本时,现代 Puppeteer 版本会生成一个与该代码片段关联的特定源 URL (Source URL)。如果脚本执行期间发生错误,V8 引擎会创建一个标准的堆栈跟踪,其中可能包含类似于以下的内容:

at pptr:evaluate;C:\Users\Admin\Projects\...\main.js:17:14
at pptr:evaluate;C:\Users\Admin\Projects\...\main.js:17:14

通过重写基础浏览器函数,反欺诈系统可以故意触发不可见的错误并检查 Error.stack 对象。它们可能会发现:

  • pptr: 前缀,直接指向 Puppeteer 的引用。

  • 本地机器或服务器上文件的绝对路径,以 C:\/var/www/... 开头。

真实用户的浏览器在访问公共网站时绝不会执行来自本地文件系统路径的代码。因此,此类标记代表了自动化的确凿证据。

Execution Context markers

更改您的 IP 地址或伪造 Canvas 指纹并不能解决此问题。要隐藏这些标记,您必须修改库本身或浏览器执行环境。

最可靠的解决方案是在命令发送到浏览器之前,直接从 Puppeteer 的源代码中移除 pptr:evaluate 标记。由于在每次 npm install 之后手动搜索和编辑文件是不切实际的,可以使用一个简单的补丁程序:

const fs = require('fs');
const path = require('path');

// Path to ExecutionContext.js in recent Puppeteer versions
const targetFile = path.resolve(__dirname, 'node_modules/puppeteer-core/lib/cjs/puppeteer/cdp/ExecutionContext.js');

if (fs.existsSync(targetFile)) {
    let content = fs.readFileSync(targetFile, 'utf8');
    // Replace the pptr:evaluate prefix with an anonymous call
    // and remove the local file path
    const patchedContent = content.replace(/pptr:evaluate;.*?\\n/g, 'anonymous:evaluation;\n');
    fs.writeFileSync(targetFile, patchedContent, 'utf8');
    console.log('Puppeteer patched successfully. pptr:evaluate markers removed.');
}
const fs = require('fs');
const path = require('path');

// Path to ExecutionContext.js in recent Puppeteer versions
const targetFile = path.resolve(__dirname, 'node_modules/puppeteer-core/lib/cjs/puppeteer/cdp/ExecutionContext.js');

if (fs.existsSync(targetFile)) {
    let content = fs.readFileSync(targetFile, 'utf8');
    // Replace the pptr:evaluate prefix with an anonymous call
    // and remove the local file path
    const patchedContent = content.replace(/pptr:evaluate;.*?\\n/g, 'anonymous:evaluation;\n');
    fs.writeFileSync(targetFile, patchedContent, 'utf8');
    console.log('Puppeteer patched successfully. pptr:evaluate markers removed.');
}

如果无法修改 node_modules,可以在运行时通过在目标页面加载之前注入伪装脚本来过滤这些标记。该脚本重写了原生的 Error 对象行为安全清理堆栈跟踪:

await page.evaluateOnNewDocument(() => {
    // Save the original Error constructor
    const NativeError = window.Error;

    window.Error = function(...args) {
        const err = new NativeError(...args);
        const originalStack = err.stack;

        if (originalStack) {
            Object.defineProperty(err, 'stack', {
                get: function() {
                    // Break stack trace into strings and remove Puppeteer-related entries from it
                    return originalStack
                        .split('\n')
                        .filter(line => !line.includes('pptr:evaluate'))
                        .join('\n');
                }
            });
        }
        return err;
    };
    
    // Restore the prototype chain to avoid detection
    window.Error.prototype = NativeError.prototype;
});
await page.evaluateOnNewDocument(() => {
    // Save the original Error constructor
    const NativeError = window.Error;

    window.Error = function(...args) {
        const err = new NativeError(...args);
        const originalStack = err.stack;

        if (originalStack) {
            Object.defineProperty(err, 'stack', {
                get: function() {
                    // Break stack trace into strings and remove Puppeteer-related entries from it
                    return originalStack
                        .split('\n')
                        .filter(line => !line.includes('pptr:evaluate'))
                        .join('\n');
                }
            });
        }
        return err;
    };
    
    // Restore the prototype chain to avoid detection
    window.Error.prototype = NativeError.prototype;
});
As a result, instead of exposing local file paths, the stack trace will display something like this

结果是,堆栈跟踪将显示类似于以下内容,而不是暴露本地文件路径

Page.addScriptToEvaluateOnNewDocument 的脆弱性

隐藏浏览器自动化的尝试(包括使用流行的 stealth 插件)通常依赖于在目标网站加载之前注入伪装的 JavaScript。在 Puppeteer 中,这通常通过 Page.addScriptToEvaluateOnNewDocument 命令来完成。

然而,该方法本身的使用在执行环境中留下了明显的痕迹。

  1. 时间异常。该命令强制 V8 引擎在创建页面上下文时,在 HTML 解析器开始工作之前同步执行您的代码。注入大型脚本会在 document_start 阶段引入可检测到的微小延迟。反欺诈系统会测量浏览器内部事件之间的时间延迟并检测这些异常的间隙。

  2. 生命周期违反。Stealth 插件不仅移除自动化标记;它们还依赖于复杂的 hook 和属性重写。防护系统可以检测到复杂的代理对象和被重写的属性在 window 对象中出现得异常得早,早于 DOMContentLoaded 事件,甚至早于 <head> 标签被解析。这破坏了自然的页面生命周期。

  3. 缺乏隔离性。通过 CDP addScriptToEvaluateOnNewDocument 命令注入的任何脚本都会在页面的主执行上下文中运行。因此,您的伪装代码和网站的反欺诈脚本共享同一个环境。最微小的 bug 或意外暴露的变量都可能足以让防护系统检测到自动化。

您无法完全放弃伪装,但可以改变伪装代码的传输方式。无需通过调试协议注入代码,您可以使用浏览器的原生扩展系统。

浏览器设计之初就允许扩展安全地注入代码。如果您将伪装逻辑打包进一个 Manifest V3 扩展并在启动 Puppeteer 时加载它,反欺诈系统就会将时间延迟和注入视为正常的浏览器扩展行为。

例如,您可以创建一个包含 manifest.json 文件的 stealth-extension 目录:

{
  "manifest_version": 3,
  "name": "My Custom Stealth",
  "version": "1.0",
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["inject.js"],
      "run_at": "document_start",
      "world": "MAIN"
    }
  ]
}
{
  "manifest_version": 3,
  "name": "My Custom Stealth",
  "version": "1.0",
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["inject.js"],
      "run_at": "document_start",
      "world": "MAIN"
    }
  ]
}

并在启动 Puppeteer 时加载它,而不是调用 evaluateOnNewDocument

const browser = await puppeteer.launch({
  args: [
    `--disable-extensions-except=${pathToExtension}`,
    `--load-extension=${pathToExtension}`
  ]
});
const browser = await puppeteer.launch({
  args: [
    `--disable-extensions-except=${pathToExtension}`,
    `--load-extension=${pathToExtension}`
  ]
});

第二种方法是完全避免使用 V8 注入机制。您可以使用外部代理服务器或 Puppeteer 内部的请求拦截来在传输过程中修改原始的 HTML 响应。

将您的伪装 <script> 标签插入为 <head> 元素中的第一行。在这种情况下,代码会自然地作为浏览器标准文档解析过程的一部分执行,而不会引起基于延迟的时间检测系统的怀疑。

如果必须使用 addScriptToEvaluateOnNewDocument,请避免加载大型的单体 stealth 插件。应该将伪装过程分为两个阶段。

在页面加载前,只移除 webdriver 标记。这段代码在几毫秒内执行完成,不会产生可以通过 Performance API 检测到的异常。

// Inject a minimal payload before HTML parsing begins
await page.evaluateOnNewDocument(() => {
    // Remove the most obvious automation marker
    Object.defineProperty(navigator, 'webdriver', {
        get: () => undefined,
    });
    // No heavy WebGL or Canvas spoofing here!
});
// Inject a minimal payload before HTML parsing begins
await page.evaluateOnNewDocument(() => {
    // Remove the most obvious automation marker
    Object.defineProperty(navigator, 'webdriver', {
        get: () => undefined,
    });
    // No heavy WebGL or Canvas spoofing here!
});

在浏览器已经开始渲染页面后,再加载所有其他修改——例如 GPU、音频、插件或字体伪装。这可以通过标准的 page.evaluate() 调用或绑定到 DOMContentLoaded 事件来完成。

// Navigate to the website
await page.goto('https://target-site.com');

// The page is already loading and timing checks are complete. 
// Now it is safer to inject heavier spoofing logic.
await page.evaluate(() => {
    // Spoof Canvas, WebGL, fonts, etc.
    const getParameter = WebGLRenderingContext.getParameter;
    WebGLRenderingContext.prototype.getParameter = function(parameter) {
        if (parameter === 37445) return 'Intel Inc.';
        if (parameter === 37446) return 'Intel Iris OpenGL Engine';
        return getParameter(parameter);
    };
});
// Navigate to the website
await page.goto('https://target-site.com');

// The page is already loading and timing checks are complete. 
// Now it is safer to inject heavier spoofing logic.
await page.evaluate(() => {
    // Spoof Canvas, WebGL, fonts, etc.
    const getParameter = WebGLRenderingContext.getParameter;
    WebGLRenderingContext.prototype.getParameter = function(parameter) {
        if (parameter === 37445) return 'Intel Inc.';
        if (parameter === 37446) return 'Intel Iris OpenGL Engine';
        return getParameter(parameter);
    };
});

防护系统通常会在页面生命周期的最开始,同步地去检测 webdriver,这使得该方法能够高效地绕过此类检测。更高级的反欺诈检测通常会在页面加载完成之后异步进行。届时,第二阶段更重的伪装脚本已经完成加载,且没有引起任何页面启动延迟。

Network.setUserAgentOverride 的问题

通过 CDP 更改 User-Agent 很简单,但问题是在这种方式下,只有 HTTP 请求头被修改了,其他环境指标仍然保持原样。

结果是,您制造了一个严重的不一致性。Network.setUserAgentOverride 方法无法正确地伪装内部的 navigator 对象属性或特定的 Client Hints 请求头。

分析系统可能会在请求头中看到移动端的 User-Agent,但同时检测到桌面端的 API 渲染行为,以及与 sec-ch-ua 头部不匹配的信息。

绝不要只更改 User-Agent 字符串。如果您正在模拟一个设备,您必须伪装整套 Client Hints、硬件传感器及特征。相对于仅依赖 setUserAgent,应使用扩展的 CDP 参数并提供一个完整的 userAgentMetadata 对象。此外,模拟移动设备时,不要忘记模拟设备特有的行为,例如触摸屏模拟。

调试 TCP 端口扫描

为了控制浏览器,Puppeteer 会在启动 Chromium 时开放一个用于 WebSocket 通信的端口。默认情况下,这通常是一个本地调试端口,例如经典的 9222 或其他任一分配的随机端口。

反欺诈脚本直接运行在用户的浏览器中。在您自己的系统内并且代表您的浏览器,它只需要简单地发出一个极其普通的 AJAX 请求,例如:

http://127.0.0.1:9222/json/version

防护系统利用所谓的时间差攻击 (timing attacks) 或通过 WebSockets 扫描本地网络。如果一个脚本连接到了标准的调试端口,并且立即从浏览器引擎收到了响应,它就会推断出该浏览器正受到自动化脚本的控制。

随机化端口并非完全的解决方案,因为扫描器可以探测整个端口范围。最简单的解决方案是**彻底停止使用 TCP 端口在 Node.js 和浏览器之间进行通信**。

Puppeteer 可以通过匿名操作系统管道 (anonymous pipes) 代替网络套接字与 Chromium 通信。在这种模式下,不会打开任何调试端口,从而不给反欺诈系统留下可供扫描的地方。

只需在启动浏览器时加上 pipe: true 参数即可。这能够保护您免受本地主机网络扫描的威胁。

暗中达成目标

完美的 Puppeteer 隐匿性在工作原理上是不可能实现的,因为任何形式的模拟都会产生偏离真实用户行为的偏差。正如上面所证明的,这只是一个技术性事实。

对于严肃的使用场景,默认的插件绝不够用。实现高度匿名需要完全不同的方法:

  1. 对 Chrome 二进制文件打补丁。 这涉及直接修改可执行浏览器程序,例如使用十六进制编辑器。目标是将二进制文件内部的硬编码协议字符串替换为随机值。不像 stealth 插件那样在启动到 JavaScript 注入之间会留下短暂的暴露窗口,对二进制文件的修改会在源头上消除这个问题。

  2. 专业的浏览器引擎。 诸如反检测浏览器等架构级的解决方案,会在 Blink 引擎的 C++ 代码库内部实现指纹伪装和自动化隐藏,而不是通过 JavaScript 注入。

  3. 完全避免使用 CDP。 控制逻辑可以转移到定制的 Chrome 插件中,这些插件通过它们自己的 WebSocket 通道与控制服务器通信,使您可以关闭调试端口。

使用反检测浏览器

诸如反检测浏览器等专业解决方案值得单独关注。它们通过修改 Chromium 的源代码,直接作用于浏览器内核来解决手头的问题。在二进制文件编译时,诸如 navigator.webdriver 等明显的自动化特征自然被移除,而不是在之后进行隐藏。

环境模拟中所包含的所有繁重工作都是在后台进行的。当防护脚本请求 GPU 信息(例如 WebGL 厂商或渲染器值)时,引擎不需要执行 JavaScript 包装函数来进行伪造,因为 C++ 代码在原生已经立刻返回了所需的值。更为复杂的指标也是同理。最重要的是,用户无需直接与这些机制进行任何交互,大抵只需要在启动环境配置文件前配置好必要的参数即可。

从反欺诈系统的角度来看,这样的浏览器就像是普通用户。同时,您只需将其连接到反检测浏览器的开放端口,就可以通过 Puppeteer 控制指纹。

不那么显眼的细节和结论

  • 使用 --disable-blink-features=AutomationControlled 参数可以移除基本的 webdriver 标记,但仍会留下间接的痕迹。

  • 强制保持 Log.enablePerformance.enable 的禁用状态可以减少可用遥测,但能提升整体的隐匿性。

  • 新的无头模式 (--headless=new) 统一了标准浏览器和无头浏览器的架构,改变了对 ClientRects 和字体渲染的检测方式,并使渲染行为显得更自然。

成功绕过检测并不是从安装一百个 npm 包开始的。它始于对浏览器引擎实际工作原理的理解。

现代浏览器自动化需要工程级的精密。CDP 泄漏并不是 bug,它们是该技术的架构特征。您对浏览器的理解越深,对每个执行上下文的控制力就越强,您的自动化系统就会越有韧性。

使用Octo Browser维护您的在线匿名性。您真实的数字指纹无法被追踪。

什么是 CDP,它为什么会留下数字痕迹?

Chrome DevTools Protocol 提供了对浏览器架构的底层访问。它最初是为调试、检测和性能剖析设计的,而不是为了隐秘爬取。

该协议通过与 V8 引擎直接交互的 WebSocket 连接起作用。当您的脚本发送命令(例如点击或页面导航)时,浏览器会打开一个本地套接字进行双向通信。这个过程不可避免地会生成内部日志,影响内存分配,并在隔离的浏览器环境中留下痕迹。安全系统可以分析这些微小变化和时间模式以检测自动化行为。因此,仅由于连接的存在就可能暴露出浏览器自动化的事实。

CDP 泄漏与传统指纹识别有何不同

必须明白,CDP 泄漏和浏览器指纹泄漏代表了两个根本不同的检测方向。

  • 传统指纹识别 (Canvas, WebGL, 字体) 用于识别独特的硬件特征和渲染行为。指纹不一致或伪造错误会使反爬虫系统检测到操纵行为,并将用户归类为可疑用户。

  • CDP 泄漏暴露了代码执行的行为和结构指标。它们暴露了浏览器正在被远程控制的事实。

您可能拥有完美且独特的 WebGL 指纹,但自动化标记可以瞬间使这一优势失效。检测浏览器指纹需要相当多的资源和处理时间,而检测环境变量或调用栈则几乎不需要成本。这正是反欺诈系统更青睐基于 CDP 检测的原因。它使他们能够在基础的协议级检测期间过滤掉机器人,从而节省本应花在更昂贵的深度行为分析上的服务器资源。

Chrome DevTools Protocol 泄漏的构造解析

执行上下文标记 (Execution Context markers)

Puppeteer 的主要弱点之一在于它在浏览器内执行代码的具体方式。在底层,page.evaluate() 方法依赖于 CDP 命令 Runtime.evaluate

向浏览器注入脚本时,现代 Puppeteer 版本会生成一个与该代码片段关联的特定源 URL (Source URL)。如果脚本执行期间发生错误,V8 引擎会创建一个标准的堆栈跟踪,其中可能包含类似于以下的内容:

at pptr:evaluate;C:\Users\Admin\Projects\...\main.js:17:14

通过重写基础浏览器函数,反欺诈系统可以故意触发不可见的错误并检查 Error.stack 对象。它们可能会发现:

  • pptr: 前缀,直接指向 Puppeteer 的引用。

  • 本地机器或服务器上文件的绝对路径,以 C:\/var/www/... 开头。

真实用户的浏览器在访问公共网站时绝不会执行来自本地文件系统路径的代码。因此,此类标记代表了自动化的确凿证据。

Execution Context markers

更改您的 IP 地址或伪造 Canvas 指纹并不能解决此问题。要隐藏这些标记,您必须修改库本身或浏览器执行环境。

最可靠的解决方案是在命令发送到浏览器之前,直接从 Puppeteer 的源代码中移除 pptr:evaluate 标记。由于在每次 npm install 之后手动搜索和编辑文件是不切实际的,可以使用一个简单的补丁程序:

const fs = require('fs');
const path = require('path');

// Path to ExecutionContext.js in recent Puppeteer versions
const targetFile = path.resolve(__dirname, 'node_modules/puppeteer-core/lib/cjs/puppeteer/cdp/ExecutionContext.js');

if (fs.existsSync(targetFile)) {
    let content = fs.readFileSync(targetFile, 'utf8');
    // Replace the pptr:evaluate prefix with an anonymous call
    // and remove the local file path
    const patchedContent = content.replace(/pptr:evaluate;.*?\\n/g, 'anonymous:evaluation;\n');
    fs.writeFileSync(targetFile, patchedContent, 'utf8');
    console.log('Puppeteer patched successfully. pptr:evaluate markers removed.');
}

如果无法修改 node_modules,可以在运行时通过在目标页面加载之前注入伪装脚本来过滤这些标记。该脚本重写了原生的 Error 对象行为安全清理堆栈跟踪:

await page.evaluateOnNewDocument(() => {
    // Save the original Error constructor
    const NativeError = window.Error;

    window.Error = function(...args) {
        const err = new NativeError(...args);
        const originalStack = err.stack;

        if (originalStack) {
            Object.defineProperty(err, 'stack', {
                get: function() {
                    // Break stack trace into strings and remove Puppeteer-related entries from it
                    return originalStack
                        .split('\n')
                        .filter(line => !line.includes('pptr:evaluate'))
                        .join('\n');
                }
            });
        }
        return err;
    };
    
    // Restore the prototype chain to avoid detection
    window.Error.prototype = NativeError.prototype;
});
As a result, instead of exposing local file paths, the stack trace will display something like this

结果是,堆栈跟踪将显示类似于以下内容,而不是暴露本地文件路径

Page.addScriptToEvaluateOnNewDocument 的脆弱性

隐藏浏览器自动化的尝试(包括使用流行的 stealth 插件)通常依赖于在目标网站加载之前注入伪装的 JavaScript。在 Puppeteer 中,这通常通过 Page.addScriptToEvaluateOnNewDocument 命令来完成。

然而,该方法本身的使用在执行环境中留下了明显的痕迹。

  1. 时间异常。该命令强制 V8 引擎在创建页面上下文时,在 HTML 解析器开始工作之前同步执行您的代码。注入大型脚本会在 document_start 阶段引入可检测到的微小延迟。反欺诈系统会测量浏览器内部事件之间的时间延迟并检测这些异常的间隙。

  2. 生命周期违反。Stealth 插件不仅移除自动化标记;它们还依赖于复杂的 hook 和属性重写。防护系统可以检测到复杂的代理对象和被重写的属性在 window 对象中出现得异常得早,早于 DOMContentLoaded 事件,甚至早于 <head> 标签被解析。这破坏了自然的页面生命周期。

  3. 缺乏隔离性。通过 CDP addScriptToEvaluateOnNewDocument 命令注入的任何脚本都会在页面的主执行上下文中运行。因此,您的伪装代码和网站的反欺诈脚本共享同一个环境。最微小的 bug 或意外暴露的变量都可能足以让防护系统检测到自动化。

您无法完全放弃伪装,但可以改变伪装代码的传输方式。无需通过调试协议注入代码,您可以使用浏览器的原生扩展系统。

浏览器设计之初就允许扩展安全地注入代码。如果您将伪装逻辑打包进一个 Manifest V3 扩展并在启动 Puppeteer 时加载它,反欺诈系统就会将时间延迟和注入视为正常的浏览器扩展行为。

例如,您可以创建一个包含 manifest.json 文件的 stealth-extension 目录:

{
  "manifest_version": 3,
  "name": "My Custom Stealth",
  "version": "1.0",
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["inject.js"],
      "run_at": "document_start",
      "world": "MAIN"
    }
  ]
}

并在启动 Puppeteer 时加载它,而不是调用 evaluateOnNewDocument

const browser = await puppeteer.launch({
  args: [
    `--disable-extensions-except=${pathToExtension}`,
    `--load-extension=${pathToExtension}`
  ]
});

第二种方法是完全避免使用 V8 注入机制。您可以使用外部代理服务器或 Puppeteer 内部的请求拦截来在传输过程中修改原始的 HTML 响应。

将您的伪装 <script> 标签插入为 <head> 元素中的第一行。在这种情况下,代码会自然地作为浏览器标准文档解析过程的一部分执行,而不会引起基于延迟的时间检测系统的怀疑。

如果必须使用 addScriptToEvaluateOnNewDocument,请避免加载大型的单体 stealth 插件。应该将伪装过程分为两个阶段。

在页面加载前,只移除 webdriver 标记。这段代码在几毫秒内执行完成,不会产生可以通过 Performance API 检测到的异常。

// Inject a minimal payload before HTML parsing begins
await page.evaluateOnNewDocument(() => {
    // Remove the most obvious automation marker
    Object.defineProperty(navigator, 'webdriver', {
        get: () => undefined,
    });
    // No heavy WebGL or Canvas spoofing here!
});

在浏览器已经开始渲染页面后,再加载所有其他修改——例如 GPU、音频、插件或字体伪装。这可以通过标准的 page.evaluate() 调用或绑定到 DOMContentLoaded 事件来完成。

// Navigate to the website
await page.goto('https://target-site.com');

// The page is already loading and timing checks are complete. 
// Now it is safer to inject heavier spoofing logic.
await page.evaluate(() => {
    // Spoof Canvas, WebGL, fonts, etc.
    const getParameter = WebGLRenderingContext.getParameter;
    WebGLRenderingContext.prototype.getParameter = function(parameter) {
        if (parameter === 37445) return 'Intel Inc.';
        if (parameter === 37446) return 'Intel Iris OpenGL Engine';
        return getParameter(parameter);
    };
});

防护系统通常会在页面生命周期的最开始,同步地去检测 webdriver,这使得该方法能够高效地绕过此类检测。更高级的反欺诈检测通常会在页面加载完成之后异步进行。届时,第二阶段更重的伪装脚本已经完成加载,且没有引起任何页面启动延迟。

Network.setUserAgentOverride 的问题

通过 CDP 更改 User-Agent 很简单,但问题是在这种方式下,只有 HTTP 请求头被修改了,其他环境指标仍然保持原样。

结果是,您制造了一个严重的不一致性。Network.setUserAgentOverride 方法无法正确地伪装内部的 navigator 对象属性或特定的 Client Hints 请求头。

分析系统可能会在请求头中看到移动端的 User-Agent,但同时检测到桌面端的 API 渲染行为,以及与 sec-ch-ua 头部不匹配的信息。

绝不要只更改 User-Agent 字符串。如果您正在模拟一个设备,您必须伪装整套 Client Hints、硬件传感器及特征。相对于仅依赖 setUserAgent,应使用扩展的 CDP 参数并提供一个完整的 userAgentMetadata 对象。此外,模拟移动设备时,不要忘记模拟设备特有的行为,例如触摸屏模拟。

调试 TCP 端口扫描

为了控制浏览器,Puppeteer 会在启动 Chromium 时开放一个用于 WebSocket 通信的端口。默认情况下,这通常是一个本地调试端口,例如经典的 9222 或其他任一分配的随机端口。

反欺诈脚本直接运行在用户的浏览器中。在您自己的系统内并且代表您的浏览器,它只需要简单地发出一个极其普通的 AJAX 请求,例如:

http://127.0.0.1:9222/json/version

防护系统利用所谓的时间差攻击 (timing attacks) 或通过 WebSockets 扫描本地网络。如果一个脚本连接到了标准的调试端口,并且立即从浏览器引擎收到了响应,它就会推断出该浏览器正受到自动化脚本的控制。

随机化端口并非完全的解决方案,因为扫描器可以探测整个端口范围。最简单的解决方案是**彻底停止使用 TCP 端口在 Node.js 和浏览器之间进行通信**。

Puppeteer 可以通过匿名操作系统管道 (anonymous pipes) 代替网络套接字与 Chromium 通信。在这种模式下,不会打开任何调试端口,从而不给反欺诈系统留下可供扫描的地方。

只需在启动浏览器时加上 pipe: true 参数即可。这能够保护您免受本地主机网络扫描的威胁。

暗中达成目标

完美的 Puppeteer 隐匿性在工作原理上是不可能实现的,因为任何形式的模拟都会产生偏离真实用户行为的偏差。正如上面所证明的,这只是一个技术性事实。

对于严肃的使用场景,默认的插件绝不够用。实现高度匿名需要完全不同的方法:

  1. 对 Chrome 二进制文件打补丁。 这涉及直接修改可执行浏览器程序,例如使用十六进制编辑器。目标是将二进制文件内部的硬编码协议字符串替换为随机值。不像 stealth 插件那样在启动到 JavaScript 注入之间会留下短暂的暴露窗口,对二进制文件的修改会在源头上消除这个问题。

  2. 专业的浏览器引擎。 诸如反检测浏览器等架构级的解决方案,会在 Blink 引擎的 C++ 代码库内部实现指纹伪装和自动化隐藏,而不是通过 JavaScript 注入。

  3. 完全避免使用 CDP。 控制逻辑可以转移到定制的 Chrome 插件中,这些插件通过它们自己的 WebSocket 通道与控制服务器通信,使您可以关闭调试端口。

使用反检测浏览器

诸如反检测浏览器等专业解决方案值得单独关注。它们通过修改 Chromium 的源代码,直接作用于浏览器内核来解决手头的问题。在二进制文件编译时,诸如 navigator.webdriver 等明显的自动化特征自然被移除,而不是在之后进行隐藏。

环境模拟中所包含的所有繁重工作都是在后台进行的。当防护脚本请求 GPU 信息(例如 WebGL 厂商或渲染器值)时,引擎不需要执行 JavaScript 包装函数来进行伪造,因为 C++ 代码在原生已经立刻返回了所需的值。更为复杂的指标也是同理。最重要的是,用户无需直接与这些机制进行任何交互,大抵只需要在启动环境配置文件前配置好必要的参数即可。

从反欺诈系统的角度来看,这样的浏览器就像是普通用户。同时,您只需将其连接到反检测浏览器的开放端口,就可以通过 Puppeteer 控制指纹。

不那么显眼的细节和结论

  • 使用 --disable-blink-features=AutomationControlled 参数可以移除基本的 webdriver 标记,但仍会留下间接的痕迹。

  • 强制保持 Log.enablePerformance.enable 的禁用状态可以减少可用遥测,但能提升整体的隐匿性。

  • 新的无头模式 (--headless=new) 统一了标准浏览器和无头浏览器的架构,改变了对 ClientRects 和字体渲染的检测方式,并使渲染行为显得更自然。

成功绕过检测并不是从安装一百个 npm 包开始的。它始于对浏览器引擎实际工作原理的理解。

现代浏览器自动化需要工程级的精密。CDP 泄漏并不是 bug,它们是该技术的架构特征。您对浏览器的理解越深,对每个执行上下文的控制力就越强,您的自动化系统就会越有韧性。

随时获取最新的Octo Browser新闻

通过点击按钮,您同意我们的 隐私政策

随时获取最新的Octo Browser新闻

通过点击按钮,您同意我们的 隐私政策

随时获取最新的Octo Browser新闻

通过点击按钮,您同意我们的 隐私政策

立即加入Octo Browser

或者随时联系客户服务,如果您有任何问题。

立即加入Octo Browser

或者随时联系客户服务,如果您有任何问题。

立即加入Octo Browser

或者随时联系客户服务,如果您有任何问题。

©

2026年

Octo Browser

©

2026年

Octo Browser

©

2026年

Octo Browser