vatt'ghern jaskier's ballads

tl;dv 的九個 Firestore collection 都設了租戶檢查,回應正確的 403;第十個——meetings——沒有,於是任何登入過的使用者,都能查到平台上其他帳號的每一場會議紀錄。

181,874 場會議,少了一道租戶檢查

篇記的是 tl;dv 一個 Firestore collection 漏掉租戶檢查、如何被人發現、資料曝光範圍多大,以及廠商足足半年沒修好的過程。tl;dv 是掛在 Google Meet、Zoom、Teams 上的 AI 會議錄製與摘要平台,官方稱擁有超過兩百萬使用者,靠著把一個機器人丟進通話裡,自動錄音、轉逐字稿、生成摘要來吸引用戶。發現這個漏洞的研究者把文章發表在 bobdahacker.com 上,標題本身就是一句雙關——「Too Lazy; Didn't Validate」,呼應 tl;dv 品牌全稱裡的「Too Long; Didn't View」;文章寫於 8 月 4 日,距離文中提到的最後一次追蹤留言,又過了將近兩週,據作者的說法,漏洞當時仍未修復。

一行查詢,命中全平台

一個已經登入 tl;dv 的帳號,去查詢後端 Firestore 裡的 meetings collection,理論上應該只看得到自己公司的會議紀錄——資料庫層本來就該替每一次查詢加上一個只准看自己 org 的條件。資安研究者實際查了這個 collection,看到的卻是完全不限於自己帳號底下的資料:「I queried the Firestore `meetings` collection and saw there were 181,874 meeting records belonging to 84,312 unique users across 35,003 email domains.」181,874 筆會議紀錄,橫跨 84,312 個唯一使用者、35,003 個 email 網域。

作者對這個漏洞性質的定性很直接:「The `meetings` collection has no tenant isolation. Any authenticated tl;dv user can query every meeting across every account on the platform.」沒有偽造 token,沒有繞過登入,帳號本身完全合法——問題出在查詢 meetings 這個 collection 時,後端沒有替結果加上任何一道租戶邊界。接下來這段關於 Firestore 資料模型的背景是我自己補上的,原文並未提及:Firestore 這類文件資料庫本身沒有傳統關聯式資料庫那種 schema 限制,也沒有外鍵拘束,一個 collection 底下可以塞進任何貼著不同 org 標籤的文件;安全規則因此是唯一一道防線,決定一次查詢能不能執行,而不是資料庫結構本身替你擋掉跨帳號讀取。這代表『資料庫有沒有租戶隔離』不是設計階段一次決定就永遠成立的事,而是要每一條規則各自寫對,少一條,就是少一道防線。

這裡是我自己的技術補充,原文沒有寫到這段:Firestore 的安全規則通常長得像 allow read: if request.auth != null && resource.data.orgId == request.auth.token.orgId——先確認登入身分,再比對這筆文件的 org 欄位是不是跟登入者一致。這一行判斷式如果漏寫,效果就是『只要登入就查得到全部』,因為第一個條件(有沒有登入)仍然成立,只是第二個條件(是不是你的資料)從來沒被檢查過。

① 登入驗證(token 有效)
② 查詢送到 collection
③ 租戶比對(org_id)——9 個 collection 有,meetings 沒有
④ 回應:403 或整批資料
這是我根據文中「九個 collection 回 403、meetings 回資料」這個現象畫出的簡化示意,不是 tl;dv 公開過的實際架構圖——真正被跳過的只有第③層。

九個 collection 都對,第十個沒有

這類問題在多租戶 SaaS 裡並不罕見,也很容易被誤認成認證問題,但重點在於使用者的登入是有效的,出問題的是資料庫層有沒有替某一個 collection 加上只准讀自己那份資料的條件。認證(authentication)答的是『你是誰』,授權(authorization)答的是『你能看什麼』——同一次請求可以完全通過第一關,卻在第二關被放水。tl;dv 這次的狀況正是後者:token 有效、帳號真實,問題出在資料存取這一層沒有替每個 collection 都套上租戶邊界。作者在文章裡把對照寫得很直白:「You already do it correctly for every other collection (users, chats, transcripts, clips, recordings, videos, notes, teams, organizations all return 403). You just forgot meetings.」users、chats、transcripts、clips、recordings、videos、notes、teams、organizations 這九個 collection 全部正確回了 403,只有 meetings 一個沒有做租戶檢查。

以下三個定義是我自己補充的產業通用說法,不是原文內容:租戶隔離多租戶系統裡,確保每次查詢只能讀到自己組織資料的機制,通常由一條「比對 org 欄位」的規則實作。BOLABroken Object Level Authorization,OWASP API 資安風險清單裡的一類漏洞:身分驗證正常,但系統沒有檢查這個物件是否真的屬於這個使用者。、以及authentication/authorization前者驗證「你是誰」,後者驗證「你能看什麼」;同一次請求可能通過前者,卻在後者被放行。——這三個詞,本文用來描述 tl;dv 這次漏洞的性質。

