---
title: C14 api ⇄ Google Drive
eyebrow: Guidant AI 資安檢視總報告 · 連線清單
h1: C14 api ⇄ Google 雲端硬碟（證據同步，依設定）
lede: 客戶把稽核證據放在 Google 雲端硬碟、由系統自動同步進來走的那條線。它是**雙向**的：系統用客戶授權的 Google 帳號去雲端建資料夾、抓檔案，Google 也會把「檔案變了」的通知推回系統。這一頁回答：這條線**怎麼連**、**哪些功能走它**、它帶來**什麼威脅、駭客怎麼打**、我們**要怎麼防、目前做到哪**。
chips:
  - { text: "依設定", kind: plain }
  - { text: "雙向・用客戶的 Google 身分", kind: warn }
  - { text: "威脅 5 種", kind: accent }
  - { text: "掃描命中 10 條", kind: plain }
---

> **這一頁怎麼來的**：[DFD Level 0](../DFD/dfd-level0.html#c14) 把產品運作時的連線編成 C01～C25；[STRIDE 六頁](../STRIDE/S-spoofing.html)每一條問題都標了發生在哪幾條線。這裡以**連線**為單位整理。10 條命中裡有 3 條（M07-1、M07-2、M07-4）屬於已拆除的舊版證據分類線——那九支網址已不存在，本頁仍列為實例，因為它們示範的手法（拿到編號就用公司的鑰匙去 Google 抓）在這條線上永遠成立。個別問題修了沒不在這頁講。

## 一、連線圖

```{.mermaid cap="C14 — api 與 Google 雲端硬碟雙向。往外：api 用客戶授權的 OAuth 權杖建資料夾、抓檔案；往內：Google 推「檔案變了」通知到不需登入的 webhook 入口，api 比對通行碼後排一個背景同步工作。紅色是三個被打過的點：webhook 入口對網路開放、系統申請的是「整個雲端硬碟」的最寬權限、建出來的資料夾設成「知道連結的任何人都能編輯」。"}
%%{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](../DFD/dfd-level0.html#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 資料外洩](../STRIDE/I-information-disclosure.html) | [M07-1 舊線「預覽證據檔」填任何雲端硬碟檔案編號就把檔案抓回來](../STRIDE/I-information-disclosure.html#m07-1) 🟠<br>[M07-2 舊線查結果、查報表不檢查專案成員，查不到還退回直接讀雲端硬碟](../STRIDE/I-information-disclosure.html#m07-2) 🟡 |
| T2 | 🟠 高 | **背景工作以系統身分跑，跨公司動別人的東西** | ① A 公司任一帳號登入；② 拿 B 公司的專案編號送「重建專案資料夾」；③ 入口只驗登入就排進背景；④ worker 以系統身分載入專案樹、不看公司，把 B 的整個專案結構建進 A 的雲端硬碟；⑤ A 往那些資料夾丟檔，下一輪同步寫成 B 公司任務的證據。另一條路：B 也接了同一個 Google 帳號，A 在雲端刪一個檔，同步按 Google 編號查到的是 B 的任務 | 別家客戶的專案結構；把假證據摻進別家的稽核；改動或刪除別家的證據對應 | [I 資料外洩](../STRIDE/I-information-disclosure.html)、[T 竄改資料](../STRIDE/T-tampering.html) | [M24-2 重建專案雲端資料夾不檢查專案是不是你的，背景還用系統身分載入](../STRIDE/I-information-disclosure.html#m24-2) 🟠<br>[M24-4 雲端硬碟背景同步查資料時不分公司](../STRIDE/T-tampering.html#m24-4) 🟡 |
| T3 | 🟡 中 | **資料夾對全世界開放編輯** | ① 公司裡任一登入帳號取得根資料夾連結（畫面上有、或同事轉傳）；② 用瀏覽器打開，**不需要任何專案權限、不需要本系統帳號**；③ 看、改、刪全公司的證據檔；④ 被改過的檔案下一輪同步時被匯入成正式證據 | 全公司雲端硬碟上的稽核證據（可讀可改可刪） | [I 資料外洩](../STRIDE/I-information-disclosure.html)、[T 竄改資料](../STRIDE/T-tampering.html) | [M24-8 所有雲端硬碟資料夾設成「知道連結的任何人都能編輯」](../STRIDE/I-information-disclosure.html#m24-8) 🟡 |
| T4 | 🟡 中 | **使用者的字拼進 Google 的搜尋語法** | ① 能編輯專案或稽核計畫名稱的人，把名稱改成結尾帶反斜線再接引號與額外條件；② 管理員初始化或重建該專案的雲端資料夾；③ 系統依名稱找同名資料夾的搜尋條件 `name='…'` 被改寫，找到並「沿用」一個不該沿用的資料夾。舊線另有資料夾編號直接拼進 `'<編號>' in parents` | 系統把證據寫進或讀自不該碰的雲端資料夾；或撈到那個帳號摸得到的檔案清單 | [T 竄改資料](../STRIDE/T-tampering.html)、[I 資料外洩](../STRIDE/I-information-disclosure.html) | [M07-4 舊版證據分類的雲端硬碟搜尋條件用字串拼接](../STRIDE/I-information-disclosure.html#m07-4) 🟡<br>[M24-14 依名稱找雲端資料夾時沒處理反斜線](../STRIDE/I-information-disclosure.html#m24-14) ⚪ |
| T5 | ⚪ 低 | **從不需登入的通知入口與驗憑證按鈕下手** | (a) ① 不需帳號，對 webhook 入口大量送請求，每次換通行碼開頭；② 量回應時間差一個字一個字猜；③ 猜中就能偽造「硬碟有異動」通知讓系統一直做同步。(b) ① 平台管理員群組裡沒分到儲存設定權限的人；② 直接送驗憑證請求帶任意一組 Google 憑證；③ 系統替他向 Google 試對錯。(c) Google 短時間推多則通知，每則都排一個工作 | 偽造通知讓系統白做同步；用系統替你試憑證；多跑幾次同步 | [S 冒充身分](../STRIDE/S-spoofing.html)、[E 權限提升](../STRIDE/E-elevation-of-privilege.html)、[D 讓服務停擺](../STRIDE/D-denial-of-service.html) | [M24-11 雲端硬碟通知的通行碼比對可用時間差慢慢猜](../STRIDE/S-spoofing.html#m24-11) ⚪<br>[M24-12 驗證雲端硬碟應用程式憑證只查是不是平台管理員](../STRIDE/E-elevation-of-privilege.html#m24-12) ⚪<br>[M24-15 雲端硬碟通知說有去重複、實際沒做](../STRIDE/D-denial-of-service.html#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](C01-browser-to-frontdoor.html) D7）、入口本身也沒計次；每次比對要查一次 DB（`get_or_none(tenant_id)`） |

**最便宜的一步**：D7——webhook 入口加一個以 `tenant_id` 為鍵的 Redis 計次（`redis_rate_limiter.py` 已有現成的），擋住「不需帳號對 DB 打查詢」這條。D6 是最有價值的一步，但要客戶功能面拍板。

## 六、依這些防線，掃描還沒看過的地方

- **OAuth scope 的半徑**（D6）：全權限 vs `drive.file` 的功能差異與客戶接受度，掃描沒評估。這是把 T1、T3、T4 一次縮小的槓桿。
- **同步處理器的租戶條件**（D2）：M24-4 只修根因（一帳號一家），六處查詢仍不帶租戶條件；若日後放寬「一帳號多家」，整組回來。
- **worker 以系統身分跑的其他 handler**：`project_tree_loader` 有帶公司比對，`process_drive_changes_handler`、`archive_drive_file_handler` 等其他 handler 有沒有同款縱深，沒逐一盤。
- **權杖加密鑰的保管**：`DRIVE_TOKEN_ENCRYPTION_KEY` 是環境變數，與 `AI_PROVIDER_ENCRYPTION_KEY` 同在 `.env`——誰能讀主機 `.env` 就能解所有客戶的 Google 權杖。
- **輪詢退路的頻率與範圍**：webhook 登記失敗時退回定時輪詢，輪詢多久一次、一次拉多少、對 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 隨舊線拆除仍列為實例。*
