解析網站如何透過 CDP 副作用(如 Runtime.enable 序列化洩漏)偵測 Puppeteer、Playwright 和 Selenium。
Puppeteer、Playwright,以及現代版本的 Selenium,幾乎都是透過同一條底層通道驅動 Chrome 的:Chrome DevTools Protocol(CDP),也就是驅動 Chrome DevTools 本身的那套介面。這種共用基礎對自動化工具的開發者來說很方便,但也意味著這些工具都可能留下同一類破綻——與 User-Agent 字串或 navigator.webdriver 是否偽裝無關。本文將解釋什麼是 CDP、它具體帶來了哪些副作用,以及偵測系統如何把這些副作用轉化為一種訊號。
核心要點
- CDP 是共用的控制通道,Puppeteer、Playwright 的 Chromium 驅動,以及基於 CDP 的 Selenium 工作階段都建立在它之上——一個專門識別 CDP 本身的偵測器,能一次捕捉這三者。
- 啟用 Runtime 域會帶來可觀測的副作用:一旦用戶端發送
Runtime.enable,每一次console.*呼叫都會被攔截並對其參數進行序列化以產生預覽,這會主動觸發一些頁面本來不會碰觸的屬性 getter。 - 這種序列化副作用本身就是一種偵測手段:頁面可以在傳給
console.log的值上定義一個 getter,檢查它是否立即被觸發——藉此在不顯示任何可見記錄的情況下,揭露是否連線了 CDP 用戶端。 - 各框架暴露程度不同:Puppeteer 和 Playwright 的 Chromium 模式預設會啟用
Runtime.enable以追蹤執行環境;經過修補的版本會用其他方式管理環境 ID,藉此避開這一點。 - 這只是分層偵測體系中的一個訊號,並非萬能解法——它與我們無頭瀏覽器偵測指南中介紹的
navigator.webdriver及算繪異常偵測互為補充。
什麼是 CDP,自動化工具為何依賴它
Chrome DevTools Protocol 是一套 JSON-RPC 介面,讓外部用戶端可以檢查並控制一個正在執行的 Chromium 實例:執行 JavaScript、攔截網路請求、擷取螢幕截圖,以及接收主控台和例外事件。這正是你手動開啟 DevTools 時,DevTools 本身與瀏覽器通訊所用的協定。
Puppeteer 和 Playwright 的 Chromium 後端直接透過 CDP 連線。Selenium 傳統上使用 W3C WebDriver 線協定,但 Chromium 的 WebDriver 實作(chromedriver)本身底層就是一個 CDP 用戶端,而 Selenium 4 針對 Chromium 的 DevTools API 更是直接暴露了 CDP。既然三者最終都共用同一個控制層,一個針對 CDP 機制本身、而非某個框架特有痕跡的偵測器,就有機會一次捕捉所有三者。
頁面能觀測到的 CDP 訊號
頁面沒有直接的 API 可以詢問「是否連線了 CDP 用戶端」。CDP 的設計初衷就是對頁面 JavaScript 不可見。但連線用戶端會以某些方式改變算繪程序內部發生的事:某些協定域只有在用戶端請求時才會發出事件,而處理這些事件會在頁面正在執行的同一 JavaScript 執行緒上執行額外程式碼。其中最有用的一種,正是啟用 Runtime 域時發生的情況。
Runtime.enable 與主控台序列化洩漏
自動化用戶端發送 Runtime.enable 以接收執行環境和主控台 API 呼叫事件——Puppeteer 和 Playwright 需要它來得知該在哪個環境 ID 中執行指令碼。一旦這個域被啟用,V8 檢查器就會在每一次 console.* 呼叫抵達可見主控台之前將其攔截,並為對應的 CDP 事件建構物件預覽——無論是否有真人開啟 DevTools 去查看。
建構這個預覽意味著要走訪參數的屬性,而走訪屬性就意味著觸發它們的 getter。在未被偵測工具插樁的頁面上,傳給 console.log 的物件上的 getter,只有在真人開啟 DevTools 並正在查看時才會觸發。而當 Runtime.enable 處於啟用狀態時,它會作為 CDP 事件管線自身的副作用,無條件、立即觸發:
// 只有當 CDP 用戶端啟用了 Runtime 域時,這段程式碼才會
// 立即執行(在任何地方出現任何輸出之前)。
let cdpDetected = false;
const probe = {};
Object.defineProperty(probe, 'flag', {
get() {
cdpDetected = true;
return 'x';
},
});
console.log(probe);
// 在下一個 tick 檢查 cdpDetected——不需要任何可見的主控台。
這是一個時序/序列化側通道,而不是一個有文件記錄的 API:它取決於 V8 檢查器如何建構主控台預覽,這屬於實作細節,而非穩定契約。這也是為什麼它從未成為一個永久破綻——當 Chromium 改變檢查器處理主控台參數的方式時,這一確切的引擎層行為也隨之變化過。
Puppeteer 與 Playwright 的痕跡
兩個框架在預設組態下都會啟用 Runtime 域,這正是這項技術能廣泛適用於開箱即用指令碼、而非罕見邊緣情況的原因。
| 工具 | 預設 CDP 行為 | 常見緩解方式 |
|---|---|---|
| Puppeteer | 發送 Runtime.enable 以追蹤執行環境 | 社群分支避免讓該域持續啟用,改用其他方式還原環境 ID |
| Playwright(Chromium) | 為其預設頁面 world 的每個執行環境啟用 Runtime | 社群修補程式在無需 Runtime.enable 取得的隔離 world 中執行自動化指令碼 |
Selenium(chromedriver / W3C) | 主要使用 W3C WebDriver 線協定;CDP 用法更窄且需主動選用 | 避免使用 DevTools API 即可完全繞開這項具體偵測 |
實際結果是:這種基於單一機制的偵測器,曾一度能把預設組態的 Puppeteer/Playwright 工作階段,與基於純 WebDriver 的 Selenium 工作階段區分開來,直到自動化工具的作者調整了各自管理 CDP 的方式。
偵測手法與規避手段的較量
這與我們機器人偵測技術指南中討論的貓鼠遊戲如出一轍,只是發生在技術堆疊更底一層的地方。這個 Runtime.enable 側通道一經公開,就迅速在各家反機器人廠商中傳播開來,因為它能用一次偵測捕捉多個框架,而且不依賴隱身外掛早已涵蓋的 JavaScript 層修補。自動化工具的維護者隨後推出了跳過 Runtime.enable、或改用其他方式追蹤執行環境的版本,補上了這個特定漏洞,同時保持框架其餘行為不變——這與一個追蹤該函式庫自動化偵測回報多年的長期 Puppeteer GitHub issue 中反映的模式如出一轍。
雙方誰都無法長期領先。一個依賴未公開檢查器內部機制的訊號,恰恰是引擎變更可能悄悄打破或恢復的那種東西,這也是為什麼真正嚴謹的偵測體系,會把它當作更大評分系統中的一項輸入——與 navigator.webdriver、WebGL 算繪異常,以及本站其他文章描述的網路層指紋並列——而非當作一個能獨立下定論的訊號。
測試你自己的自動化環境
如果你維護 CI 瀏覽器測試、無障礙稽核或監控指令碼,了解偵測系統能看到你的工作階段中的什麼,是值得的。BrowserInsight 的機器人偵測工具會顯示你瀏覽器目前暴露的 navigator.webdriver、無頭痕跡及其他自動化訊號,核心檢測則會確認底層實際執行的算繪引擎。這兩個工具都不會直接檢查 CDP 協定流量本身——那種檢查發生在伺服器端——但它們展示了真實偵測體系會與之結合使用的相鄰訊號。
常見問題
網站能否直接偵測到 CDP 已連線?
無法透過有文件記錄的 API 直接偵測——CDP 的設計初衷就是對頁面 JavaScript 不可見。頁面能偵測到的,是特定 CDP 域被啟用時產生的副作用,例如 Runtime 域啟用後出現的主控台序列化行為。
這對 Selenium 的影響和 Puppeteer、Playwright 一樣嗎?
不完全一樣。Selenium 的經典路徑執行在 W3C WebDriver 線協定之上,而非直接使用 CDP,因此它不會像 Puppeteer 和 Playwright 預設的 Chromium 組態那樣,自動觸發 CDP 域的副作用。但如果 Selenium 工作階段明確使用了 Chromium 的 DevTools API,仍然會觸發同樣的偵測。
啟用 Runtime.enable 是自動化工具作者的失誤嗎?
不是——這是透過 CDP 取得執行環境和主控台事件最直接的方式,也正是人類開啟 DevTools 時它自身所做的事。這個副作用之所以成為機器人訊號,僅僅是因為指令碼不像人類,在生產環境中沒有其他理由連線 CDP 用戶端。
修復這一個訊號能讓自動化變得無法偵測嗎?
不能。它只是從評分系統中移除了一項輸入,而該系統還包括算繪不一致性、行為分析和網路層指紋——詳見我們關於無頭瀏覽器偵測和機器人偵測技術的文章,了解這套體系的其餘部分。