滑鼠移到底線詞彙上,或用 Tab 鍵移過去,看完整定義。

這代表 tl;dv 的工程團隊清楚知道要做這件事,多數地方也做對了,只是有一個 collection 漏掉。合理的推測是,這類租戶檢查是逐一手寫在每條安全規則上,而不是統一經過某一層一定會套用的中介——才會出現九個對、一個錯這種局部失效:只要新增一個 collection 時漏寫一次,就是一次獨立的資料外洩,不是整個系統的認證機制壞掉。這種『大部分做對、一處漏掉』的失效模式,稽核時特別難抓——滲透測試如果只抽測幾個 collection、剛好避開沒做的那一個,測試會通過,問題卻還在;要抓到這類漏洞,通常得把每一個 collection 都各自跑一次未授權查詢測試,而不是抽樣測幾個就當作整體合格。

這類局部失效常見的成因,是安全規則沒有跟著 collection 一起被『模板化』——工程團隊一開始可能是照著已有的 collection 複製貼上寫新的規則,新增 meetings 這樣的 collection 時,如果剛好是另一個人、用另一種方式加進去的,就容易漏掉那一行租戶判斷式。這不需要惡意,也不需要多離譜的疏忽,只要一次 code review 沒抓到、一次上線前的檢查清單沒把『新 collection 是否有租戶檢查』列進去,漏洞就會安靜地存在,直到有人專門去查。這類稽核方法並不複雜——把應用程式實際用到的每一個 collection 名稱列出來,對每一個各自跑一次跨帳號存取測試,確認回應是 403 還是資料本身,而不是只抽測幾個看起來重要的;這種『每個都測』的地毯式檢查效率不高,卻是抓出『九個對、一個錯』這種局部失效唯一可靠的辦法。

meetings 這個 collection 存的是會議的中繼資料,不是逐字稿本身。作者列出了每一筆會議紀錄裡有什麼:「Each meeting record hands you the creator's email address, the conference ID (which is a joinable Google Meet or Teams room), the provider, the recording status, and timestamps.」建立者的 email 位址、會議代號(一個可以直接加入的 Google Meet 或 Teams 房間)、服務供應商、錄製狀態,還有時間戳記。真正的談話內容存在 transcripts 裡,而 transcripts 剛好是九個做對的其中一個,查不到——這也是為什麼同一份外洩資料,能一路從紀錄延伸到正在進行的通話:會議代號本身就寫在裡面。

點選 checkbox 切換每個 collection 的租戶檢查狀態 · 6 個 collection

200 OK,這是目前真實狀況:181,874 筆會議紀錄可被任何登入帳號讀到
403 Forbidden(目前真實狀況)
403 Forbidden(目前真實狀況)
403 Forbidden(目前真實狀況)
403 Forbidden(目前真實狀況)
403 Forbidden(目前真實狀況)
切換任一 collection 的檢查開關,右側文字會即時更新。預設狀態就是 tl;dv 被揭露時的真實狀況——六個裡面只有 meetings 沒打勾。

上面這個開關只做一件事:把「meetings 該不該有租戶檢查」這個決定,跟其他九個 collection 放在同一排來看。現實裡差的正是這一格:十個裡漏掉一個。這也是為什麼九個 collection 都對、一個沒對這件事格外諷刺——tl;dv 明明知道租戶隔離該怎麼做,而且在九個地方都做對了,唯一缺的不是技術能力,是一次完整的覆蓋率檢查。

Firestore、Supabase 這類 BaaS 服務近幾年被大量新創採用,理由很直接——不用自己管資料庫叢集、不用自己寫一層 API gateway,前端可以直接對著資料庫查詢。這個省下來的工程量,換來的代價是安全規則從『後端程式碼裡的一個 if 判斷』變成『資料庫設定裡的一條宣告式規則』,寫的人往往是同一批做產品功能的工程師,而不是專職的資安團隊逐條審查。tl;dv 這次的漏洞,就出現在這個轉移的縫隙裡:九個 collection 的規則都寫對了,meetings 這一個沒有被覆核到。

政府會議室,加上員工的姓名與信箱

