navigator.mediaDevices.enumerateDevices() 在授予攝影機/麥克風權限前揭露裝置數量、ID 和標籤,追蹤者藉此建立指紋。
每一台內建攝影機和麥克風的筆電、每一台插著 USB 耳機的桌上型電腦、每一支擁有雙鏡頭的手機——這一切都能透過一次不起眼的 API 呼叫被網頁看見。navigator.mediaDevices.enumerateDevices() 最初的設計目的,是讓網站在發起視訊通話前列出可用的攝影機和麥克風。它確實做到了這一點,但同時也交出了一個完全無需權限提示的指紋訊號,而一旦使用者真的授予了攝影機或麥克風權限,這個訊號會變得更加豐富。
關鍵要點
enumerateDevices()無需任何權限提示即可執行,任何呼叫它的頁面都能取得已連接音訊/視訊裝置的數量和類型。- 在權限授予之前,
deviceId和label為空,但裝置的數量和類型(有多少攝影機、麥克風、喇叭)依然增加了資訊熵。 - 權限授予之後(哪怕只對任意一個網站授予過一次),
deviceId、groupId以及人類可讀的label字串——往往包含硬體型號名稱——就會變得可見。 groupId會關聯共享同一實體硬體的裝置,讓腳本能夠推斷出,例如某個攝影機和麥克風來自同一台網路攝影機裝置。- 僅裝置數量本身就是一個有意義的跨站訊號:大多數桌上型裝置回報的裝置集合較小且穩定,會在多次瀏覽工作階段乃至瀏覽器重新啟動之間保持不變。
enumerateDevices() 會回傳什麼
MediaDevices.enumerateDevices() 方法是 Media Capture and Streams 規範的一部分,它回傳一個 Promise,解析為一個 MediaDeviceInfo 物件陣列——對應作業系統向瀏覽器揭露的每一個音訊輸入、音訊輸出和視訊輸入裝置。每個物件攜帶四個欄位:
kind—"audioinput"、"audiooutput"或"videoinput"deviceId— 一個不透明的、限定於來源站點的裝置識別碼groupId— 一個由同一實體硬體的多個裝置共享的不透明識別碼label— 一個人類可讀的裝置名稱,例如 "FaceTime HD Camera" 或 "Logitech BRIO"
關鍵在於,這次呼叫本身不需要攝影機或麥克風權限——只有 getUserMedia() 才需要。頁面可以在載入時、在使用者與任何內容互動之前就呼叫 enumerateDevices(),立即得知這台機器上有多少音訊和視訊裝置。
權限授予前後的差異
規範刻意將 deviceId、groupId 和 label 置空,直到呼叫來源已至少被授予過一次攝影機或麥克風存取權限——這是瀏覽器廠商對指紋風險做出的讓步。但這種置空是部分而非完全的,而這兩種狀態之間的差異本身就具有資訊量。
async function getMediaDeviceFingerprint() {
const devices = await navigator.mediaDevices.enumerateDevices();
const counts = { audioinput: 0, audiooutput: 0, videoinput: 0 };
const labeled = [];
for (const d of devices) {
counts[d.kind]++;
if (d.label) {
// 只有在該來源至少被授予過一次 getUserMedia() 權限後才會填入
labeled.push({ kind: d.kind, groupId: d.groupId, label: d.label });
}
}
return { counts, total: devices.length, labeled };
}
權限授予之前:每個 deviceId 和 label 都是空字串,但 devices.length 以及按 kind 分類的數量是精確的。一台內建一個麥克風、一個攝影機,並配對了藍牙耳機的機器,與一台什麼外接裝置都沒有的裸機筆電,回報的結果不同——而這個數量在使用者造訪的每一個網站上都保持穩定,構成一個低基數但持久的跨站訊號。
權限授予之後:標籤和 ID 會立即填入,而且往往洩露的資訊不止是一個通用名稱。廠商和型號字串("Logitech BRIO"、"iPhone Microphone")很常見,deviceId 值在該來源站點上會跨工作階段保持穩定——實際上成為一個限定於該站點的半持久識別碼,其原理類似於持久訪客 ID如何借助其他穩定訊號在清除快取後依然存活。
groupId:關聯實體硬體
groupId 的存在,是為了讓應用程式能夠避免選中來自同一實體裝置、可能造成回音的麥克風和喇叭組合——但它同時也充當了一個關聯鍵。兩個共享同一 groupId 的 MediaDeviceInfo 項目,必然來自同一件硬體,這讓腳本無需讀取任何型號字串,就能推斷出例如「這個攝影機和這個麥克風是同一台 USB 網路攝影機」這樣的資訊。結合裝置數量和任何可用的標籤,groupId 透過排除實體上不可能同時出現的裝置組合,進一步收緊了指紋。
裝置數量作為跨站訊號
在眾多指紋採集技術中,裝置列舉頗為特殊:它不需要算繪管線、不需要 GPU、也不需要計時測量——它是對實體硬體清單的直接、結構化讀取。結合諸如 WebGL 和 Canvas 輸出等訊號,媒體裝置數量增加了另一個獨立維度:兩個 GPU 相同、Canvas 雜湊也相同的訪客,仍然可以被區分開——如果其中一個連接了外接顯示器喇叭和一個內建麥克風陣列的攝影機,而另一個沒有。這與我們瀏覽器指紋完整指南中所講的組合原理一致——沒有任何單一訊號是決定性的,但獨立訊號會相乘放大。
它還與 WebRTC 存在一種值得特別指出的關聯:已經為合法功能(視訊聊天、語音留言)請求攝影機/麥克風權限的網站,會「免費」取得完整的帶標籤裝置清單,使一次功能性的權限授予,變成一份持久且異常詳細的指紋貢獻。
防護措施
- 預設拒絕攝影機/麥克風權限,只在確實打算使用該功能的站點上按需授予——這樣能讓不需要該權限的站點上
deviceId和label始終為空。 - Firefox 的
media.navigator.enabled及相關偏好設定,以及基於統一性理念建構的隱私瀏覽器(Tor Browser),會限制或完全阻止大多數來源站點的列舉,代價是破壞合法的 WebRTC 通話功能。 - 瀏覽器權限管理員——審查並撤銷過時的攝影機/麥克風授權(
chrome://settings/content/camera以及 Firefox/Safari 中的對應設定)能縮小可以看到帶標籤裝置的來源站點集合。 - 拔除不使用的周邊裝置能減少裝置數量帶來的資訊熵,但對大多數人來說並不現實,而且只能封堵眾多維度中的一個。
以上措施都無法完全消除權限授予前的裝置數量訊號,因為直接封鎖 enumerateDevices() 會破壞任何需要提供裝置選擇器的網站——現實可行的目標是限制有多少來源站點能夠到達資訊更豐富的權限授予後狀態。
想知道你自己的瀏覽器已經暴露了哪些指紋訊號——Canvas、WebGL、字型等等——可以執行 BrowserInsight 的指紋檢測,衡量這些訊號讓你有多容易被識別。
常見問題
enumerateDevices() 需要攝影機或麥克風權限嗎?
不需要。呼叫本身無需任何提示即可運作,並立即回傳裝置數量和類型。只有 deviceId、groupId 和 label 欄位需要該來源站點事先取得 getUserMedia() 權限才會被填入。
網站能在不詢問的情況下知道我有多少攝影機或麥克風嗎?
可以。devices.length 以及按 kind 分類的明細,在任何權限對話框出現之前就已可用,而且它們會在多次造訪之間保持不變,因為它們反映的是連接的硬體,而不是使用者清除 Cookie 就能重設的東西。
清除 Cookie 會重設我的媒體裝置指紋嗎?
不會。裝置數量,以及一旦取得授權後的 deviceId/groupId 值,都源自硬體和按來源劃分的權限狀態,而不是清除 Cookie 會移除的儲存資料——這與瀏覽器指紋完整指南中 Canvas 和 WebGL 指紋在清除快取後依然存活的原理類似。
媒體裝置指紋已經在真實網站上使用了嗎?
指紋採集函式庫通常會將裝置數量作為眾多訊號之一納入其中,而任何擁有合法視訊通話或語音錄製功能的網站,一旦取得權限,就已經擁有了完整的帶標籤清單,無論它是否將這些資料用於追蹤。


