Guidant AI 資安檢視總報告 · 連線清單
證據同步,依設定
客戶把稽核證據放在 Google 雲端硬碟、由系統自動同步進來走的那條線。它是雙向的:系統用客戶授權的 Google 帳號去雲端建資料夾、抓檔案,Google 也會把「檔案變了」的通知推回系統。這一頁回答:這條線怎麼連、哪些功能走它、它帶來什麼威脅、駭客怎麼打、我們要怎麼防、目前做到哪。
這一頁怎麼來的:DFD Level 0 把產品運作時的連線編成 C01~C25;STRIDE 六頁每一條問題都標了發生在哪幾條線。這裡以連線為單位整理。10 條命中裡有 3 條(M07-1、M07-2、M07-4)屬於已拆除的舊版證據分類線——那九支網址已不存在,本頁仍列為實例,因為它們示範的手法(拿到編號就用公司的鑰匙去 Google 抓)在這條線上永遠成立。個別問題修了沒不在這頁講。
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB'}}}%%
flowchart LR
ADM(["👤 客戶管理員<br/>(授權 Google 帳號)"])
ANY(["🕵 網路上任何人"])
LINK(["👤 拿到資料夾連結的人<br/>(不需本系統帳號)"])
subgraph MAIN["🏠 主系統"]
direction TB
TOK[("OAuth 權杖<br/>每租戶一組,Fernet 加密落庫")]
API["guidant-api<br/>授權回呼・建資料夾・抓檔案"]
WH["webhook 入口<br/>/webhooks/google-drive/{tenant}<br/>不需登入,比對通行碼"]
WK["worker<br/>背景同步(系統身分)"]
TOK --> API
TOK --> WK
WH -- "C09 排工作" --> WK
end
GD(["☁️ Google Drive<br/>客戶的雲端硬碟"])
ADM -- "C02 · 授權/重建資料夾" --> API
API == "C14 · HTTPS · OAuth(scope: drive 全權限)<br/>建資料夾・設分享・抓檔" ==> GD
WK == "C14 · 拉變更清單・下載檔案" ==> GD
GD -. "C14 · 推變更通知(通行碼)" .-> WH
ANY -. "可直接打" .-> WH
LINK -. "開連結即可編輯" .-> GD
classDef ext fill:#FBFCFC,stroke:#9AA8AA,stroke-dasharray:3 2
classDef open fill:#FDECEC,stroke:#C0392B
class ADM,ANY,LINK,GD ext
class WH open
| 項目 | 現況 |
|---|---|
| 誰 → 誰 | 往外:guidant-api(授權、建資料夾、驗憑證)與 guidant-worker(背景同步、下載檔案、重建資料夾)→ Google Drive API。往內:Google → guidant-api 的 webhook 入口 |
| 協定/埠 | HTTPS 到 googleapis.com(SDK 預設憑證驗證);webhook 是 Google 打我們的 POST /webhooks/google-drive/{tenant_id}——Google 規定通知網址必須 HTTPS,系統對外網址不是 https 時登記失敗、退回定時輪詢(功能照常、只是慢) |
| 用誰的身分 | 客戶管理員在系統裡點「授權」→ Google OAuth 同意畫面 → 回呼頁(C04)→ 系統拿到該 Google 帳號的權杖。申請的範圍是 auth/drive(整個雲端硬碟的完整讀寫)+ userinfo.email,不是只限本應用建的檔案 |
| 權杖存哪 | 每租戶一組,DRIVE_TOKEN_ENCRYPTION_KEY Fernet 加密後落庫(fernet_crypto.py);一個 Google 帳號只能被一家客戶接(M24-4 修後,應用層 409 + DB 部分唯一索引) |
| 應用程式憑證 | OAuth client_id/client_secret 存 GOOGLE_DRIVE_APP_CONFIG,只有平台管理員能改(DRIVE_APP_CONFIG_PLATFORM_ADMIN_ONLY = True),密鑰加密落庫、讀取遮成 is_set |
| webhook 怎麼驗 | 比對 X-Goog-Channel-ID 與 X-Goog-Channel-Token 跟庫裡登記的,用 hmac.compare_digest 固定時間比對;庫裡沒設的租戶一律拒。驗過只做一件事:排一個同步工作(不信通知內容) |
| 建出來的資料夾 | 專案/輪次/任務每一層建資料夾後呼叫 share_anyone_writer()——「知道連結的任何人都能編輯」(決策者 10-01 裁定不改) |
| 誰能操作 | 授權、刪除連結、重建、登記通知:能力點 cloud_integration.{read,update,delete} +授權檢查 require_license("cloud_integration");「重建專案資料夾」另加專案管理者檢查(M24-2 修後) |
| 何時存在 | 依設定(授權完成後)。授權檔沒買 cloud_integration 模組時整組端點 403 |
依據:app/cloud_integration/service/google_drive_integration_service.py(REQUIRED_SCOPES、輪詢退路)、google_drive_webhook_service.py(_same_secret)、infra/cloud_integration/crypto/fernet_crypto.py、infra/cloud_integration/google_drive/google_drive_api_client.py(share_anyone_writer)、api/cloud_integration/routes/*.py(守門)、common/constant/drive_app_config.py、app/system_config/service/guarded_system_config_service.py。
DFD 對照:DFD 寫「OAuth token(DRIVE_TOKEN_ENCRYPTION_KEY 加密落庫);webhook 驗通行碼」與實況相符。補充 DFD 沒寫的:① scope 是整個雲端硬碟;② webhook 登記失敗會退回輪詢,所以「Drive 推變更通知進來」不是必然存在。
| 群 | 功能 | 對威脅的意義 |
|---|---|---|
| ① 授權與斷開 | 管理員點「授權」→ Google 同意畫面 → 回呼;「斷開」刪權杖 | 權杖是整個雲端硬碟的鑰匙;誰能授權、一個帳號能接幾家,決定鑰匙落在誰手上 |
| ② 建資料夾 | 專案啟動時自動建;「重建專案資料夾」「全部重建」手動觸發 | 每個資料夾建完就設「任何人可編輯」;重建時依專案編號找專案——編號是不是你的、專案是不是這家的,是 M24-2 的洞 |
| ③ 背景同步 | Google 通知或定時輪詢 → worker 拉變更清單 → 查「這個檔對應哪張任務」→ 新增/更新/標記刪除證據 | worker 以系統身分跑(SYSTEM_USER_ID = 0、SYSTEM_IS_ADMIN = True,不受 RLS);查對應時帶不帶租戶條件,決定一家的變動會不會落到另一家 |
| ④ 抓檔案 | 同步時下載證據;(已拆除)舊線的「預覽證據檔」 | 用的是客戶那把全權限鑰匙——拿到任何檔案編號就抓得到那個帳號摸得到的所有東西 |
| ⑤ 驗應用程式憑證 | 系統設定頁「驗證應用程式憑證」按鈕 | 拿操作者送來的 client_id/secret 向 Google 試;是「用系統替你試憑證」的入口 |
掃描在這條線上抓到 10 條問題,歸納成五種攻擊手法。
| # | 風險 | 威脅 | 駭客怎麼打 | 得手什麼 | STRIDE | 實例 |
|---|---|---|---|---|---|---|
| T1 | 🟠 高 | 拿到編號就用公司的鑰匙去 Google 抓 | ① 任一登入帳號;② 在預覽或查詢網址填一個雲端硬碟檔案編號(別人分享過的連結、郵件裡的連結都看得到);③ 系統用授權那個 Google 帳號的完整權限去抓;④ 那個帳號摸得到的任何檔案——別的專案的證據、人事檔、合約——原樣送回來。查不到紀錄時還退回直接讀雲端硬碟 | 授權那個 Google 帳號所能觸及的全部檔案 | I 資料外洩 | M07-1 舊線「預覽證據檔」填任何雲端硬碟檔案編號就把檔案抓回來 🟠 M07-2 舊線查結果、查報表不檢查專案成員,查不到還退回直接讀雲端硬碟 🟡 |
| T2 | 🟠 高 | 背景工作以系統身分跑,跨公司動別人的東西 | ① A 公司任一帳號登入;② 拿 B 公司的專案編號送「重建專案資料夾」;③ 入口只驗登入就排進背景;④ worker 以系統身分載入專案樹、不看公司,把 B 的整個專案結構建進 A 的雲端硬碟;⑤ A 往那些資料夾丟檔,下一輪同步寫成 B 公司任務的證據。另一條路:B 也接了同一個 Google 帳號,A 在雲端刪一個檔,同步按 Google 編號查到的是 B 的任務 | 別家客戶的專案結構;把假證據摻進別家的稽核;改動或刪除別家的證據對應 | I 資料外洩、T 竄改資料 | M24-2 重建專案雲端資料夾不檢查專案是不是你的,背景還用系統身分載入 🟠 M24-4 雲端硬碟背景同步查資料時不分公司 🟡 |
| T3 | 🟡 中 | 資料夾對全世界開放編輯 | ① 公司裡任一登入帳號取得根資料夾連結(畫面上有、或同事轉傳);② 用瀏覽器打開,不需要任何專案權限、不需要本系統帳號;③ 看、改、刪全公司的證據檔;④ 被改過的檔案下一輪同步時被匯入成正式證據 | 全公司雲端硬碟上的稽核證據(可讀可改可刪) | I 資料外洩、T 竄改資料 | M24-8 所有雲端硬碟資料夾設成「知道連結的任何人都能編輯」 🟡 |
| T4 | 🟡 中 | 使用者的字拼進 Google 的搜尋語法 | ① 能編輯專案或稽核計畫名稱的人,把名稱改成結尾帶反斜線再接引號與額外條件;② 管理員初始化或重建該專案的雲端資料夾;③ 系統依名稱找同名資料夾的搜尋條件 name='…' 被改寫,找到並「沿用」一個不該沿用的資料夾。舊線另有資料夾編號直接拼進 '<編號>' in parents |
系統把證據寫進或讀自不該碰的雲端資料夾;或撈到那個帳號摸得到的檔案清單 | T 竄改資料、I 資料外洩 | M07-4 舊版證據分類的雲端硬碟搜尋條件用字串拼接 🟡 M24-14 依名稱找雲端資料夾時沒處理反斜線 ⚪ |
| T5 | ⚪ 低 | 從不需登入的通知入口與驗憑證按鈕下手 | (a) ① 不需帳號,對 webhook 入口大量送請求,每次換通行碼開頭;② 量回應時間差一個字一個字猜;③ 猜中就能偽造「硬碟有異動」通知讓系統一直做同步。(b) ① 平台管理員群組裡沒分到儲存設定權限的人;② 直接送驗憑證請求帶任意一組 Google 憑證;③ 系統替他向 Google 試對錯。(c) Google 短時間推多則通知,每則都排一個工作 | 偽造通知讓系統白做同步;用系統替你試憑證;多跑幾次同步 | S 冒充身分、E 權限提升、D 讓服務停擺 | M24-11 雲端硬碟通知的通行碼比對可用時間差慢慢猜 ⚪ M24-12 驗證雲端硬碟應用程式憑證只查是不是平台管理員 ⚪ M24-15 雲端硬碟通知說有去重複、實際沒做 ⚪ |
五種手法的共同點:這條線的鑰匙是一個真人的 Google 帳號、整個雲端硬碟的權限——T1、T4 都是「系統拿這把鑰匙替呼叫者做事,卻沒先問呼叫者有沒有資格」;T2 是「背景工作沒有呼叫者身分,於是也沒有公司邊界」;T3 是「鑰匙之外還把門打開」。只要 scope 還是整個雲端硬碟、資料夾還是任何人可編輯,程式層的守門修得再好,鑰匙本身的半徑就是這麼大。
對應五種威脅與這條線的本質,防線分七條。D1~D5 直接對應掃描抓到的手法;標 ◇ 的(D6、D7)是依這條線的特性補的標準防線,掃描範圍沒涵蓋、沒有實例。「目前」欄寫程式與設定裡實際有的機制;⚠️ 表示只靠慣例或只守到局部。
| # | 防線 | 擋哪種威脅 | 目前做到哪 |
|---|---|---|---|
| D1 | 拿雲端編號之前先驗「這個檔案屬於這一批、你是這個專案的人」。不接受呼叫者直接給 Drive 檔案編號;查不到紀錄就 404,不退回直接讀雲端 | T1 | ✅ 舊線九支網址(含預覽、查結果、報表)整條拆除(CM-2222),契約測試「這九支必須不在」;現行同步路徑的檔案編號全由系統從變更清單取得,呼叫者給不了。⚠️ evidence_drive_ops.py 類別檔仍在(DI 仍建它),只是沒有網址呼叫到;日後有人掛回來,契約測試會紅 |
| D2 | 背景工作帶「預期的公司」,不同就整棵不建;入口先以呼叫者身分(受 RLS)查資源。手動觸發的端點排工作前查專案存在且呼叫者是管理者;worker 載入時比對專案公司與工作公司;證據回寫前反查任務所屬專案比對公司 | T2 | ✅ trigger_init_project_folders() 404/403 守門;ProjectTreeLoader.load 帶 expected_tenant_id 不同就終止不重試;import_drive_file_handler 回寫前比對;一個 Google 帳號只能被一家接(應用層 409 + DB 部分唯一索引)。驗證:DEV 真服務 6 情境。⚠️ M24-4 修的是「不讓兩家接同一帳號」這個根因,同步處理器六處補租戶條件屬另一條路、本卡沒做——若唯一索引被繞過(手動改 DB),同步仍不分公司 |
| D3 | 資料夾分享只限公司網域或專案成員,根資料夾連結不回給沒權限的人 | T3 | 🚫 裁定不修(決策者 2026-10-01):雲端硬碟分享權限交由客戶在 Google 端管、或未來的權限管理功能。share_anyone_writer 仍在四處呼叫(init_project_folders_handler、create_folder_handler、archive_drive_file_handler、drive_sync_orchestration_service)。這是有意識的取捨——記在這裡是因為它直接決定 T3 成不成立 |
| D4 | 拼進 Google 搜尋語法的值一律跳脫:先把反斜線換成兩個、再跳脫單引號(順序不能反);資料夾編號驗格式 | T4 | ✅ find_folders_by_name() 兩步跳脫(CM-2213),驗證 a\'b、x\ 兩種輸入;舊線的編號拼接隨拆除消失。⚠️ 跳脫寫在 API client 單一方法,新加一種依名稱查的功能要記得 |
| D5 | 不需登入的入口用固定時間比對,驗過只排工作不信內容;驗憑證按鈕疊能力點 | T5 | ✅ _same_secret() 走 hmac.compare_digest(轉 bytes,中文標頭不炸);庫內未設一律拒;通知只排工作、內容只看 resource_state;verify() 疊 viewer_has_capability("storage-config.read") 擋在打 Google 之前。🚫 去重複裁定不修(同步可重跑、無害) |
| D6 ◇ | OAuth scope 收窄到 drive.file(只看得到本應用建的檔案),而不是整個雲端硬碟 |
T1、T4 的半徑;D3 不修時的替代方案 | ⚠️ 沒有。REQUIRED_SCOPES 是 auth/drive 全權限。這一條沒在 168 條裡——掃描看的是程式有沒有守門,不看鑰匙本身多大。收窄 scope 會讓「客戶自己手動丟進資料夾的檔」看不到,是功能取捨,要決策者裁 |
| D7 ◇ | webhook 入口有速率限制。不需登入的端點被大量打時,固定時間比對只讓猜不準、不讓打不動 | T5(a) 的 DoS 面 | ⚠️ 沒有。前門不限速率(C01 D7)、入口本身也沒計次;每次比對要查一次 DB(get_or_none(tenant_id)) |
最便宜的一步:D7——webhook 入口加一個以 tenant_id 為鍵的 Redis 計次(redis_rate_limiter.py 已有現成的),擋住「不需帳號對 DB 打查詢」這條。D6 是最有價值的一步,但要客戶功能面拍板。
drive.file 的功能差異與客戶接受度,掃描沒評估。這是把 T1、T3、T4 一次縮小的槓桿。project_tree_loader 有帶公司比對,process_drive_changes_handler、archive_drive_file_handler 等其他 handler 有沒有同款縱深,沒逐一盤。DRIVE_TOKEN_ENCRYPTION_KEY 是環境變數,與 AI_PROVIDER_ENCRYPTION_KEY 同在 .env——誰能讀主機 .env 就能解所有客戶的 Google 權杖。依據:STRIDE 六頁信任邊界連線標記(CM-2403/2404 驗收後版本)、app/cloud_integration/service/(google_drive_integration_service.py、google_drive_webhook_service.py、drive_sync_admin_service.py、project_tree_loader.py、handlers/)、infra/cloud_integration/(google_drive_api_client.py、crypto/fernet_crypto.py)、api/cloud_integration/routes/、common/constant/drive_app_config.py、DFD Level 0。M07-1/M07-2/M07-4 隨舊線拆除仍列為實例。