Giá trị và thứ tự Accept, Accept-Language, Accept-Encoding tạo dấu vân tay trình duyệt trước khi JavaScript chạy — tắt script cũng không ngăn được.
Chặn hết mọi script, tắt cookie, thậm chí khóa trình duyệt bằng NoScript — máy chủ vẫn có thể lấy dấu vân tay của bạn. Mỗi request HTTP đều mang theo một tập header mô tả trình duyệt của bạn chấp nhận gì, dùng ngôn ngữ nào, và ưu tiên kiểu nén nào, và những header này tới nơi trước khi bất kỳ dòng JavaScript nào được thực thi. Giá trị của chúng, thứ tự của chúng, và việc header nào xuất hiện đều khác nhau tùy trình duyệt — khác nhau một cách ổn định, rất khó giả mạo cho thật thuyết phục, và bất kỳ máy chủ nào cũng đọc được ngay từ request đầu tiên. Đây là kiểu lấy dấu vân tay không cần canvas, không cần WebGL, không cần đối tượng navigator — chỉ cần bản thân dòng request là đủ.
Tóm tắt nhanh
- Accept, Accept-Language và Accept-Encoding mang entropy thực sự. Giá trị của chúng phản ánh tùy chọn ngôn ngữ đã cài đặt và khả năng hỗ trợ codec, và kết hợp lại chúng thu hẹp một khách truy cập theo đúng cách mà canvas hay WebGL làm — chỉ có điều không cần bất kỳ script nào chạy.
- Thứ tự header tự nó là một tín hiệu, độc lập với giá trị. Ngăn xếp mạng của mỗi trình duyệt phát ra header theo một trình tự cố định; trình tự đó khác nhau giữa Chrome, Firefox và Safari, và hiếm khi thay đổi giữa các request từ cùng một bản cài đặt.
- Không điều nào trong số này cần JavaScript. Máy chủ thấy toàn bộ tập header ngay ở request đầu tiên — từ trước khi HTML được phân tích cú pháp, chưa nói đến trước khi thẻ script chạy — nên việc chặn JS và NoScript không đụng tới nó.
- Nó nằm dưới Client Hints, không thay thế Client Hints. User-Agent Client Hints bổ sung thêm một tín hiệu tầng HTTP nữa lên trên các header nhóm Accept vốn luôn được gửi: vài gợi ý entropy thấp được gửi mặc định, còn những gợi ý chi tiết chỉ được gửi khi máy chủ chủ động yêu cầu qua
Accept-CH. Cả hai đều kích hoạt trước JavaScript. - Nó kết hợp với các lớp TLS và TCP bên dưới nó. Lấy dấu vân tay TLS đọc quá trình bắt tay được mã hóa nằm dưới các header này, nên máy chủ kiểm tra cả hai lớp sẽ có được một chữ ký cấp request hoàn toàn không phụ thuộc vào việc trang có tải xong hay không.
Dòng Request Tự Nó Đã Là Một Dấu Vân Tay
Trước khi trình duyệt render bất cứ thứ gì, nó đã gửi một request HTTP chứa vài header mà toàn bộ công việc của chúng là thương lượng nội dung: báo cho máy chủ biết trình duyệt có thể hiển thị gì, bằng ngôn ngữ nào, và muốn phản hồi được nén theo cách nào. Trên HTTPS, request đó được mã hóa trên đường truyền, nhưng máy chủ kết thúc kết nối vẫn đọc được nó ở dạng thô. Một request Chrome điển hình trông như thế này:
GET / HTTP/1.1
Host: example.com
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8
Accept-Language: en-US,en;q=0.9
Accept-Encoding: gzip, deflate, br, zstd
Trên HTTP/2 và HTTP/3, cùng những thông tin đó được truyền dưới dạng các trường nhị phân đã nén, tên trường viết thường, và Host được thay bằng pseudo-header :authority — ngữ nghĩa thương lượng nội dung thì giống hệt, và việc chúng tới nơi trước bất kỳ script nào cũng vậy. (Bản thân lớp đóng khung còn mang thêm tín hiệu riêng; xem lấy dấu vân tay HTTP/2.)
Không có gì ở đây cần thực thi script — đây chính là request lấy về tài liệu HTML. Một máy chủ chỉ cần ghi log ba header này đã nắm trong tay nhiều thông tin hơn hầu hết mọi người tưởng: nhóm header Accept — Accept, Accept-Language, Accept-Encoding — không phải là thứ khuôn mẫu, chúng được thương lượng riêng theo từng trình duyệt và từng cấu hình, còn RFC 9110 — chuẩn ngữ nghĩa HTTP hiện hành — định nghĩa cú pháp giá trị chất lượng (q=) cho phép mỗi trình duyệt thể hiện thứ tự ưu tiên riêng của mình, và đó chính xác là nơi sự khác biệt bắt nguồn.
Mỗi Header Thực Sự Tiết Lộ Điều Gì
Accept-Language là tín hiệu trực tiếp nhất. Nó liệt kê những ngôn ngữ người dùng đã cấu hình, theo thứ tự ưu tiên, kèm giá trị q thể hiện trọng số tương đối. Người dùng đa ngôn ngữ — chẳng hạn cấu hình en-US,en;q=0.9,fr;q=0.8,de;q=0.7 — để lộ một tổ hợp cụ thể và tương đối hiếm; cách tính trọng số q chính xác cũng khác nhau đôi chút tùy trình duyệt và cài đặt vùng miền của hệ điều hành, thêm vào một lớp không chỉ là "cấu hình những ngôn ngữ nào" mà còn là "chính ngăn xếp này định dạng danh sách ngôn ngữ ra sao".
Cũng cần nói rõ header này đáng giá bao nhiêu nếu đứng một mình: một khách truy cập gửi en-US,en;q=0.9 trơn tru chia sẻ giá trị đó với một đám đông khổng lồ, nên tự nó gần như không đóng góp thông tin nhận dạng nào. Entropy nằm ở các trường hợp bất thường — một danh sách ba bốn ngôn ngữ, một biến thể vùng miền hiếm gặp, một ngôn ngữ không khớp với quốc gia của IP thoát — và ở sự kết hợp với mọi thứ còn lại trong request. Đó cũng chính là phép toán entropy và tập ẩn danh chi phối các tín hiệu phía JavaScript.
Accept-Encoding liệt kê những thuật toán nén mà client có thể giải mã: gzip, deflate, br (Brotli), và ngày càng nhiều là zstd. Việc hỗ trợ thuật toán mới được triển khai theo từng trình duyệt, từng phiên bản, nên tập hợp cụ thể — và thứ tự liệt kê của chúng — không chỉ thu hẹp xuống "một trình duyệt Chromium" mà còn xuống gần đúng phạm vi phiên bản đã gửi request.
Accept mô tả những loại nội dung và loại con nào trình duyệt sẽ render, có trọng số ưu tiên — và trực tiếp tiết lộ những thứ như hỗ trợ định dạng ảnh AVIF hay WebP, điều tương quan chặt chẽ với dòng trình duyệt và phiên bản.
Xét riêng lẻ, mỗi header chỉ thu hẹp phạm vi một chút. Nhưng đọc cùng nhau, tổ hợp này hoạt động y hệt phép toán entropy đằng sau dấu vân tay canvas và WebGL: các tín hiệu độc lập mà số bit thông tin nhận dạng của chúng cộng dồn lại với nhau.
Thứ Tự Và Sự Hiện Diện Của Header: Một Chữ Ký Ẩn Dưới Giá Trị
Không chỉ giá trị mới thay đổi — bản thân tập hợp và thứ tự header mà trình duyệt gửi cũng gần như là một chữ ký cố định. Thư viện client HTTP của mỗi trình duyệt lắp ráp header theo đúng trình tự mà mã mạng của nó vốn dựng nên, và trình tự đó nhất quán giữa các request từ cùng một trình duyệt và phiên bản, nhưng khác nhau giữa các trình duyệt. Một request tự xưng là Chrome nhưng lại đến với thứ tự header trùng khớp thư viện requests của Python hoặc curl thì lộ ra ngay lập tức — cùng logic khiến thứ tự trong TLS ClientHello trở thành dấu vân tay cũng áp dụng ở một lớp cao hơn — lớp đóng khung HTTP — cho thứ tự header thông thường. Và vì HTTP/2 và HTTP/3 áp đặt các quy ước pseudo-header riêng lên trên, bản thân lớp đóng khung cũng mang thêm tín hiệu mà một proxy HTTP/1.1 thô hoặc thư viện script thường vô tình làm sai một cách tinh vi.
Đây cũng là lý do tại sao tập hợp header quan trọng ngang với bất kỳ giá trị đơn lẻ nào: một trình duyệt thật gửi một cụm header đầy đủ, có thể dự đoán được ở mỗi lần điều hướng; còn một client dạng script chỉ đặt User-Agent mà quên Accept-Language hay Accept-Encoding — hoặc gửi chúng theo thứ tự mà không trình duyệt thật nào dùng — sẽ nổi bật chính vì cái thiếu hoặc sai chỗ, chứ không phải vì một giá trị đơn lẻ nào đó trông có vẻ sai.
Vì Sao Kỹ Thuật Này Sống Sót Qua Việc Tắt JavaScript
Những tín hiệu lấy dấu vân tay ở cấp DOM được bàn ở nơi khác trên trang này — canvas, WebGL, âm thanh, phông chữ, quyền — đều cần một script chạy và gọi một API nào đó. Header HTTP thì không cần bất kỳ điều gì như vậy. Chúng được ngăn xếp mạng của trình duyệt gắn vào chính request đầu tiên lấy byte đầu tiên của HTML, nghĩa là:
- Tắt hoàn toàn JavaScript (NoScript, trình duyệt chỉ có văn bản) không có tác dụng gì — header vẫn được gửi đi.
- Chặn script hay trình theo dõi bên thứ ba cũng không giúp được gì, vì chính máy chủ phục vụ trang (bên thứ nhất) đã thấy header một cách trực tiếp.
- Ngay cả một request không bao giờ render ra trang — một request HEAD, một redirect chưa bao giờ được theo hết — vẫn mang theo toàn bộ tập header.
Đây chính là đặc tính khiến lấy dấu vân tay TLS và TCP/IP cũng chống chịu được việc chặn script: bất kỳ tín hiệu nào sống ở lớp mạng hoặc lớp giao thức, dưới DOM và dưới engine JavaScript, đơn giản là nằm ngoài tầm với của những công cụ bảo vệ quyền riêng tư vốn hoạt động bằng cách tắt script.
Nó Kết Hợp Với Client Hints Và TLS Ra Sao
Lấy dấu vân tay chỉ bằng HTTP không thay thế các tín hiệu tầng mạng khác — nó là lớp mà tất cả những tín hiệu đó nằm chồng lên, hoặc song song với nó:
| Lớp | Tín hiệu | Cần JS? | Cần HTTPS? |
|---|---|---|---|
| TCP/IP | Kích thước cửa sổ, TTL, thứ tự tùy chọn | Không | Không |
| Bắt tay TLS | JA3/JA4 từ ClientHello | Không | Có |
| Header HTTP thuần | Giá trị + thứ tự Accept/Accept-Language/Accept-Encoding | Không | Không |
| Client Hints | Giá trị mặc định entropy thấp Sec-CH-UA-* | Không | Có |
| Client Hints (entropy cao) | Phiên bản đầy đủ, kiến trúc, model | Không (nhưng máy chủ phải yêu cầu qua Accept-CH) | Có |
| API trình duyệt | Canvas, WebGL, phông chữ, âm thanh | Có | Không |
Một máy chủ đọc xuyên suốt toàn bộ ngăn xếp này có được một chữ ký được lắp ráp hoàn toàn trước — và độc lập với — bất kỳ tín hiệu nào dựa trên JavaScript, rồi chồng thêm các tín hiệu cấp DOM cho những khách truy cập thực sự thực thi script. Các header nhóm Accept và Client Hints đều thuộc tầng HTTP và đều không phụ thuộc JS; khác biệt là Client Hints hiện chỉ được các trình duyệt nền Chromium hỗ trợ, và muốn lấy những giá trị phong phú hơn thì máy chủ phải chủ động yêu cầu; còn header Accept thông thường thì đến từ mọi trình duyệt, ở mọi request, không cần thương lượng gì cả.
Kiểm Tra Header Của Chính Bạn
Bạn có thể tự xem cả hai phía của đường ranh này. Công cụ kiểm tra dấu vân tay của BrowserInsight hiển thị danh sách ngôn ngữ mà trình duyệt của bạn để lộ cho script (navigator.languages) bên cạnh phần còn lại của các tín hiệu phía JavaScript, và nó cũng hiển thị tín hiệu tầng mạng duy nhất mà một trang không thể tự tính ra: chính lần bắt tay TLS của kết nối này theo đúng những gì máy chủ quan sát được, gồm JA3/JA4, phiên bản TLS và độ dài ClientHello. Thẻ bắt tay đó chính là lớp nằm ngay bên dưới những header được nói tới ở đây — cùng đặc tính "trước bất kỳ script nào", chỉ khác là máy chủ đọc nó thay vì trang web.
Để đối chiếu điều đó với riêng lớp header, hãy ghi lại danh sách ngôn ngữ mà công cụ hiển thị, rồi xem trình duyệt thực sự gửi gì trong Accept-Language bằng một dịch vụ phản hồi lại request thô, hoặc bằng chính DevTools của trình duyệt (mở tab Network, bấm vào request tài liệu, đọc phần Request Headers). Hai bên thường lệch nhau ở chi tiết — navigator.languages và Accept-Language xuất phát từ cùng một bộ tùy chọn nhưng được định dạng độc lập — và đúng kiểu lệch đó là thứ hệ thống phát hiện đang tìm. Hãy đổi cài đặt ngôn ngữ, hoặc làm lại trên một trình duyệt khác, và bạn sẽ thấy cả hai phía dịch chuyển ngay trước mắt.
Câu Hỏi Thường Gặp
Dùng VPN có làm thay đổi những header này không?
Không. VPN thay đổi địa chỉ IP của bạn, không phải ngăn xếp mạng của trình duyệt — Accept, Accept-Language và Accept-Encoding do chính trình duyệt tạo ra và đi qua đường hầm VPN mà không đổi. Nếu Accept-Language của bạn vẫn hiển thị en-US trong khi IP thoát của VPN lại định vị địa lý ở một quốc gia khác, sự bất nhất đó tự nó trở thành một tín hiệu, tương tự bất nhất giữa múi giờ và IP mà các công cụ khác kiểm tra.
Tôi có thể giả mạo các header này để trông giống trình duyệt khác không?
Về giá trị thì có — bất kỳ thư viện client HTTP nào cũng cho phép bạn đặt header tùy ý. Nhưng khớp nhất quán tập hợp và thứ tự header chính xác của một trình duyệt thật, ở mỗi request, kể cả cách HTTP/2 đóng khung chúng, thì khó hơn nhiều — đó là lý do một tập header không khớp hoặc thiếu sót thường là dấu hiệu tố cáo lưu lượng do script tạo ra, chứ không chỉ riêng chuyện giá trị bị giả mạo.
Điều này có giống lấy dấu vân tay TLS không?
Có liên quan nhưng khác biệt. Lấy dấu vân tay TLS đọc quá trình bắt tay diễn ra từ trước khi bất kỳ request HTTP nào được gửi đi; lấy dấu vân tay header HTTP đọc request đi theo ngay sau khi bắt tay hoàn tất — request đó được mã hóa trên đường truyền, nhưng máy chủ kết thúc kết nối vẫn đọc được trọn vẹn. Cả hai đều không phụ thuộc JavaScript, và máy chủ ngày càng kiểm tra cả hai cùng lúc, vì một request hoàn toàn có thể vượt qua một kiểm tra nhưng lại trượt ở kiểm tra kia.
Tắt cookie có ngăn được kiểu lấy dấu vân tay này không?
Không — cookie không liên quan gì ở đây. Lấy dấu vân tay dựa trên header không lưu trữ bất cứ thứ gì trên thiết bị của bạn; nó đọc những gì trình duyệt mặc định gửi ở mỗi request, giống hệt cách lấy dấu vân tay trình duyệt nói chung cũng không dựa vào dữ liệu được lưu trữ.
Kết Luận
Phần lớn các thảo luận về lấy dấu vân tay tập trung vào các tín hiệu do JavaScript dẫn động — hash canvas, chuỗi renderer WebGL, liệt kê phông chữ — vì đó là những nguồn giàu thông tin, entropy cao nhất mà một bên theo dõi có thể có được. Nhưng ở một lớp thấp hơn, vẫn tồn tại một chữ ký có ý nghĩa: những header HTTP thuần túy mà mỗi trình duyệt gửi trước khi bất kỳ script nào có cơ hội chạy — chấp nhận gì, bằng ngôn ngữ nào, với kiểu nén nào, và theo thứ tự nào. Tắt JavaScript đóng lại các tín hiệu cấp DOM; nhưng không ảnh hưởng gì tới bản thân dòng request. Kết hợp với dấu vân tay TLS và TCP bên dưới cùng Client Hints song song, lớp tín hiệu chỉ tồn tại nhờ header này có nghĩa là: không hề có request nào thực sự "sạch tín hiệu" — chỉ có những request mà chưa ai buồn nhìn xuống dưới cả trang.
Đề xuất đọc thêm:


