深入解析網站如何透過 CDP 痕跡、WebGL 算繪器和特性缺口偵測 Selenium、Puppeteer、Playwright 等無頭瀏覽器。
無頭瀏覽器驅動了大量的網路自動化作業:持續整合測試套件、上線監控、價格爬蟲,以及另一面的憑證填充攻擊、搶購機器人和點擊詐欺程式。偵測技術早已超越使用者代理字串直接包含 HeadlessChrome 的年代。如今,網站透過一層層訊號網路來識別受程式控制的無頭瀏覽器——而這些訊號幾乎無從隱藏。本文將逐一說明這些訊號及其難以消除的原因。
重點整理
navigator.webdriver是自動化的標誌性旗標,任何 WebDriver 驅動的瀏覽器均會將其設為true;錯誤的修補本身就是一種指紋。- CDP 痕跡難以完全抹除:Chrome DevTools Protocol 會留下特殊的全域變數與時序特徵,即使套用許多隱匿修補仍難以消除。
- 特性缺口暴露無頭執行環境:純軟體 WebGL 算繪器、缺少的音訊編解碼器、矛盾的權限狀態,以及空白的外掛程式列表,都與真實桌面瀏覽器有明顯差異。
- Selenium、Puppeteer、Playwright 各有獨特的特徵,即使偽造使用者代理字串後仍然存在——每個框架在全域範疇中都留有已知的特徵值,且預設網路行為也各不相同。
- 隱匿外掛程式能提高門檻但無法完全堵漏:它們只能修補 JavaScript 層的洩漏,無法觸及更底層的訊號——TLS 指紋、HTTP/2 封包排序或作業系統層級的行程樹。
什麼是無頭瀏覽器?
無頭瀏覽器執行完整的算繪引擎——HTML 解析、CSS 排版、JavaScript 執行——但不在可見螢幕上顯示任何畫面。程式透過控制協定發送指令,瀏覽器做出回應,全程無需人員介入監看。Puppeteer、Playwright 和 Selenium WebDriver 都採用此模型,通常以 --headless 旗標啟動 Chrome Headless 或以無頭模式執行 Firefox。
無頭瀏覽器對測試如此有用的特性,恰恰也是使其可被偵測的原因:它們受程式控制,而程式必然留下痕跡。
navigator.webdriver 的洩露
最直接的無頭訊號是 navigator.webdriver。WebDriver 規範要求任何受 WebDriver 控制的瀏覽器都必須將此屬性公開為 true。在真實桌面瀏覽器中,它會回傳 false 或 undefined。
// A detector reads this in a single line.
if (navigator.webdriver === true) {
// Browser is almost certainly under WebDriver control.
}
單憑這個訊號力道尚弱——隱匿修補多年前便已能壓制它。但錯誤的修補本身也是一種洩露。若屬性描述器不符合標準、get 函式可被序列化,或 Object.getOwnPropertyDescriptor(navigator, 'webdriver') 回傳的物件與真實瀏覽器的不符,細心的偵測器就能察覺。一個修補失誤的 navigator.webdriver = false,往往比未修補的 true 更加可疑。
CDP 與 DevTools Protocol 的痕跡
Puppeteer 和 Playwright 都透過 Chrome DevTools Protocol(即 Chrome 開發人員工具背後使用的同一協定)來驅動 Chrome。這個控制通道會在頁面中留下痕跡。
全域變數洩漏
某些 CDP 使用模式會將物件注入頁面的全域範疇。早期版本的 Puppeteer 會暴露 __puppeteer_evaluation_script__;其他框架則留有 __nightmare、_phantom 或 window.callPhantom。偵測腳本會逐一枚舉這些特徵:
const knownArtifacts = [
'__nightmare', '_phantom', 'callPhantom',
'__puppeteer_evaluation_script__',
'__selenium_evaluate', '__webdriver_evaluate',
];
const found = knownArtifacts.filter(k => k in window);
// Any hit is a strong automation signal.
事件監聽器異常
攔截 fetch 或 XMLHttpRequest 以修改回應的自動化框架,會在事件監聽器對應表中留下特定模式。偵測器查詢 window 物件上的 getEventListeners 時,可以看到真實使用者工作階段不會產生的監聽器堆疊。
時序旁路通道
CDP 通訊具有可量測的延遲。以 performance.now() 對一系列任務計時,可揭露每個步驟都需要一次 CDP 往返通訊所產生的特有抖動,而非在瀏覽器事件迴圈上同步執行的正常行為。
Selenium vs Puppeteer vs Playwright 的特徵差異
三個框架最終都是驅動瀏覽器,但各自留下不同的痕跡。
| 框架 | 控制介面 | 主要特徵 |
|---|---|---|
| Selenium | W3C WebDriver(基於 HTTP) | navigator.webdriver = true(未修補),客戶端函式庫特有的 HTTP 標頭順序 |
| Puppeteer | Chrome DevTools Protocol | CDP 全域注入、舊版 window.__puppeteer_* 變數,僅支援 Chrome,因此 TLS 與真實 Chrome 相符 |
| Playwright | CDP(Chrome/Edge),內部協定(Firefox/WebKit) | 預設值比 Puppeteer 更乾淨;Firefox/WebKit 變體在特性集上與真實瀏覽器存在差異 |
Playwright 在減少底層洩漏方面投入更多,因此當 Playwright 被懷疑為目標框架時,偵測研究已轉向上述更細微的訊號。
缺失與矛盾的瀏覽器特性
在多個特性領域,無頭環境與真實桌面瀏覽器之間持續存在差異。偵測腳本會組合探測這些缺口。
外掛程式列表
真實桌面版 Chrome 內建一組外掛程式(如 PDF 檢視器等),可透過 navigator.plugins 存取。無頭版 Chrome 在過去會回傳空陣列。雖然現代版本已有所改善,但外掛程式物件本身在屬性描述器上仍有差異,可與真實使用者瀏覽器的應有值進行交叉比對。
媒體編解碼器
真實瀏覽器會與作業系統及底層 GPU 協商編解碼器支援。在無顯示硬體的情況下執行的無頭環境,通常會透過 HTMLMediaElement.canPlayType() 回報有限的編解碼器支援。一個宣稱自己是 Windows 上 Chrome 卻回報無法播放 H.264 影片的瀏覽器,是一個顯著的矛盾訊號。
WebGL 與 GPU 算繪
WebGL 是最強烈的無頭偵測訊號之一。在無實體 GPU 存取的情況下,Chrome Headless 會退而使用 SwiftShader——其軟體光柵化算繪器。偵測器讀取 gl.getParameter(gl.RENDERER) 並檢查是否出現 SwiftShader 或 Google SwiftShader 等字串,而非真實 GPU 名稱:
const canvas = document.createElement('canvas');
const gl = canvas.getContext('webgl');
const renderer = gl.getParameter(gl.RENDERER);
// "ANGLE (Intel, ...)" or "Apple M2" → real hardware
// "Google SwiftShader" → headless software render
使用者代理宣稱搭載強大裝置,WebGL 算繪器卻回報純 CPU 軟體算繪,這是直接的矛盾。
Permissions API 的矛盾狀態
Permissions API 讓腳本可查詢使用者是否已授予特定功能的存取權限。在無頭環境中,內部權限儲存可能處於真實瀏覽器工作階段中不可能出現的狀態——例如,通知同時處於 denied 和 prompt 的狀態。偵測腳本透過探測多個權限並檢查邏輯一致性,可捕捉到這些無效組合。
隱匿外掛程式如何嘗試隱藏(及其失敗之處)
puppeteer-extra-plugin-stealth 等函式庫會在偵測腳本讀取之前修補上述許多訊號:覆蓋 navigator.webdriver、注入逼真的外掛程式列表、替換 CDP 特有的全域變數,並正規化 Permissions API 的回應。面對簡單的偵測,這些措施效果良好。
然而問題在於結構性的限制。首先,每個修補本身都可能是洩露:Object.defineProperty 包裝器的 toString() 輸出與原生屬性不同,探測 Object.getOwnPropertyDescriptor(navigator, 'webdriver') 會揭露人工構造的描述器。偵測系統現在已將「修補行為的存在本身」視為訊號。其次,隱匿外掛程式完全在 JavaScript 層運作,無法觸及以下層面:
- TLS ClientHello 指紋——瀏覽器在握手時提供的密碼套件與擴充功能,反映的是底層網路函式庫(Chromium 使用 BoringSSL),而非 JavaScript 層。無頭 Chrome 與真實 Chrome 共享相同的 TLS 指紋,但冒充 Chrome 的 Python 腳本則無法做到。
- HTTP/2 封包排序——真實瀏覽器與基於其上構建的自動化客戶端之間,HTTP/2 封包的序列與優先順序有所不同。
- 作業系統行程樹——伺服器端若偵測到 ChromeDriver 或 geckodriver 行程與瀏覽器行程並存,即擁有了任何 JavaScript 修補都無法消除的帶外自動化證據。
測試你自己的自動化設置
若你基於合法目的執行無頭自動化——持續整合管線、無障礙稽核、上線監控——可以親眼看看偵測系統對你的工作階段所觀察到的內容。BrowserInsight 的機器人偵測工具能即時回報你的 navigator.webdriver 狀態、外掛程式數量、WebGL 算繪器及其他自動化訊號。核心環境檢測則能揭示使用者代理宣稱背後實際執行的算繪引擎。在部署至正式環境前,請善用這些工具稽核你的設置。
關於 JavaScript 無法偵測的網路層訊號,另一篇關於機器人偵測技術的文章涵蓋了 TLS 和 HTTP/2 指紋識別的內容。
常見問題
修補 navigator.webdriver 能欺騙偵測系統嗎?
只能騙過簡單的檢查。真實瀏覽器中此屬性自然為缺失狀態,但「缺失的方式」才是關鍵。原生的 false 與修補後的 false 在偵測器讀取屬性描述器時會呈現不同面貌。將修補與 WebGL 及外掛程式檢查結合後,剩餘的訊號已足以將大多數自動化行為分類出來。
Playwright 能比 Selenium 更好地規避偵測嗎?
Playwright 的預設值比傳統 Selenium 更為乾淨,其跨瀏覽器支援意味著 Firefox 和 WebKit 驅動程式可以避開某些 Chrome 特有的 CDP 特徵。但偵測系統追蹤 Playwright 特有訊號的方式與追蹤 Selenium 的方式相同。目前沒有任何框架能對測試完整訊號集的偵測堆疊保持隱形。
為什麼無頭 Chrome 會回報 SwiftShader?
SwiftShader 是 Chrome 基於 CPU 的軟體算繪器——在沒有真實 GPU 的情況下使用。執行無頭 Chrome 的伺服器或容器通常沒有實體顯示器介面卡,因此 GPU 退而使用 SwiftShader。真實桌面 Chrome 則會回報實際的 GPU 驅動程式名稱:Intel、NVIDIA、AMD 或 Apple Silicon。
是否存在能通過所有檢查的無頭設置?
理論上可行,但實際上極為困難。你需要:修復 JavaScript 層的洩漏(隱匿外掛程式能處理大部分)、配置真實的 GPU 存取(修復 WebGL 問題)、使用帶有正確 TLS 的真實 Chrome 網路堆疊(Chrome 系自動化大致沒問題),以及大規模產生逼真的行為噪音。商業反偵測瀏覽器嘗試達成這一目標;我們關於反偵測瀏覽器偵測的文章介紹了網站如何應對這類工具。


