Phát hiện Puppeteer thực sự hoạt động như thế nào: 12 tín hiệu tố cáo bot
Các framework tự động hóa kế thừa fingerprint của một trình duyệt thật nhưng thay đổi nó theo hàng chục cách quan sát được. Cẩm nang về 12 tín hiệu JavaScript, mạng và hành vi mà bên phòng thủ dùng để bắt Puppeteer và Playwright.
Các framework tự động hóa điều khiển những trình duyệt thật, nghĩa là chúng kế thừa fingerprint của trình duyệt đó. Một Chrome do Puppeteer điều khiển có cùng user agent, cùng phiên bản Chrome và cùng bản build Chromium như Chrome của người dùng. Nhìn bề ngoài, chúng trông giống hệt nhau.
Nhưng các framework tự động hóa thay đổi trình duyệt theo hàng chục cách tinh vi — phần lớn trong số đó quan sát được từ JavaScript. Việc phát hiện không phải phép màu. Nó là một danh sách kiểm tra.
1. Cờ navigator.webdriver
Tín hiệu đơn giản và nổi tiếng nhất. Khi Chrome chạy dưới một framework tự động hóa, navigator.webdriver trả về true. Đây là một phần của đặc tả W3C WebDriver và Chromium triển khai nó theo mặc định.
Mọi dự án tự động hóa nghiêm túc đều vá điều này trong vài giây. Puppeteer-extra-stealth ghi đè getter. Người dùng Playwright chèn các script Object.defineProperty lên document mới. Cờ này ẩn đi cực kỳ dễ dàng.
Nhưng chính việc nó có thể được ẩn dễ dàng lại là một tín hiệu. Một Chrome hợp lệ không cần ẩn bất cứ thứ gì. Khi một trang kiểm tra Object.getOwnPropertyDescriptor(Navigator.prototype, 'webdriver') và descriptor trông khác so với của một trình duyệt gốc — sự tự động hóa bị bắt ngay cả sau khi cờ đã được vá.
2. Bất thường của đối tượng chrome
Chrome hợp lệ phơi bày một đối tượng toàn cục window.chrome với cấu trúc đáng kể — chrome.runtime, chrome.loadTimes, chrome.csi. Chrome headless và các thiết lập Puppeteer cũ hoặc bỏ hẳn đối tượng này, hoặc phơi bày một phiên bản bị lược bớt.
Các stealth plugin tái tạo đối tượng này, nhưng việc tái tạo không hoàn hảo. chrome.runtime.onConnect có thể hiện diện mà thiếu chrome.runtime.PlatformOs. Chữ ký hàm có thể trả về đối tượng thay vì undefined. Mỗi điểm lệch đều là một tín hiệu dương.
3. Sự thiếu nhất quán của Permissions API
Các trình duyệt thật trả về kết quả mạch lạc khi được truy vấn qua Permissions API. navigator.permissions.query({name: 'notifications'}) nên trả về 'default' nếu người dùng chưa cấp hoặc từ chối quyền một cách rõ ràng.
Chrome headless trả về 'denied' theo mặc định, vì không có giao diện để hiển thị lời nhắc cấp quyền. Các framework tự động hóa vá điều này — nhưng thường không đúng. Một dấu hiệu phổ biến: truy vấn một quyền bất thường như 'clipboard-read' trả về kết quả không khớp với những gì Chrome thật làm ở phiên bản hiện tại.
4. Mảng plugin và MIME type
navigator.plugins trong một trình duyệt thật trả về một PluginArray với các mục như PDF viewer và Chromium PDF plugin. Ở chế độ headless, mảng này rỗng.
Các stealth plugin thêm những mục giả, nhưng các mục này thường có thuộc tính sai — thiếu length, sai trường description, hoặc các đối tượng plugin không hành xử như instance của Plugin khi kiểm tra bằng instanceof.
5. Lệch ngôn ngữ và locale
navigator.language và navigator.languages nên khớp với header HTTP Accept-Language. Chúng cũng nên nhất quán với múi giờ do Intl.DateTimeFormat().resolvedOptions().timeZone báo cáo.
Một bot tự nhận là đang ở Kyiv (múi giờ Europe/Kyiv) nhưng lại gửi Accept-Language: en-US,en;q=0.9 và báo cáo navigator.language === 'en-US' là có thể xảy ra nhưng bất thường về mặt thống kê. Khi kết hợp với một IP từ trung tâm dữ liệu ở Frankfurt, bức tranh trở nên rõ ràng.
6. Chuỗi WebGL renderer
WebGL2RenderingContext.getParameter(WebGLDebugRendererInfo.UNMASKED_RENDERER_WEBGL) trả về nhà cung cấp và model của GPU. Trên một máy thật, nó có thể là:
ANGLE (Intel, Intel(R) UHD Graphics 620 Direct3D11 vs_5_0 ps_5_0)
Chrome headless chạy trong một container thường trả về:
ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device (Subzero)), SwiftShader driver)
Bất kỳ chuỗi GPU nào chứa SwiftShader hoặc Subzero đều là tín hiệu bot mạnh.
7. Bất thường của màn hình và viewport
Một cửa sổ trình duyệt thật có window.outerWidth và window.outerHeight tính đến phần chrome của trình duyệt — thanh địa chỉ, tab, thanh dấu trang. Chênh lệch giữa kích thước ngoài và trong thường là 80–140 pixel theo chiều dọc.
Các trình duyệt headless thường có outerHeight === innerHeight vì không có chrome để hiển thị. Puppeteer với headless: false khắc phục điều này, nhưng nhiều thiết lập tự động hóa vẫn chạy với kích thước bằng nhau.
8. Liệt kê font
Người dùng thật có hàng trăm font được cài đặt, thay đổi theo hệ điều hành, ngôn ngữ và các ứng dụng đã cài. Windows 11 có cài Office có hơn 500 font. Các container tự động hóa chạy Alpine Linux có 30 font.
Việc liệt kê font qua document.fonts.check() hoặc kết xuất canvas ngoài màn hình bộc lộ điều này ngay lập tức. Một trình duyệt tự nhận là Windows 11 Chrome nhưng chỉ phơi bày 30 font gần như chắc chắn là tự động hóa.
9. Entropy của chuyển động chuột
Người dùng thật di chuyển chuột với độ rung. Đường cong không tuyến tính. Tốc độ thay đổi. Giữa hai lần click, thường có hàng trăm sự kiện mousemove.
Các framework tự động hóa tổng hợp chuyển động — hoặc bằng những đường thẳng đến tọa độ, hoặc bằng những đường cong được lập trình vốn thiếu vi rung của bàn tay con người. Ngay cả các mô phỏng đường cong Bezier cũng có xu hướng có hồ sơ gia tốc mượt một cách đáng ngờ.
Các hệ thống phát hiện bot thu thập vết chuyển động và chấm điểm chúng dựa trên một mô hình về chuyển động của con người. Những vết có vận tốc quá nhất quán, đường cong quá sạch, hoặc thiếu các vi sự kiện đều bị gắn cờ.
10. Chữ ký thời gian
Các framework tự động hóa thực thi JavaScript với đặc điểm thời gian khác với các trình duyệt do người dùng điều khiển. Thời gian performance.now() giữa các sự kiện, đặc biệt giữa pointerdown và pointerup, tuân theo một phân phối khác.
Thời gian giữ click thật là 50–150ms với phương sai cao. Thời gian giữ click mặc định của Puppeteer là một giá trị cố định — thường là 30ms hoặc 100ms — với gần như không có phương sai. Một trăm lần click với thời gian giữ giống hệt nhau là tự động hóa chắc chắn.
11. Lệch TLS fingerprint
Tín hiệu này bỏ qua hoàn toàn JavaScript. Khi Puppeteer kết nối, nó dùng stack TLS của Chromium — giống như một Chrome thật. Nhưng các scraper dựa trên undici hoặc axios giả làm Chrome bằng cách giả mạo user agent lại dùng stack TLS của Node.js, vốn có một chữ ký Client Hello (băm JA4) khác biệt rõ rệt.
Một yêu cầu tự nhận là User-Agent: Chrome/124.0.6367.60 nhưng lại trình ra một TLS fingerprint của Node.js là xác nhận tức thì về một client không phải trình duyệt.
12. Rò rỉ giao thức CDP
Puppeteer và Playwright giao tiếp với Chrome qua Chrome DevTools Protocol. Trong một số cấu hình, giao thức này để lại các dấu vết phát hiện được — các thuộc tính bổ sung trên đối tượng Runtime, định dạng Error.stack bị thay đổi, hoặc các đầu ra console.debug cụ thể chỉ xuất hiện khi một client CDP được gắn vào.
Riêng lệnh Runtime.enable thay đổi cách kết xuất stack trace. Các bot không bao giờ kích hoạt lỗi tránh được tín hiệu này, nhưng bất kỳ luồng tự động hóa nào gặp ngoại lệ (và hầu hết đều gặp) đều để lại fingerprint.
Xếp lớp các tín hiệu này
Không tín hiệu đơn lẻ nào là bằng chứng của tự động hóa. Một người dùng thật với driver GPU bất thường có thể trả về một chuỗi WebGL kỳ lạ. Một người dùng chú trọng quyền riêng tư có thể đã sửa đổi navigator.plugins.
Sức mạnh của việc phát hiện hiện đại đến từ việc kết hợp các tín hiệu — 12 tín hiệu dương yếu mạnh hơn một tín hiệu dương mạnh, vì một người dùng thật hiếm khi kích hoạt quá 2–3 bất thường cùng lúc.
Các hệ thống phát hiện gán cho mỗi tín hiệu một trọng số và tính ra một điểm số. Trên ngưỡng, khách truy cập được xử lý như tự động hóa. Ngưỡng và trọng số chính xác là những phần khó làm đúng nhất — quá nghiêm thì người dùng hợp lệ bị chặn; quá lỏng thì các bot tinh vi lọt qua.
Bản thân các tín hiệu là phần dễ. Gần như mọi framework tự động hóa đều rò rỉ cả 12 tín hiệu. Phần kỹ thuật thực sự là quyết định tổ hợp nào quan trọng, cái nào là dương tính giả, và cập nhật mô hình như thế nào khi các framework tự động hóa thích nghi.