navigator.getBattery() từng lộ mức pin và trạng thái sạc mà không cần xin quyền. Vì sao Firefox gỡ API này, WebKit chưa từng triển khai, và bài học rút ra.
Trong vài năm, bất kỳ trang web nào cũng có thể hỏi laptop hoặc điện thoại của bạn một câu hỏi khá riêng tư — "còn bao nhiêu pin, và có đang sạc không?" — và nhận câu trả lời ngay lập tức, không cần bất kỳ hộp thoại xin quyền nào. Battery Status API ra đời vì một lý do khá chính đáng: cho phép một trang giảm hiệu ứng động hoặc hoãn đồng bộ nền khi thiết bị người dùng sắp hết pin. Các nhà nghiên cứu chứng minh nó cũng có thể bị đọc như một tín hiệu theo dõi tồn tại trong thời gian ngắn, và chỉ trong vài năm, một trình duyệt lớn đã gỡ bỏ nó còn một trình duyệt khác thì chưa bao giờ triển khai. Đây là một trong những ví dụ thực tế rõ ràng nhất về việc một Web API có ý tốt bị thu hồi khi cái giá về quyền riêng tư của nó lộ rõ.
Tóm tắt nhanh
navigator.getBattery()từng lộlevel,charging,chargingTimevàdischargingTimecho bất kỳ script nào, không cần xin quyền — một sự kết hợp hiếm gặp với một API liên quan đến phần cứng.- Một nghiên cứu học thuật năm 2015 ("The Leaking Battery", tác giả Olejnik, Acar, Castelluccia và Diaz) cho thấy các số liệu này có thể nhận diện lại cùng một trình duyệt trên nhiều trang khác nhau trong một khoảng thời gian ngắn; và ở những trình duyệt trả về số liệu với độ chính xác đầy đủ, script còn suy ra được dung lượng thực của pin — một tín hiệu tồn tại lâu hơn nhiều.
- Firefox đã gỡ quyền truy cập của nội dung web vào API này từ phiên bản 52 (tháng 3/2017), với lý do rủi ro quyền riêng tư; Safari/WebKit chưa từng triển khai nó, vì cho rằng bề mặt lấy dấu vân tay này không đáng đánh đổi.
- Các trình duyệt dựa trên Chromium vẫn còn hỗ trợ API này — Chrome, Edge, Opera và Samsung Internet đều hỗ trợ
navigator.getBattery(). - Công cụ kiểm tra dấu vân tay của BrowserInsight cho biết trình duyệt của bạn hiện còn để lộ những tín hiệu ít nổi bật nào thuộc nhóm này.
navigator.getBattery() từng lộ những gì
API này gắn một phương thức duy nhất trả về promise vào navigator. Ở nơi được hỗ trợ, việc đọc chỉ cần một dòng mã và không cần bất kỳ thao tác nào từ người dùng:
navigator.getBattery().then((battery) => {
console.log(battery.level); // 0 đến 1, ví dụ 0.73
console.log(battery.charging); // true / false
console.log(battery.chargingTime); // số giây đến khi đầy pin, hoặc Infinity
console.log(battery.dischargingTime);// số giây đến khi hết pin, hoặc Infinity
});
Bốn giá trị này đóng vai trò chính: mức sạc level dạng số thập phân, trạng thái boolean charging, và hai giá trị ước tính thời gian mà trình duyệt liên tục tính lại khi quan sát hành vi của pin. API này còn phát ra các sự kiện levelchange, chargingchange, chargingtimechange và dischargingtimechange, nên một script thậm chí không cần liên tục truy vấn — chỉ cần lắng nghe là có thể theo dõi mức pin của khách truy cập cạn dần theo thời gian thực.
Điều kiện truy cập thì gần như không có gì. API này chỉ hoạt động trong ngữ cảnh bảo mật (tức là cần HTTPS) và không được cấp cho Web Worker, nhưng bên trong một trang HTTPS bình thường thì không có hộp thoại xin quyền, không cần thao tác của người dùng, và giao diện cũng không hề báo hiệu rằng trang vừa đọc thứ gì đó. Quan trọng hơn, một script phân tích hay quảng cáo của bên thứ ba được nhúng thẳng vào trang — cách triển khai phổ biến nhất — có đúng quyền truy cập như mã của chính trang web đó. (Bản đặc tả hiện hành về sau đã bổ sung tính năng permissions policy tên battery với danh sách cho phép mặc định là ["self"], ít nhất cũng chặn được các iframe khác nguồn gốc trừ khi trang nhúng chủ động mở quyền.)
Nghiên cứu "The Leaking Battery"
Vấn đề quyền riêng tư ở đây không nằm ở việc một số liệu đơn lẻ có tính nhận diện cao — pin ở mức 73% tại một thời điểm bất kỳ là điều rất nhiều người cùng có. Vấn đề là bốn giá trị này, khi kết hợp lại và liên tục cập nhật, tạo thành một vector trạng thái tồn tại ngắn nhưng chính xác, cho phép hai trang web khác nhau (hoặc hai trình theo dõi khác nhau trên cùng một trang) đối chiếu và khớp với nhau — ngay cả khi người dùng đã chặn cookie, mở cửa sổ ẩn danh, hoặc chuyển giữa các tab. Trong một khung thời gian từ vài giây đến vài phút, level khớp nhau cùng với các ước tính chargingTime/dischargingTime gần sát nhau đã đủ để liên kết hai lượt truy cập tưởng chừng tách biệt về cùng một thiết bị.
Phát hiện thu hút chú ý nhiều nhất lại nằm ở chuyện các con số này có được làm tròn hay không — và câu trả lời là không. Trên Firefox chạy trên Linux, giá trị level được giao thẳng cho script với đầy đủ độ chính xác của kiểu double, gần như truyền nguyên xi từ tiến trình quản lý nguồn điện của hệ điều hành, nên thứ trang web nhận được không phải một con số gọn gàng như 0.73 mà là một phân số dài dằng dặc. Điều đó quan trọng vì level chính là tỷ lệ giữa mức sạc hiện tại và dung lượng đầy của pin: có đủ chữ số thập phân, script có thể đi ngược từ tỷ lệ đó ra chính dung lượng — một thuộc tính của phần cứng vật lý chứ không phải mức sạc hiện tại, và hầu như không đổi từ phiên duyệt web này sang phiên khác.
Nhóm nghiên cứu chỉ ra rằng điều này gây thiệt hại nặng nhất cho đúng những người khó nhận ra nó nhất. Dung lượng pin suy giảm dần theo thời gian sử dụng, lệch khỏi thông số xuất xưởng theo cách riêng của từng viên pin, nên một viên pin cũ cho ra con số "lạ" hơn — và vì thế dễ nhận diện hơn — so với pin mới. Đề xuất của họ không phải là xóa bỏ API mà chỉ là làm thô số liệu báo về: việc đó chẳng làm mất đi chút hữu ích thực tế nào — một trang muốn giảm hiệu ứng động khi pin còn 20% đâu cần tới mấy chữ số thập phân.
Vì sao Firefox gỡ bỏ nó, còn WebKit thì chưa từng triển khai
Firefox là một trong những trình duyệt triển khai sớm nhất: navigator.mozBattery có tiền tố đã bật mặc định từ Firefox 11 năm 2012, còn navigator.getBattery() dạng promise xuất hiện ở Firefox 43 — Chrome đã có getBattery() từ trước đó một năm, trong Chrome 38. Phản ứng đầu tiên của Mozilla trước nghiên cứu này đúng là bản vá hẹp mà nhóm nghiên cứu đề nghị: cắt bớt độ chính xác của số liệu. Quyết định lớn hơn đến sau đó. Firefox 52, phát hành tháng 3/2017, gỡ hẳn khả năng gọi navigator.getBattery() của nội dung web, cho rằng cái giá về quyền riêng tư không còn xứng đáng với lợi ích nhỏ mà API mang lại cho phần lớn các trang web. Tính năng này vẫn dùng được từ mã đặc quyền của chính Firefox, nên thay đổi đó thường được mô tả là thu hồi quyền truy cập của nội dung web, chứ không phải xóa bỏ hoàn toàn API.
Công cụ WebKit của Apple đi theo hướng thận trọng hơn ngay từ đầu: nó chưa bao giờ đưa navigator.getBattery() vào Safari, phù hợp với lập trường chung của WebKit là từ chối triển khai các API mà đội ngũ đánh giá là mang rủi ro lấy dấu vân tay đáng kể so với lợi ích mang lại.
Các trình duyệt dựa trên Chromium là ngoại lệ. Bảng tương thích trên caniuse.com hiện vẫn cho thấy API này được hỗ trợ trên Chrome, Edge, Opera và Samsung Internet — khoảng 78% lượt sử dụng trình duyệt toàn cầu theo số liệu caniuse tại thời điểm viết bài — trong khi Firefox và mọi phiên bản Safari, kể cả Safari trên iOS, đều báo không hỗ trợ. Sự phân hóa ba chiều đó (Chromium: có, Firefox: đã gỡ, WebKit: chưa từng có) có nghĩa là chỉ riêng việc navigator.getBattery có tồn tại trên trang hay không đã là một tín hiệu nhận diện engine thô nhưng khó giả mạo, cùng nhóm với việc Network Information API chỉ tồn tại trên Chromium.
Bài học cho các Web API mới
Việc thu hồi Battery Status API là một case study hữu ích chính vì nó diễn ra công khai, trong một khoảng thời gian khá ngắn, và vì một lý do không liên quan gì đến ác ý từ phía bất kỳ ai triển khai nó — API này ra mắt đúng như đặc tả, và rủi ro chỉ trở nên rõ ràng sau khi các nhà nghiên cứu độc lập xem xét kỹ điều gì có thể xảy ra khi một cảm biến dạng số được đọc liên tục, không cần xin quyền, trên nhiều nguồn khác nhau. Bản dự thảo làm việc hiện tại của W3C giờ đây thể hiện trực tiếp bài học đó, nêu rõ rằng user agent "không nên lộ ra các số liệu trạng thái pin với độ chính xác cao vì điều đó có thể tạo ra một vector lấy dấu vân tay mới", và "có thể làm mờ giá trị được lộ ra" để script không thể chắc chắn liệu mình có đang thấy dữ liệu phần cứng thật hay không.
Đó chính là quy luật đáng được khái quát hóa: bất kỳ API nào báo cáo một giá trị số thực, liên tục cập nhật từ phần cứng — pin, cảm biến, bộ đếm hiệu năng — đều cần tự hỏi không chỉ "một lần đọc này có tính nhận diện không" mà còn "việc quan sát giá trị này thay đổi theo thời gian có cho phép hai script khác nhau nhận ra cùng một trình duyệt hay không". Việc chia khoảng và làm tròn — cùng biện pháp giảm thiểu mà những người thiết kế Network Information API áp dụng cho downlink và rtt — giờ đây là kỳ vọng mặc định cho nhóm API này, thay vì một biện pháp bổ sung sau.
Điều này có ý nghĩa gì với bạn hôm nay
Nếu bạn dùng Firefox hoặc Safari, tín hiệu cụ thể này đã bị đóng lại từ trước — cả hai trình duyệt đều không lộ nó, không cần cấu hình gì. Nếu bạn dùng trình duyệt dựa trên Chromium, navigator.getBattery() vẫn có thể được bất kỳ trang nào bạn truy cập gọi, dù đóng góp của nó vào dấu vân tay trình duyệt tổng thể nhỏ hơn nhiều so với canvas hay WebGL, vì mức pin và trạng thái sạc liên tục thay đổi thay vì giữ ổn định qua các phiên. Đây là ứng viên phù hợp cho những biện pháp phòng vệ chung tương tự các tín hiệu nhỏ khác: một tiện ích mở rộng chặn script hoặc nội dung, hoặc một trình duyệt hướng đến quyền riêng tư với cấu hình mặc định chặt chẽ hơn.
Còn một công dụng ngược lại đáng biết, và đó là lý do công cụ kiểm tra dấu vân tay của BrowserInsight vẫn đọc API pin ở những nơi nó tồn tại thay vì bỏ qua. Bốn giá trị đó buộc phải nhất quán với nhau: một thiết bị vừa khai rằng mình không sạc và chưa đầy pin, lại vừa báo "0 giây nữa là đầy" và "vô hạn thời gian mới cạn", đang mô tả một viên pin không thể tồn tại về mặt vật lý. Phần cứng thật không tạo ra tổ hợp đó; một hồ sơ trình duyệt bị giả mạo bằng script thì có. Vậy là chính những số liệu từng biến API này thành rủi ro theo dõi nay lại trở thành một phép kiểm tra tính nhất quán rất rẻ — thêm một lý do nữa cho thấy trình duyệt nói dối về bản thân thường bị lộ vì vân tay tự mâu thuẫn chứ không phải vì một giá trị đơn lẻ nào.
Câu hỏi thường gặp
Battery Status API hiện còn dùng được trên trình duyệt nào không?
Còn. Chrome, Edge, Opera và Samsung Internet vẫn triển khai navigator.getBattery(). Firefox đã gỡ quyền truy cập từ phiên bản 52 (2017), còn Safari/WebKit thì chưa từng triển khai nó.
Một trang web có thể biết chính xác pin của tôi còn bao nhiêu không?
Trên các trình duyệt còn hỗ trợ API này thì có — level báo mức sạc dưới dạng số thập phân từ 0 đến 1, không cần xin quyền. Trên Firefox và Safari, API này đơn giản là không tồn tại nên không có gì để gọi.
Battery Status API có thực sự từng bị lợi dụng rộng rãi để theo dõi không?
Không có bằng chứng cho thấy nó bị lạm dụng rộng rãi như một công cụ theo dõi chính trước khi các trình duyệt hành động — phản ứng này xuất phát từ nghiên cứu học thuật chỉ ra rủi ro, chứ không phải từ việc khai thác quy mô lớn đã được ghi nhận. Các trình duyệt đã chủ động thu hồi trước, chứ không phải phản ứng sau, đây cũng là một phần lý do khiến nó trở thành một case study đáng chú ý.
Chặn JavaScript có ngăn được tín hiệu này không?
Có. Vì việc gọi navigator.getBattery() cần thực thi script, việc tắt JavaScript hoặc dùng tiện ích chặn script sẽ ngăn mọi trang đọc được nó, trên các trình duyệt vẫn còn giữ API này.
Kết luận
Battery Status API là một mảnh nhỏ, gần như bị lãng quên trong lịch sử nền tảng web, nhưng nó minh họa rõ ràng một ý tưởng lớn hơn: một API không cần xin quyền không cần lộ tên hay vị trí của bạn để trở thành vấn đề quyền riêng tư — một cảm biến số liên tục cập nhật, được đọc từ đủ nhiều nguồn, cũng có thể làm được điều tương tự. Việc Firefox gỡ bỏ và WebKit từ chối triển khai là những ví dụ cụ thể, sớm, cho thấy các trình duyệt bắt đầu coi bề mặt lấy dấu vân tay là một ràng buộc thiết kế hàng đầu chứ không phải trường hợp ngoại lệ — lập trường mà giờ đây thể hiện trực tiếp trong cách các API mới như Network Information được đặc tả ngay từ đầu. Hãy chạy công cụ kiểm tra dấu vân tay của BrowserInsight để xem trình duyệt của bạn còn lộ những tín hiệu ít nổi bật nào.
Đọc thêm:


