2026 年的 CDP 检测:Runtime.enable 修复之后变了什么

曾经暴露 Puppeteer 和 Playwright 的 Error.stack getter 在 Chrome 138 中不再触发。本文说明 V8 改了什么、2026 年哪些 CDP 与非 CDP 信号依然有效,并提供一个探测页面供你测试自己的技术栈。

CDP Detection in 2026: What Changed After the Runtime.enable Fix
本文目录

最知名的 CDP 检测技巧是在 Error.stack 上定义一个 getter:只要自动化客户端调用过 Runtime.enable,它就会被触发。2025 年 5 月的两次 V8 inspector 提交随 Chrome 138 发布后,这一招失效了。但这并没有让 CDP 驱动的浏览器隐身。研究者此后找到了其他观察 Runtime 域是否启用的方法,而且多数商业检测方案如今依赖的是框架痕迹、无头与运行环境破绽、输入属性,以及行为和网络信号,这些从来就不依赖那一项检查。

什么是 CDP 检测,为什么主流框架都会暴露

CDP 检测指的是识别出浏览器由 Chrome DevTools Protocol 驱动,而不是由人操作。它之所以可行,是因为这个协议本为调试而设计,部分调试功能会留下页面 JavaScript 可观察到的痕迹。

Chrome DevTools Protocol 就是 DevTools 窗口自身使用的 WebSocket API,由 Runtime、Page、Network、Input 等域组成,每个域都有命令和事件。大多数 Chromium 自动化都要经过它:

  • Puppeteer 通过 CDP 控制 Chrome;对 Firefox 则改用 WebDriver BiDi。
  • Playwright 对 Chromium 使用 CDP;对 Firefox 使用 Juggler,这是 Firefox 内部的另一套协议。
  • Selenium 4 用 WebDriver 与 ChromeDriver 通信,ChromeDriver 在内部通过 CDP 驱动 Chrome;在迁移到 BiDi 的过程中,Selenium 也直接暴露 CDP 命令。

Firefox 已经告别 CDP。Mozilla 在 Firefox 129 中弃用了 CDP 实现,并在 Firefox 141 中彻底移除。因此 2026 年所说的“CDP 检测”,指的就是检测被自动化的 Chromium 系浏览器。

这类信号吸引防御方的原因,DataDome 在其 2024 年 6 月的研究(转载自其威胁研究博客)中说得很清楚:“能够检测浏览器被 CDP 插桩,是识别大多数现代机器人框架的关键。”新版 headless Chrome 与普通 Chrome 共用代码库之后,大部分指纹不一致都消失了。而协议层面的副作用可以同时抓住 Puppeteer、Playwright、Selenium,以及任何基于同一底层机制的未知框架。

经典的 Runtime.enable 检查如何运作

这项检查利用了一个事实:Runtime 域启用后,V8 的 inspector 会序列化每个 console 参数,以便向客户端发送预览。序列化对象要读取其属性,而读取带 getter 的属性就会执行页面代码。

Castle 记录的版本如下:

let wasAccessed = false;
const e = new window.Error();
Object.defineProperty(e, 'stack', {
  configurable: false,
  enumerable: false,
  get: function () {
    wasAccessed = true;
    return '';
  }
});
console.debug(e);
// wasAccessed === true  =>  有程序在监听 Runtime 域

逐步来看:

  1. Puppeteer、Playwright 和 ChromeDriver 会调用 Runtime.enable,因为它们需要 Runtime.executionContextCreated 事件来确定在哪里执行脚本。协议文档将其描述为启用“执行上下文创建的上报”。
  2. 域启用后,每次 console.* 调用都会产生一个 Runtime.consoleAPICalled 事件,V8 会为其中每个参数构建预览。
  3. 对于 Error,构建预览需要读取 .stack(以及 .name 和 .message)。页面已把 .stack 换成了 getter,于是 getter 被执行,标志位随之翻转。
  4. 在没有任何调试器连接的普通浏览器中,不会构建预览,getter 也永远不会执行。

它只需几行 JavaScript,几乎没有开销,却能抓住所有调用了 Runtime.enable 的框架,无论它们怎样处理 navigator.webdriver 或 User-Agent。给 JavaScript 属性打补丁的 stealth 插件对此无能为力,因为泄漏发生在协议层,而不在页面里。后来出现的对策都在驱动层:rebrowser-patches 和 Patchright 避免调用 Runtime.enable;DataDome 在 2024 年也指出,nodriver 和 Selenium Driverless 使用的是“不依赖 Runtime.enable 的底层 CDP 命令”。

V8 在 2025 年改了什么

V8 在 2025 年 5 月通过两次提交堵住了这个副作用:inspector 在预览对象和错误时不再执行用户定义的 getter。修复进入 V8 13.8,随 Chrome 138 发布(2025 年 6 月 24 日进入稳定版)。

  • 61a90754(2025 年 5 月 7 日),“[inspector] Prevent side effects during object preview”,避免“在启用 runtime 域时执行用户代码”。
  • e08e9734(2025 年 5 月 9 日),“[inspector] Prevent side effects during error preview”,修改错误描述的生成方式,“以避免执行带副作用的 getter”。

两次提交都引用了 Chromium bug 415094795。第一次提交的作者是微软的 Playwright 维护者 Pavel Feldman,所以这项改动来自自动化工具一方,而不是某个 stealth 项目。Castle 的 Antoine Vastel 于 2025 年 8 月 28 日撰文分析,标题为 《一个经典的 CDP 机器人检测信号为何突然失效(而且没人注意到)》,文中指出新增的 getErrorProperty() 辅助函数会在预览时跳过用户 getter。在 Chrome 138 及更高版本上,即便使用原版 Puppeteer,上面的代码片段也会返回 false。

Runtime.enable 还能被检测到吗?

大概率还能,只是不如以前可靠。2026 年 3 月,独立研究者 Sveba 发表了一篇关于 V8 预览路径的分析,指出仍有两条途径。一条是 GetOwnPropertyDescriptor 失败时、2025 年 5 月的防护没有覆盖到的代码路径,作者称已“验证……在打补丁后的 Chrome 中可被利用”。另一条是在对象原型链上放置一个 Proxy:

let detected = false;
const trap = new Proxy({}, {
  ownKeys() { detected = true; return []; }
});
const obj = Object.create(trap);
console.groupEnd(obj);   // 任意 console 方法都可以
// detected === true  =>  inspector 正在构建预览

按照这篇文章的说法,inspector 只检查值本身是否为 Proxy,而不检查其原型,并且会急切地收集所有键。因此遍历原型链时就会触发 ownKeys 陷阱。作者在 2026 年 3 月的 content_shell 构建上做了测试,报告称当时尚未修复。我们没有在每个 Chrome 稳定版上验证过,所以请把它当作“有报告”,而非普遍适用。

有两点限制了防御方如今对任何 Runtime.enable 信号的依赖程度。第一,打开 DevTools 的真实用户会走同一条代码路径,所以这个信号表达的是“有 inspector 连接”,而不是“这是机器人”。第二,最值得抓的那些框架早已避开 Runtime.enable,所以它主要标记的是未经修改的 Puppeteer、Playwright 和 Selenium。行业已转向叠加大量较弱的信号,Castle 的文章也是这么说的。

2026 年仍然有效的信号

如今站得住的信号来自框架在页面里做了什么、浏览器如何构建和启动、输入如何到达,以及会话在网络上的表现。隐藏某一个 CDP 副作用,消除不了其中任何一项。

主世界中的框架痕迹

原版框架会留下自己的指纹。rebrowser bot detector 列出了几项:

  • __pwInitScripts:Playwright 默认会把它添加到每个页面的全局作用域。
  • sourceURL 泄漏:Puppeteer 会给执行的脚本打上 //# sourceURL=pptr:… 标记,它可能出现在被钩住的函数内部捕获的错误堆栈里。
  • 主世界执行:如果你的脚本在页面自己的世界里调用 document.querySelector,而页面已经包装了这个函数,页面就能看到这次调用。
  • exposeFunction / binding:page.exposeFunction() 和 Playwright 的 window.__playwright__binding__ 会安装页面可以枚举到的全局变量。它们底层使用 Runtime.addBinding,该命令会“在所有被检查上下文的全局对象上”添加函数。
  • 工具世界名称:框架创建的隔离世界带有可识别的名称,rebrowser-patches 允许你重命名。

初始化脚本痕迹

Page.addScriptToEvaluateOnNewDocument 会“在每个 frame 创建时(在加载 frame 的脚本之前)”执行一段脚本。stealth 插件正是用它在网站代码运行前修改 navigator。协议本身不泄漏,但补丁会。正如 Camoufox README 所说,注入的 JavaScript 可以通过 Object.getOwnPropertyDescriptor 被发现,也可以通过函数的 toString() 不再返回 [native code],或者 window 与 Worker 中取值不同而被发现。

Worker 上下文

Worker 是补丁最常失手的地方。作用于页面的初始化脚本和属性覆盖,不会自动延伸到 dedicated、shared 或 service Worker。如果 Worker 报告真实的 navigator.userAgent、hardwareConcurrency 或 WebGL 渲染器,而 window 报告的是伪造值,这就是很强的破绽。CreepJS 专门检查 Worker 作用域,正是出于这个原因。

无头模式与构建特征

  • Headless shell 与新版 headless。Chrome 在 Chrome 112 推出了 --headless=new,从 Chrome 132 起,旧模式只存在于独立的 chrome-headless-shell 二进制文件中。Playwright 在无头模式下默认使用 chromium headless shell,其行为与真实 Chrome 不同。
  • 构建身份。“Chrome for Testing”的 User-Agent、默认视口(Puppeteer 为 800×600,Playwright 为 1280×720),以及保持开启的 navigator.webdriver,都是 rebrowser 检测器列出的低成本检查。
  • 编解码器。据 Chromium 项目说明,开源 Chromium 构建不包含 Google Chrome 附带的专有编解码器,例如 H.264 和 AAC。自称 Chrome 却在 H.264 的 canPlayType 检查上失败的浏览器,实际上是 Chromium。
  • 权限与媒体设备。在无头和容器环境中,通知权限状态、空的媒体设备列表和缺失的语音合成声音,常常与 User-Agent 声称的不符。CreepJS 和 BrowserScan 会检查这些。

合成输入

通过 Input.dispatchMouseEvent 发送的输入会产生受信任事件(isTrusted 为 true),但它曾带有真实输入没有的属性。最有名的是在非全屏状态下 screenX/screenY 等于 pageX/pageY。Chromium 在 change 6917162(“Fix screen coordinates to avoid automation detection”)中修复了这个问题,于 2025 年 9 月 15 日合并,早于 Chrome 142 的分支点。为绕过这个问题而存在的 CDP-Patches 项目现在表示,CDP 输入也会产生合并事件(coalesced events),并已将自身归档。在当前的 Chrome 上,这两项具体检查基本失效,但轨迹、时序和点击位置分析依然适用。

行为与网络

下面这些信号都与 CDP 无关,却构成了厂商评分的大部分。Castle 列出了客户端行为分析、环境“红药丸”(red pills)、代理与网络检测,以及针对协同活动的时间序列分析。网络层面包括必须与所声称浏览器相符的 TLS 指纹和 HTTP/2 指纹,以及 IP 信誉。GPU 层面的指纹识别见我们的 Canvas 与 WebGL 深度解析。

Patchright、nodriver 和 Camoufox 如何处理 CDP 泄漏

三者都以不同方式避开了 Runtime.enable 类泄漏,但没有一个能解决行为或网络信誉问题。

Patchright 是仅支持 Chromium 的 Playwright 补丁版驱动。根据其 README,它的主要补丁“通过在(隔离的)ExecutionContext 中执行 JavaScript”来避免 Runtime.enable。它还完全禁用了 Console 域(因此 console 日志不可用),去掉 --enable-automation,加上 --disable-blink-features=AutomationControlled,并建议使用真实 Chrome(channel="chrome")且不注入指纹。它紧跟 Playwright 的版本发布(v1.63.0 驱动于 2026 年 9 月发布)。安装配置见我们的 Patchright 深度解析。

nodriver 是 undetected-chromedriver 的继任者,它去掉了 WebDriver,直接用 CDP 与你安装的 Chrome 通信。查看其当前 core/ 源码可以发现,它使用 Runtime.evaluate 和 Runtime.callFunctionOn 执行代码,从不调用 Runtime.enable,这与 DataDome 2024 年的描述一致。代价是代码在主世界中执行,包装了 DOM 函数的页面能看到它,而且没有指纹管理。PyPI 上最新版本是 2026 年 5 月发布的 0.50.3。参见我们的 nodriver 指南。

