Cách website phân biệt V8, SpiderMonkey và JavaScriptCore qua stack trace, thông báo lỗi, toString và độ chính xác Math, cùng giới hạn của tín hiệu này.
Chuỗi User-Agent chỉ là một lời khai báo. Thứ thực sự chạy mã của trang là engine JavaScript, và các chi tiết cài đặt của nó lộ ra qua những API chuẩn thông thường. Đó là lý do tín hiệu từ engine JavaScript hữu ích: nó không phải định danh entropy cao theo bạn qua các trang, mà là máy phát hiện nói dối cho danh tính mà trình duyệt tự nhận. Nếu cần nền tảng về engine JavaScript là gì và ghép cặp với engine render ra sao, hãy đọc Engine render và engine JavaScript trước; bài này đi thẳng vào các kỹ thuật.
Điểm chính
- Ba engine bao phủ gần như mọi trình duyệt thật: V8 (Chrome, Edge và các trình duyệt Chromium khác), SpiderMonkey (Firefox) và JavaScriptCore (Safari và trên thực tế gần như mọi trình duyệt trên iOS).
- Dấu hiệu của engine đến từ những chỗ đặc tả ngôn ngữ để ngỏ: định dạng stack trace, cách diễn đạt thông báo lỗi dựng sẵn, văn bản mã nguồn của hàm native và độ chính xác của một số hàm
Math. - Tín hiệu này thô: nó chỉ chia khách truy cập vào vài nhóm (cộng thêm khác biệt theo thời kỳ phiên bản), nên tự nó chỉ nhận ra một engine, không nhận ra một con người.
- Giá trị thật của nó là kiểm tra tính nhất quán: trình duyệt tự nhận là Safari nhưng chạy như V8 đã kể cho trang hai câu chuyện khác nhau.
- Bộ kiểm tra nhất quán của BrowserInsight, khi các tín hiệu engine khác không đủ kết luận, sẽ dùng định dạng
Error().stackđể phân loại engine.
Vì sao engine tự để lộ
Đặc tả ECMAScript quy định ngôn ngữ phải làm gì, chứ không quy định mọi chi tiết quan sát được phải trông thế nào. Ở những chỗ đặc tả im lặng hoặc nêu rõ là do cài đặt quyết định, mỗi engine tự chọn, và các lựa chọn đó ổn định nhiều năm vì các website đã dựa vào chúng. Không cần quyền đặc biệt nào: script đọc một chuỗi, so với những gì mỗi engine được biết là tạo ra, rồi có câu trả lời.
Kỹ thuật 1: Hình dạng của Error.stack
Error.prototype.stack không thuộc chuẩn, và chính vì thế mỗi engine một kiểu. V8 mở đầu chuỗi bằng tên và thông điệp của lỗi, rồi ghi mỗi frame dạng at functionName (url:line:column). SpiderMonkey và JavaScriptCore không có dòng tiêu đề đó và ghi functionName@url:line:column. Hai engine không phải V8 có thể phân biệt nhau qua các chi tiết nhỏ hơn như cách ghi frame ẩn danh và số cột; những chi tiết này thay đổi theo phiên bản nên cần kiểm tra lại thay vì cố định cứng.
V8 cũng khởi nguồn hai API đi kèm: Error.captureStackTrace và Error.stackTraceLimit. MDN ghi captureStackTrace là phi chuẩn, và bảng tương thích của nó cho biết engine nào về sau đã bổ sung. Có hay không các phần mở rộng kiểu V8 này là gợi ý độc lập thứ hai.
Kỹ thuật 2: Văn bản thông báo lỗi dựng sẵn
Gây cùng một lỗi trên ba engine (gọi thứ không phải hàm, đọc thuộc tính của null) sẽ ra ba câu khác nhau, vì đặc tả định nghĩa loại lỗi chứ không định nghĩa thông báo. Tài liệu tham chiếu lỗi JavaScript của MDN liệt kê nhiều lỗi cùng các biến thể thông báo theo engine. Trang có thể bắt ngoại lệ, đọc error.message rồi đối chiếu với một bảng tra nhỏ. Văn bản này cũng đổi giữa các phiên bản của cùng một engine, nên một phép kiểm tra cẩn thận chỉ coi nó là bằng chứng về họ engine, không phải phiên bản chính xác.
Kỹ thuật 3: Function.prototype.toString trên hàm native
Gọi Function.prototype.toString trên một hàm dựng sẵn như Array.prototype.push trả về văn bản dạng function push() { [native code] }. Đặc tả hiện đại cố định hình dạng chung nhưng vẫn để chỗ cho khoảng trắng và định dạng, và chuỗi cùng độ dài của nó từng khác nhau giữa các engine. Script có thể so kết quả của vài hàm dựng sẵn với những gì trình duyệt tự nhận lẽ ra phải tạo ra. Phép thăm dò này còn một công dụng nữa: hàm bị thay bằng wrapper JavaScript sẽ không còn in [native code], vì vậy các công cụ bắt nói dối rất hay dùng nó.
Kỹ thuật 4: Độ chính xác Math ở các biên
Đặc tả ECMAScript chỉ yêu cầu các hàm như Math.sin, Math.exp hay Math.pow trả về kết quả xấp xỉ do cài đặt quyết định. Tài liệu Math của MDN lưu ý rằng độ chính xác của nhiều hàm trong số này phụ thuộc cài đặt. Vì vậy các engine (đôi khi cả thư viện toán của CPU hoặc hệ điều hành) có thể lệch nhau ở những chữ số cuối với tham số bất thường. Do hành vi này phụ thuộc cả thư viện toán lẫn engine, hãy xem nó là tín hiệu hỗ trợ để gom nhóm khách truy cập, không phải nhãn sạch cho từng engine. Bài này không liệt kê chữ số cụ thể vì chúng đổi theo phiên bản và nền tảng.
Kỹ thuật 5: Giới hạn đệ quy
Cho đệ quy đến khi engine bỏ cuộc rồi bắt lấy lỗi nó ném ra. V8 và JavaScriptCore ném RangeError với thông báo vượt quá kích thước call stack tối đa; SpiderMonkey ném InternalError phi chuẩn của riêng nó với thông báo "too much recursion" (xem mục too much recursion trên MDN). Cả loại lỗi lẫn cách diễn đạt đều là dấu hiệu engine khá thô. Còn độ sâu đạt được trước khi lỗi phụ thuộc kích thước frame, kích thước stack và thiết bị, nên nó chỉ là gợi ý đại khái về bản build và môi trường, không phải định danh engine chính xác, và là phép thăm dò kém ổn định nhất trong danh sách.
Vì sao khó giả mạo
Một trình duyệt anti-detect dựa trên Chromium có thể viết lại User-Agent, Client Hints và các thuộc tính navigator, nhưng nó vẫn chạy V8. Bắt V8 tạo ra định dạng stack, thông báo lỗi, văn bản toString và kết quả Math theo kiểu SpiderMonkey hay JavaScriptCore nghĩa là phải vá rất nhiều hành vi độc lập, và mỗi hàm dựng sẵn bị vá lại là một chỗ mà thay đổi ở tầng prototype có thể lộ ra. Vì vậy kiểm tra engine kết hợp tốt với logic của phân tích kiểu CreepJS, và khoảng cách giữa hồ sơ bị vá với trình duyệt thật (xem trình duyệt anti-detect và trình duyệt thật) thường được phát hiện ở sự thiếu nhất quán giữa các tín hiệu hơn là ở một trường đơn lẻ. Bài này mô tả cách phát hiện, không phải hướng dẫn vượt qua nó.
Dùng ở đâu
Mục đích chính đáng là kiểm tra nhất quán. Hệ thống chống bot và gian lận so sánh trình duyệt mà phiên tự nhận (xem phát hiện giả mạo User-Agent) với engine thực sự đang chạy mã, như một đầu vào trong nhiều đầu vào của phát hiện bot và phát hiện trình duyệt headless. Ý tưởng tương tự dùng được ở mức thô hơn cho dải phiên bản: phát hiện phiên bản qua tính năng cho biết engine mới đến đâu, còn các kỹ thuật trên xác định nó là engine nào.
BrowserInsight thực sự kiểm tra gì
Nói thẳng, có hai việc. Bộ kiểm tra nhất quán đứng sau /fingerprint-check, /bot-detection và /vpn-check sẽ dùng định dạng Error().stack để phân loại engine khi các tín hiệu khác không đủ kết luận. Còn kernel check ước lượng riêng phiên bản engine từ mức hỗ trợ tính năng. Chúng tôi không thăm dò cách diễn đạt thông báo lỗi, độ chính xác Math hay độ dài toString, và mọi thứ chạy trong trình duyệt của bạn; không có gì được gửi lên máy chủ.
Câu hỏi thường gặp
Website có nhận ra tôi qua engine JavaScript không? Tự nó thì không. Engine chỉ chia khách thành vài nhóm rất lớn nên gần như không thêm gì vào độ độc nhất của dấu vân tay. Giá trị của nó là kiểm tra xem các khai báo khác của bạn có nhất quán không.
Engine JavaScript khác nghĩa là trình duyệt khác? Thường là vậy. Chrome, Edge và Brave đều chạy V8, Firefox chạy SpiderMonkey, Safari và gần như mọi trình duyệt trên iOS chạy JavaScriptCore. Các trình duyệt cùng họ không thể phân biệt chỉ bằng engine.
Những khác biệt này có vĩnh viễn không? Không. Các engine đổi cách diễn đạt thông báo, định dạng stack và cài đặt toán học giữa các bản phát hành, và công việc chuẩn hóa có thể xóa bớt khác biệt. Mọi cách phát hiện dựa trên chúng đều cần được bảo trì.
Error.stack có phải chuẩn không?
Nó được cài đặt rộng rãi nhưng không thuộc chuẩn, như MDN ghi nhận, và cũng vì thế mà định dạng của nó trở thành tín hiệu engine hữu ích.
Kết luận
Dấu vân tay engine JavaScript là một tín hiệu nhỏ mà trung thực: vài nhóm engine được đọc từ stack trace, thông báo lỗi, mã nguồn hàm native, kết quả Math và giới hạn đệ quy. Nó không thể chỉ đích danh bạn, nhưng cho thấy lời mô tả của trình duyệt và hành vi của nó có mâu thuẫn hay không. Để xem engine của bạn dưới mắt trang web, hãy chạy kiểm tra dấu vân tay hoặc kernel check.