181,874 不是一個抽象數字。作者列出的清單裡,有來自 23 個國家的政府會議——巴西、哥倫比亞、秘魯、烏克蘭、薩爾瓦多、菲律賓、智利、印尼、墨西哥、美國、卡達、馬來西亞、烏茲別克、斯里蘭卡、海地、南非、牙買加、宏都拉斯、阿根廷、泰國、日本、以色列、貝里斯;也有柏克萊、東京大學、德拉薩大學、哥倫比亞國立大學這類學術機構的會議;企業這邊,三井倉庫(Mitsui-Soko)一家就貢獻了 484 場會議,分散在四個地區辦公室。

類型具體案例規模
政府機關馬來西亞教育部、烏克蘭、美國等在內的政府會議23 個國家
大學柏克萊、東京大學、德拉薩大學、哥倫比亞國立大學4 所點名
企業三井倉庫(Mitsui-Soko),分散於四個地區辦公室484 場會議
作者列出的曝光案例只是抽樣,完整名單涵蓋 84,312 個唯一使用者、35,003 個 email 網域——這裡挑的三類,是文章裡點名到具體機構或數字的部分。

84,312 個唯一使用者分布在 35,003 個 email 網域,粗略換算,平均一個網域不到三個使用者——這代表曝光的不是少數幾家超大型企業,而是大量規模不一的組織同時被捲進來,從政府部門到幾個人的新創團隊都在裡面。馬來西亞教育部的會議紀錄、跨國企業的內部溝通、大學研究團隊的討論——這些原本應該只留在各自組織內部的對話,因為一個 collection 的租戶檢查缺失,變成任何一個 tl;dv 帳號都能翻到的資料。曝光的除了「誰跟誰開過會」這種 metadata,也包括使用者原本對這家工具的信任——他們把敏感討論交給 tl;dv 錄下來,預期資料會被妥善保護。

為了確認這不只是資料庫裡的靜態紀錄,作者用曝光資料裡撈到的會議代號,實際加入了兩場正在進行的視訊會議——一場是馬來西亞教育部的會議,一位女性正在對 157 位以上的與會者簡報;另一場是美國一所大學裡,一群學生在討論他們正在做的新創應用,21 人線上。這兩次實測把資料外洩的抽象說法,變成了陌生人可以直接旁聽正在開的會這個具體後果。

從紀錄外洩到實際旁聽一場正在進行的會議,中間有一段常見的爭論——多少驗證才算「足夠」,多少驗證又已經算「濫用」。多數負責任揭露的慣例是:只驗證到能證明風險真實存在就停,不下載、不散布、不進一步深挖曝光資料裡的內容——這條慣例同樣是我自己補充的產業背景,原文並未寫到。作者這裡選擇用「加入一場正在開的會,親眼看到裡面有真人」來證明影響,而不是單純引用一個資料庫查詢結果的截圖,用意應該是讓廠商沒辦法把問題輕描淡寫成「只是後端測試資料」。

揭露過程裡還帶出一個附帶發現——tl;dv 內部給員工玩的一個世界盃預測遊戲,後端建在 Base44 上,它的 Player 資料 API 完全沒有認證:「The Player entity API has zero authentication. `GET /api/entities/Player` returns every player record without a session cookie. 43 players. 19 @tldv.io employees with full names and corporate emails.」這跟 meetings 的問題不是同一類——meetings 是登入了但沒做租戶隔離的授權失效,Player API 是根本不需要登入的認證缺失。兩者放在一起看:同一家公司、同一段時間,同時存在兩種不同層級的存取控制漏洞,一個在核心產品,一個在內部小工具。43 名玩家裡,19 個是 tldv.io 的員工——換句話說,這個內部小遊戲的參與者,超過四成本來就是公司自己人,員工姓名跟公司信箱幾乎是白送出去的資料,而且不需要任何查詢技巧,直接打一個沒有驗證的端點就能拿到全部。

作者對這個小工具的描述是:「A FIFA World Cup 2026 vibecoded prediction game built on Base44 for tl;dv employees.」——這個給 tl;dv 員工玩的 2026 世界盃預測遊戲,名字叫 World Cup Pick'em。問題不在遊戲本身,而在於這類工具通常不被當成「正式產品」對待,資安檢查的優先順序自然排到後面,卻照樣連著公司網域的帳號系統,一樣能被拿來收集員工的姓名和信箱——核心產品與內部小工具的資安等級不一致,是很多公司共同的盲點,不是 tl;dv 獨有。

把兩個漏洞放在一起看,剛好是同一堂課的兩個極端:meetings 是「驗證了身分、卻沒檢查邊界」,Player API 是「邊界形同虛設,因為根本沒有驗證身分」。前者需要在資料存取層逐一補齊租戶斷言,後者只是少了一個最基本的「要不要登入」判斷——同一家公司,兩種完全不同層級的疏忽同時存在。

切換分頁比較兩個漏洞 · 2 個分頁

失效層級:授權(authorization)。