Camoufox 完全绕开了 CDP。它是 Firefox 的分支,Playwright 通过 Juggler 驱动 Firefox,而 Camoufox 对 Juggler 打了补丁,让 Playwright 在页面的隔离副本上工作:页面脚本既看不到注入的辅助代码,也无法监视 getter 读取。输入经由 Firefox 正常的输入处理流程,指纹值在 C++ 层伪造,而不是靠初始化脚本,这就消除了 toString 和 Worker 不一致这一类问题。代价是它终究是 Firefox。README 提到部分 WAF 会测试 SpiderMonkey 引擎行为,所以它无法冒充 Chrome。

信号速查表

信号2026 年还有效吗?如何规避规避成本
console 上的 Error.stack getter(经典版)在 Chrome 138+(V8 13.8)上无效使用最新版 Chrome无
其他 inspector 预览副作用(如原型链 Proxy)截至 2026 年 3 月有报告称有效;打开 DevTools 的真实用户也会触发不调用 Runtime.enable(Patchright、nodriver、rebrowser-patches)或不使用 CDP(Camoufox)低到中:驱动补丁,失去 console API
__pwInitScripts、binding、pptr: sourceURL有效,针对原版框架补丁版驱动;避免使用 exposeFunction低
主世界执行钩子有效在隔离世界中执行中:无法直接访问页面全局变量
被 JS 打补丁的属性(toString、描述符)有效不打补丁;使用真实 Chrome 或 C++ 层伪造中到高
window 与 Worker 不一致有效同上;补丁必须覆盖 Worker若用 JS 打补丁则高
Headless shell / Chrome for Testing / 编解码器有效品牌版 Chrome,有头模式或新版 headless,虚拟显示中:更多内存,服务器上需 Xvfb
CDP 输入:screenX == pageX、无合并事件在当前 Chrome 中基本已修复使用当前版 Chrome无
行为与时序有效,且在评分中占比越来越大真实的节奏,每个身份更少的请求高:吞吐量
TLS / HTTP/2 / IP 信誉有效真实浏览器网络栈,干净的住宅或 ISP IP持续的代理开销

如何测试你自己的环境

先用你自己的探测页面测试,再用公开检测页面测试,每次只改变一个变量。公开页面告诉你它们检测到了什么;你自己的页面告诉你哪种 Chrome 构建与驱动的组合仍会触发哪项检查。

1. 本地探测页面

把下面的内容保存为 probe.html。它会运行经典检查、原型链检查和几项低成本的痕迹检查,然后把结果写进 DOM,这样无论驱动的脚本运行在哪个世界,都能读取到。

<!doctype html>
<pre id="out">running</pre>
<script>
function classic() {
  let hit = false;
  const e = new Error();
  Object.defineProperty(e, 'stack', { configurable: false, enumerable: false,
    get() { hit = true; return ''; } });
  console.debug(e);
  return hit;
}
function protoProxy() {
  let hit = false;
  const trap = new Proxy({}, { ownKeys() { hit = true; return []; } });
  console.groupEnd(Object.create(trap));
  return hit;
}
const r = {
  classicStackGetter: classic(),
  protoProxyPreview: protoProxy(),
  webdriver: navigator.webdriver,
  pwInitScripts: '__pwInitScripts' in window,
  h264: document.createElement('video').canPlayType('video/mp4; codecs="avc1.42E01E"'),
  ua: navigator.userAgent,
};
setTimeout(() => { document.getElementById('out').textContent = JSON.stringify(r, null, 2); }, 300);
</script>

然后在你关心的每种环境下运行它:

import pathlib
from playwright.sync_api import sync_playwright   # 可替换为:from patchright.sync_api import sync_playwright

url = pathlib.Path("probe.html").resolve().as_uri()

with sync_playwright() as p:
    browser = p.chromium.launch(channel="chrome", headless=False)
    page = browser.new_page()
    page.goto(url)
    page.wait_for_function("document.getElementById('out').textContent !== 'running'")
    print(page.text_content("#out"))
    browser.close()

依次运行原版 Playwright、Patchright,再在普通 Chrome 窗口中打开同一页面,分别测试打开和关闭 DevTools 的情况。DevTools 那一轮是你的对照组:凡是在那里触发的检查,对真实用户也会触发,因此单独来看只是弱信号。

