1. 簡介
多租戶應用程式的安全,特別是管理敏感排程與使用者資料的系統,基本上仰賴強健的存取控制機制。最近在一個廣泛使用的開源排程平台中發現了一個嚴重的漏洞,展示了一系列看似微小的邏輯缺陷如何演變成完全的帳號接管(Account TakeOver, ATO)以及數百萬筆私密預約資料的外洩 [1] 。這份報告提供對此鏈狀存取控制缺陷(Broken Access Control, BAC)漏洞的技術分析,剖析實現該攻擊的底層程式邏輯與架構弱點。此外,還將其與其他當代的存取控制和認證繞過缺陷進行比較,以突顯現代應用程式開發中常見的反模式(Anti-patterns)。
2. 鏈狀漏洞架構
帳號接管漏洞並非單一缺陷造成的結果,而是應用程式註冊與使用者管理流程中的三個不同邏輯錯誤序列,特別是當使用組織團隊邀請 Token 時 [1] 。這種攻擊途徑尤其隱蔽,因為它利用了多租戶環境帶來的複雜性,以及將全域使用者身分與組織層面的運作相協調的需求。該攻擊的關鍵在於,攻擊者在進行憑證更新之前,未能正確驗證使用者在整個系統中的存在。
攻擊流程可以視覺化為一個三階段過程,其中一個有缺陷步驟的成功執行會促成下一個步驟,最終導致受害者帳號遭到入侵。
Org Invite Token] --> B{Victim's Email
used in Signup}; B --> C{Step 1:
Username Check Bypassed?}; C -- User is
an Org Member --> D[Validation Logic Skipped:
Available=TRUE]; C -- User is NOT
an Org Member --> E[Validation Fails:
Available=FALSE]; D --> F{Step 2:
Org-Scoped Email Check?}; F -- Victim
in Different Org --> G[Query Returns NULL:
Available=TRUE]; G --> H{Step 3:
Global Email Upsert}; H --> I[Victim's Password
Overwritten]; I --> J[Victim's Account
Taken Over]; E --> K[Signup Blocked]; G -- Victim in
Attacker's Org --> K;
3. 技術深入探討:程式碼級別分析
3.1. 缺陷 1:使用者名稱驗證繞過
初始失效點位於
usernameCheckForSignup
函式中。此函式的目的是驗證給定的電子郵件是否已與現有的有效的使用者關聯。然而,該邏輯為已經是組織成員的使用者引入了一個危險的例外。
下方的程式碼片段說明了條件式繞過 [1] :
- const usernameCheckForSignup = async ({ username, email }) => {
- // ... initialization ...
- if (user) {
- // Check if user belongs to any organization
- const userIsAMemberOfAnOrg = await prisma.membership.findFirst({
- where: {
- userId: user.id,
- team: { isOrganization: true },
- },
- });
- // Vulnerability 1: Only validates if user is NOT in an org
- if (!userIsAMemberOfAnOrg) {
- // This validation only runs for non-org users
- const isClaimingAlreadySetUsername = user.username === username;
- const isClaimingUnsetUsername = !user.username;
- response.available = isClaimingUnsetUsername || isClaimingAlreadySetUsername;
- // ...
- }
- // If userIsAMemberOfAnOrg is true, response.available stays TRUE
- // This allows org members to be "re-signed up" by attackers
- }
- return response; // Returns { available: true } for org members
- };
如圖所示,如果
userIsAMemberOfAnOrg
被評估為 true,則整個驗證區塊會被跳過,使
response.available
旗標保持在其預設的
true
值。這項設計決策錯誤地發出訊號,表示該電子郵件可用於新註冊,只要受害者是任何組織的一部分,實際上就允許攻擊者使用受害者的電子郵件地址繼續操作。這是一個典型的例子,說明授權邏輯不足導致全域安全不變規則(唯一、已驗證的使用者身分)無法有效執行,原因是條件檢查過於複雜且有缺陷。
3.2. 缺陷 2:組織範圍驗證失效
第二個缺陷是後續驗證步驟中的範圍失效。此檢查嘗試尋找現有使用者,但將搜尋限制在攻擊者組織(由
organizationId
識別)的環境中
[1]
。
- const existingUser = await prisma.user.findFirst({
- where: {
- // Vulnerability 2: Only searches within the target organization
- ...(organizationId ? { organizationId } : {}), // WHERE organizationId = attacker's org
- OR: [
- // ... other checks ...
- {
- AND: [
- { email }, // Check for this email
- {
- OR: [
- { emailVerified: { not: null } }, // Email is verified
- { AND: [{ password: { isNot: null } }, { username: { not: null } }] },
- ],
- },
- ],
- },
- ],
- },
- select: { email: true },
- });
- // ...
- return { isValid: !existingUser }; // Returns true = email "available"
產生的 SQL 查詢實際上被
WHERE organizationId =
限制了範圍。如果受害者是另一個組織的成員,查詢會回傳
NULL
,導致系統錯誤地得出該電子郵件可用於建立新帳號的結論。這突顯了一個關鍵的設計錯誤,即需要全域執行的安全檢查卻以局部、多租戶的範圍來實作。
3.3. 缺陷 3:全域電子郵件 Upsert 覆寫
最後一個、也是最具破壞性的一步是執行
prisma.user.upsert()
操作。在上述兩個有缺陷的驗證通過後,系統嘗試根據全域唯一的
email
欄位建立新使用者或更新現有使用者
[1]
。
- // ...
- // Vulnerability 3: Email is globally unique, so this finds any user with this email
- const user = await prisma.user.upsert({
- where: { email }, // Matches victim's email across all orgs
- update: {
- username, // Changes username
- emailVerified: new Date(Date.now()),
- identityProvider: IdentityProvider.CAL,
- password: {
- upsert: {
- create: { hash: hashedPassword },
- update: { hash: hashedPassword }, // Overwrites victim's password
- },
- },
- organizationId, // Moves victim to attacker's org
- },
- create: {
- // This block won't execute, victim already exists
- // ...
- },
- });
- // Victim is now locked out, attacker has full access
由於
where: { email }
子句比對到了受害者現有的、已驗證的使用者紀錄,因此
update
區塊被執行。此操作使用攻擊者選擇的密碼覆寫了受害者的密碼 hash,並強制將受害者的帳號遷移到攻擊者的組織中。這單一資料庫操作完成了帳號接管,授予攻擊者對受害者資料的完整存取權,包括行事曆整合、OAuth Token 與 API key
[1]
。此漏洞是一個嚴酷的提醒:資料庫操作,特別是涉及認證更新的操作,必須先對使用者是否存在進行絕對的系統範圍檢查以及適當的授權。
4. 與其他存取控制缺陷的比較分析
Cal.com 漏洞與其他最近的存取控制和認證繞過缺陷具有共同的架構弱點,特別是關於識別碼和範圍的處理不當。
4.1. Session 驗證不足
在 Versa Concerto 管理介面中也觀察到了類似的驗證不足模式,該處發現了一個認證繞過缺陷(CVE-2023-XXXX)
[2]
。在那種情況下,漏洞源於
SessionValidator
類別中的邏輯缺陷,該類別未能對 Session 識別碼強制執行加密完整性。系統接受了一個自定義的
X-Concerto-Session
header,該 header 只需要符合 UUID 格式,而不需要任何 HMAC 簽章或其他加密驗證
[2]
。這類比於 Cal.com 缺陷的前兩個步驟,系統在沒有進行必要的全域安全檢查的情況下,錯誤地信任了一個識別碼(註冊流程中的電子郵件)。Versa Concerto 案例進一步展示了這種缺乏加密驗證的情況,結合參數注入和潛在的可預測 Session ID,如何導致未經授權的存取
[2]
。
4.2. 路徑與資源範圍操縱
另一個相關的比較可以從 AI 編排框架中發現的 ChainLeak 漏洞中得出,其中包括任意檔案讀取(AFR)和服務端請求偽造(SSRF)
[3]
。AFR 缺陷(CVE-2026-22218)的產生是因為檔案持久化函式
BaseSession.persist_file
接受了攻擊者控制的
path
屬性,而沒有驗證是否造成路徑操縱(Directory traversal)的問題
[3]
。這允許已認證的攻擊者透過操縱路徑指向預期範圍之外(例如
../../../../etc/passwd
)來讀取任意檔案。雖然 Cal.com 缺陷涉及的是使用者身分而非檔案路徑,但底層原理是一樣的:系統在處理外部輸入(電子郵件地址)時未能強制執行邊界(全域使用者存在 vs. 組織範圍),就像 ChainLeak 缺陷在處理路徑時未能強制執行邊界(Session 目錄 vs. 檔案系統 root)一樣
[3]
。這兩個案例都強調了在沒有嚴格的系統範圍範圍強制執行的情況下,信任用戶端提供的資料(無論是註冊表單中的電子郵件還是 API 請求中的檔案路徑)的危險。
5. 緩解與縱深防禦
Cal.com 漏洞的修復涉及在允許使用邀請 Token 進行註冊流程之前,增加全面的使用者存在驗證 [1] 。然而,為了防止類似缺陷,需要更廣泛的縱深防禦策略。
1. 強制執行全域性不變規則(Global Invariants): 任何修改全域唯一且關鍵欄位(如電子郵件或密碼)的操作,都必須先驗證使用者在整個應用程式中的狀態,而不受當前多租戶環境的限制。驗證應該是一個簡單、無範圍限制的檢查:「具有此電子郵件的已驗證使用者是否存在於任何地方?」
2. 安全的 ORM 使用:
開發人員必須敏銳地意識到
upsert
等物件關係對應(Object-Relational Mapping, ORM)函式的安全影響。當使用同時也是主要安全 key(如電子郵件)的唯一識別碼進行
upsert
時,
update
區塊必須受顯式授權檢查的保護。使用者應該只能更新自己的紀錄,且註冊流程絕不應被允許更新現有的、已驗證使用者的認證。
3. 架構隔離: 正如在 Versa Concerto 分析中所見,單體設計或元件隔離失效等架構弱點會加劇漏洞 [2] 。將認證與授權服務與核心業務邏輯隔離,並對所有資料庫連線強制執行最小權限原則,可以限制受損元件的損害範圍。
4. 輸入驗證與清理: ChainLeak 案例強調了對輸入進行嚴格驗證的需求,特別是對於 URL 和檔案路徑等資源識別碼 [3] 。對於 Cal.com 案例,這意味著要確保註冊流程中使用的電子郵件地址不僅在語法上有效,而且在現有、已驗證使用者的環境中也是語義上有效的。
6. 結論
Cal.com 帳號接管漏洞是一個關於多租戶應用程式安全中鏈狀邏輯缺陷危險性的引人注目的個案研究。該攻擊利用組織範圍的註冊流程來繞過全域使用者驗證,並透過全域資料庫 upsert 強制覆寫受害者的認證,展示了應用程式授權模型的深刻失效。透過將此缺陷與最近涉及 Session 驗證和資源範圍操縱的其他漏洞進行比較 [2] [3] ,一個清晰的模式浮現:現代應用程式架構的複雜性,特別是涉及多租戶和 ORM 的架構,要求開發人員對所有輸入採取零信任方法,並在每個關鍵節點嚴格執行全域安全不變規則。解決此類漏洞不僅需要修補直接的程式碼,還需要對應用程式的安全架構進行根本性審查,以確保縱深防禦。