狀況:使用者已登入、token 有效,但查詢 meetings collection 時,後端沒有檢查這筆資料是不是屬於使用者自己的組織。

後果:181,874 筆會議紀錄、84,312 個唯一使用者、35,003 個 email 網域,任何登入帳號都能讀到。

失效層級:認證(authentication)。

狀況:World Cup Pick'em 這個內部小遊戲的 Player 資料 API 完全不要求登入,任何人都能直接呼叫。

後果:43 名玩家紀錄外洩,其中 19 名是 tldv.io 員工的姓名與公司信箱。

兩個漏洞同時存在同一家公司、同一段時間,卻是完全不同層級的存取控制問題。

從一月到七月,回覆一直是同一句

tl;dv 的資安頁面寫著:「Our security team will respond within 24 hours.」作者 1 月 28 日把漏洞報給 Raphael Allstadt 和 CTO 信箱,當天就收到回覆:「thank you! can you report it to our CTO and we will look at it immediately?」兩天後,1 月 30 日,又收到一句:「I am sure the team is reviewing it very very soon ❤️」

接下來三個星期沒有下文。2 月 19 日,第二次安撫:「We're on it. It needs some time, but rest assured we're following through.」3 月 6 日,作者再留言追蹤,寫的是「still not fixed.」——訊息當天被讀過,沒有回覆。之後再沒有新的進展,直到 7 月 22 日,作者最後一次留言:「still not fixed...」同樣沒有回覆。作者自己在文章裡算了這筆帳:「I reported this on January 28th, 2026. It is now July 2026. Six months later.」

拖曳把手或用左右方向鍵看每個時間點的回應內容 · 5 個事件、跨 175 天

揭露時間軸(1/28 → 7/22,175 天) 1/28 1/30 2/19 3/6 7/22 Day 0
拖曳把手,查看每個時間點作者收到(或沒收到)的回應。
五個事件的時間間隔並不平均——前兩次回覆只隔兩天,之後動輒相隔三到五週,最後一次追蹤與上一次追蹤之間空了超過四個月。

tl;dv 資安頁面上的「24 小時回應」承諾,保證的其實只是「有人看一眼」,不保證「問題被解決」。這次的時間軸剛好把兩者的落差攤開來看:一月二十八日、一月三十日、二月十九日,每一次回覆都發生在合理時間內,語氣也都是正面的;只是回覆的內容從頭到尾沒有變成一次實際的修復,而是一次又一次的「我們在處理」。

三月六號到七月二十二號之間,中間空了超過四個月,是整條時間軸裡最長的一段靜默——沒有新的安撫訊息,也沒有修復通知,只有作者自己標記的「已讀不回」。作者最後在文章裡自己把帳算了一遍:一月二十八日通報,現在是七月,六個月過去了。半年,對一個涉及十八萬筆會議紀錄、跨二十三國政府單位的漏洞來說,是很長的一段時間。

合理的推測是:業界常見的協調揭露慣例,通常給廠商 90 天窗口——收到通報後 90 天內修復或至少給出明確時程,過了才考慮公開細節,用意是逼廠商動起來,同時不讓漏洞無限期懸在那裡。以上產業背景是我自己額外補充的,原文並未提及;tl;dv 這個案例遠遠超過了這個窗口,作者選擇把整條時間軸攤開來公開,很可能也是在回應這種慣例失靈之後,研究者常見的最後一步。

截至作者最後一次追蹤,這個漏洞的狀態仍然是「存在」——這代表任何時候,只要有人願意花時間查詢 meetings 這個 collection,都還能重複拿到同樣範圍的資料。對已經把會議紀錄餵進 tl;dv 的組織來說,這是一個持續開著的外洩窗口,直到修復真的上線為止。

這起事件對讀 Firestore、Supabase 之類 BaaS 後端的工程師來說,值得抄一份查核清單下來:把每一個 collection 或 table 的存取規則列成一張表,逐條確認有沒有租戶欄位比對;針對每一個 collection 各自送一次跨帳號的未授權讀取測試,而不是只測熱門的那幾個;把「新增 collection 時要不要租戶檢查」寫進上線前的檢查清單,讓它變成流程的一部分,而不是靠工程師記性。更穩妥的做法,是把租戶檢查放進一層所有查詢都必須經過的中介,而不是靠工程師逐一記得替新 collection 補上這一句。安全頁面上寫著 24 小時回應,跟實際六個月未修這兩件事之間的落差,說明的也不只是流程問題——是同一個漏洞,被反覆確認、反覆延後,卻始終沒有排進真正的修復排程。

查核清單:多租戶架構的資安稽核不能只看有沒有做租戶隔離,要看是不是每一個 collection 都做了——181,874 筆會議紀錄外洩的原因,只是十個裡漏了一個。