2. 公开检测页面

  • rebrowser bot detector:专注 Chromium,涵盖 Runtime.enable、sourceURL、主世界执行、exposeFunction、__pwInitScripts、视口和 User-Agent。页面说明中提到,部分测试只有在你的脚本调用页面的 dummyFn 或暴露函数时才会触发。
  • BrowserScan 机器人检测:报告 WebDriver、User-Agent、CDP 以及 navigator 一致性检查结果。
  • CreepJS:开源指纹分析工具,会标记“谎言”(被打补丁的属性)、无头特征和 Worker 作用域不一致。要发现 JavaScript 层面的伪造,它是最好的选择。
  • Device and Browser Info「Are you a bot?」:指纹与行为测试,包括 CDP、WebDriver、Playwright 和 headless Chrome 检查。

请通过生产环境中将使用的同一代理运行这些测试。其中几个页面会同时显示 IP 和时区以及浏览器信号,而在数据中心 IP 上拿到完美的浏览器评分,对真实流量几乎说明不了什么。使用粘性会话(USERNAME-sid-abc123-ttl-10m,配合住宅代理网关使用)可以在对比各轮测试时保持 IP 不变。

记住,通过测试页面并不等于通过真实网站。商业厂商使用服务端模型、跨会话历史,以及这些页面看不到的信号。

这些解决不了什么

堵上 CDP 泄漏只是去掉了一类证据。它不会让自动化看起来像人,也不会让数据采集变得合规。请求速率、访问顺序和每个身份的请求量仍会暴露自动化,IP 信誉和 TLS 仍必须与你所声称的浏览器相符。访问规则依然适用:robots.txt、服务条款,以及涉及个人信息时的数据保护法律。如果你做检测,2025 年的实际教训是:不要依赖浏览器厂商一次例行提交就能移除的单一副作用。如果你做自动化,请保持工具更新,优先选择不给 JavaScript 打补丁的驱动,并假定剩下的信号主要是行为层面的。

常见问题

Runtime.enable 检测现在还有效吗?

经典版本(在 Error.stack 上定义 getter 并通过 console 输出)在 2025 年 5 月两次 V8 inspector 提交之后,已于 Chrome 138 失效。2026 年 3 月一份独立分析报告了其他预览副作用,例如原型链上的 Proxy,仍能暴露已启用的 Runtime 域。但真实用户打开 DevTools 时同样会触发这些信号。

网站如何检测 Puppeteer 和 Playwright?

主要依靠框架痕迹和运行环境,而不是某个单一的 CDP 技巧:__pwInitScripts 等全局变量和暴露的 binding、Puppeteer 的 pptr: sourceURL、主世界函数钩子、headless shell 与 Chrome for Testing 特征、被 JavaScript 打补丁的属性、Worker 不一致,再加上行为、TLS/HTTP2 指纹和 IP 信誉。

哪个 Chrome 版本修复了 CDP console 检测?

修复通过 V8 13.8 的提交 61a90754(2025 年 5 月 7 日)和 e08e9734(2025 年 5 月 9 日)落地,它们阻止 inspector 在生成对象和错误预览时执行用户定义的 getter。V8 13.8 随 Chrome 138 发布,该版本于 2025 年 6 月 24 日进入稳定版。

Camoufox 能通过 CDP 被检测到吗?

不能通过 CDP:Camoufox 是 Firefox 的分支,由 Playwright 通过 Juggler 控制,而 Firefox 已在 141 版移除 CDP 实现。不过 Camoufox 仍可能通过其他方式被识别,包括指纹不一致和 Firefox 特有的引擎检查。

怎么测试我的无头浏览器会不会被检测?

在本地运行一个包含已知检查的探测页面,逐个驱动测试,并与普通 Chrome 窗口(分别在打开和关闭 DevTools 时)对比。然后通过生产环境使用的同一代理访问公开检测页面,如 rebrowser bot detector、BrowserScan 机器人检测页、CreepJS 以及 Device and Browser Info 的「Are you a bot?」测试。

准备好开始了吗?

覆盖 195+ 国家的住宅、ISP 和移动代理。创建免费账户。

创建免费账户
← 返回博客