---
title: U4 檢查結果：雲端硬碟整合——設定、授權、金鑰
---

# U4 檢查結果：雲端硬碟整合——設定、授權、金鑰

> 檢查日期 2026-09-25｜對應卡片 CM-2159（母卡 CM-2152）｜範圍 13 個檔案、1,786 行｜工具 run ID `wf_a828ae6e-597`｜驗證章 `verified`

## 🔴 一句話結論

**這一棒守門檢查很多，大多也問對了問題；真正的新洞在一個沒人想到要守的地方：Google 授權完成後跳回來的那一頁，可以被塞進一段程式，點一下連結就可能被盜帳號。**

- **🔴 新發現（高風險，三人面板 3:0）**：Google 授權完成後會跳回一個小頁面（「回呼頁」）。這個頁面**不用登入**，會把網址上一個叫 `error` 的欄位原樣寫進頁面裡的程式碼。今年 6 月曾修過一次（改用 `json.dumps` 包起來），但**那次修法擋不住 `</script>` 這種寫法**。我親手送了一段惡意網址，回應原封不動帶回攻擊程式碼。落地版的前端和 API 放在同一個網址底下，登入憑證又存在瀏覽器裡，所以受害者只要在登入狀態下點一條連結，**登入憑證就會被偷走**；受害者如果是管理員，攻擊者就拿到管理員權限。
- **工具另外報的 3 條都是已知問題**：跨公司「重建專案資料夾」＝U5-1／U6-1；根資料夾編號發給任何登入者＝U5-2／U6-2 的後半段；Google 應用程式密鑰寫在對話紀錄裡＝CM-1631。另有 1 條「權杖加密金鑰也寫在對話紀錄裡」被面板以 0:3 否決，否決理由是光有金鑰、拿不到資料庫裡的密文就沒用，這條也在 CM-1631 裡。
- **我人工另補 3 條低度問題**（沒經過三人面板）：加密金鑰換掉後系統不會報錯，只會一直顯示「已連線」（非資安，屬於出事查不出來）；授權連結可以轉給別人代按；「驗證憑證」按鈕的守門比它的註解寫的少一道。
- **卡片點名的疑點大多不成立**：權杖更新不會寫錯公司；Google 應用程式設定只有總部（平台）管理員改得動，一般使用者寫不進去；手動重試同步工作跨不了公司，資料庫層會擋；新方法 `get_project_folders` 沒有問題。

**帶著前棒的形狀來看**：U5／U6 的洞在「背景工作用可看全部公司的身分跑」。這一棒的 13 支是那條線的入口，**問對問題的比例很高**：寫入類都問「你是不是這家公司的管理員」，平台設定問「你是不是總部管理員」。出事的是兩個「刻意不掛權限」的入口：一是**回呼頁**（本來就不能掛，但漏了跳脫）；二是**狀態查詢**（為了讓一般成員看得到「有沒有連線」，卻連根資料夾編號一起回傳）。第三個是 U5 已報過的「重建資料夾」。

## 這一棒在檢查什麼

客戶可以把系統接上自己公司的 Google 雲端硬碟。接的時候，管理員按「連接」，系統把人帶到 Google 的同意畫面；同意後 Google 把人帶回我們的「回呼頁」，系統拿到一組**授權權杖**（之後代表這家公司存取雲端硬碟的憑證），加密後存進資料庫。

這一棒的 13 支程式負責這條線的「設定、授權、金鑰」：

- **網址入口**（4 支）：連線狀態、產生授權網址、回呼頁、驗證憑證、同步工作管理、重建資料夾等端點。
- **授權流程**（3 支）：產生授權網址、處理回呼、斷開連線、手動維運操作、驗證憑證。
- **Google 應用程式設定**（1 支）：我們在 Google 登記的應用程式帳號（`client_id`＋密鑰）從哪讀。
- **權杖與金鑰**（5 支）：權杖過期時換新、用哪把金鑰加解密、存取「哪家公司接了哪個 Google 帳號」這張表。

核心問題有兩個：**每道守門問的問題對不對？加密金鑰從哪來、會不會出錯？**

| 檔案 | 行數 | 角色 |
|------|-----:|------|
| `app/cloud_integration/service/google_drive_integration_service.py` | 288 | 授權流程主體：產生授權網址、處理回呼、斷開 |
| `api/cloud_integration/routes/google_drive_sync_route.py` | 260 | 同步維運入口：工作清單、重試、重建資料夾 |
| `app/system_config/service/google_drive_app_config_resolver.py` | 224 | Google 應用程式帳號從哪讀、密鑰解密 |
| `infra/cloud_integration/google_drive/google_oauth_client.py` | 180 | 打 Google 的授權端點（換票、更新、撤銷） |
| `app/cloud_integration/service/drive_sync_admin_service.py` | 164 | 同步維運操作本體 |
| `api/cloud_integration/routes/google_drive_integration_route.py` | 140 | 連線狀態、授權網址、**回呼頁** |
| `domain/cloud_integration/service/google_drive_token_manager.py` | 131 | 權杖過期時換新、被撤銷時標記 |
| `app/cloud_integration/service/drive_app_credential_verify_service.py` | 94 | 「驗證憑證」按鈕的後端 |
| `infra/cloud_integration/repository/tenant_drive_integration_repo_impl.py` | 94 | 「哪家公司接了哪個 Google 帳號」這張表的讀寫 |
| `api/cloud_integration/__init__.py` | 84 | 網址註冊表 |
| `domain/cloud_integration/service/tenant_drive_integration_domain_service.py` | 65 | 連線紀錄的建立、覆寫、斷開 |
| `api/cloud_integration/routes/drive_app_credential_verify_route.py` | 44 | 「驗證憑證」網址入口 |
| `infra/cloud_integration/crypto/fernet_crypto.py` | 18 | 加解密（Fernet，一種對稱加密法） |

## 掃到什麼（總覽）

| # | 這是什麼問題 | 出事會怎樣 | 要先有什麼才打得到 | 該補檢查的位置 | 嚴重度＋為什麼 | 來源 |
|---|---|---|---|---|---|---|
| U4-1 | 回呼頁把網址上的 `error` 原樣寫進頁面程式碼，6 月那次修法擋不住 `</script>` | 受害者點一條連結，攻擊程式碼就在我們的網站上執行，偷走登入憑證、冒用身分；管理員中招＝管理員權限外流 | 受害者在同一個瀏覽器裡**已經登入**，並且點了攻擊者給的連結。攻擊者不需要任何帳號 | `api/cloud_integration/routes/google_drive_integration_route.py:120`（`error` 改成只收固定幾種錯誤代碼，不原樣寫回；至少把 `<` `>` `&` 轉義）；`:110`（例外訊息 `str(e)` 別寫回頁面）；回應加安全標頭（CSP） | 🔴 高：外人不用帳號就能發動，只需要受害者點一下 | **工具 F1，面板 3:0**；runner 實測重現 |
| U4-2 | 「重建專案資料夾」收任何專案編號，不查是誰家的 | 見 U5-1／U6-1 | 見 U5-1 | `app/cloud_integration/service/drive_sync_admin_service.py:125` | 🟠 同 U6-1（U6 評高） | 工具 F3，面板 3:0；**U5-1／U6-1 重現，不另計** |
| U4-3 | 不掛權限的「連線狀態」查詢，回傳內容含根資料夾編號 | 根資料夾設成「知道連結就能編輯」，公司內任何登入者都能看、改、刪全公司的證據 | 同公司的任一登入帳號 | `api/cloud_integration/routes/google_drive_integration_route.py:49`（狀態回應拿掉 `root_folder_id` 和 `google_account_email`） | 🟠 中 | 工具 F4，面板 3:0；**U5-2／U6-2 重現，不另計** |
| U4-4 | Google 應用程式密鑰明文寫在對話紀錄檔 | 能讀程式庫的人可以冒用本產品的 Google 應用程式身分 | 能讀程式庫 | — | 🟠 中（面板從高降為中） | 工具 F2，面板 3:0；**CM-1631 重現，不另計** |
| U4-5 | 加密金鑰換掉或弄丟後，權杖解不開，系統不報錯、仍顯示「已連線」 | 該公司所有同步工作重試 5 次後失敗，錯誤訊息是**空白**；畫面照樣顯示「已連線」，一線人員無從判斷原因 | 主機的 `DRIVE_TOKEN_ENCRYPTION_KEY` 被改、遺失，或還原了別台的資料庫 | `domain/cloud_integration/service/google_drive_token_manager.py:60`、`:66`（接住解密失敗，標記成「需重新授權」並寫一句白話原因） | 🟢 低（**非資安**，屬於出事查不出來） | runner 開檔＋實測（未經三人面板） |
| U4-6 | 授權網址沒有綁定「按下連接的那個瀏覽器」 | 攻擊者把自己公司的授權網址轉給別人，對方在 Google 同意畫面按下同意，**對方的雲端硬碟就接進攻擊者的公司** | 攻擊者是某家公司有「建立連線」權限的管理員；受害者願意在 Google 同意畫面上按同意 | `app/cloud_integration/service/google_drive_integration_service.py:90-92`（產生時把 state 也寫進發起者的瀏覽器）；`:107-110`（回呼時核對，並改成「讀取與刪除一次完成」） | 🟢 低：受害者要自己在 Google 畫面上點同意；**未實測**（要真的 Google 帳號） | runner 開檔（未經三人面板） |
| U4-7 | 「驗證憑證」只檢查是不是總部的人，比同一組設定的讀寫端點少一道能力點檢查，跟它自己的註解不符 | 總部任何一個登入帳號（不必是管理員）都能按：看得到目前設定能不能用、回呼網址是什麼，也能拿伺服器去試任意一組 Google 憑證 | 總部公司的任一登入帳號 | `app/cloud_integration/service/drive_app_credential_verify_service.py:71`（比照讀寫端點先檢查 `storage-config` 能力點） | 🟢 低：拿不到密鑰本身，只拿得到「對或錯」 | runner 開檔（未經三人面板） |

> **現況（2026-10-01）**：本棒各條後來的處理結果如下（過程紀錄保留，不改）。
>
> - U4-1（回呼頁注入）＝總表第 188 項，✅ 已修（CM-2200，commit `f0a740bd7`，1.21.0 出貨）。
> - U4-2（重建專案資料夾）＝U5-1／U6-1＝總表第 192 項，✅ 已修（CM-2201，commit `73f5f90af`，1.21.0 出貨）。
> - U4-3（狀態查詢回根資料夾編號）：併 M24 第 8 條，🚫 裁定不修（決策者 10-01，雲端硬碟分享權限不由產品管）。
> - U4-4（Google 密鑰寫在對話紀錄）：原卡 CM-1631 作廢，✅ 已修（CM-2051，commit `ccbb27148`，總表第 74 項）。
> - U4-6（授權連結綁瀏覽器）＝總表第 190 項，✅ 已修（CM-2218，commit `a671d0b64`，1.21.0 出貨）。
> - U4-7（驗證憑證少一道能力點）＝總表第 191 項，✅ 已修（CM-2211，commit `fe928859f`，1.21.0 出貨）。
> - U4-5（金鑰換掉仍顯示已連線，非資安）：SUMMARY 無對應條目，查不到現況。

淨新增：**高 1（U4-1）＋低 3（U4-5／U4-6／U4-7，其中 U4-5 為非資安）**。

## 工具報的逐條

工具交回 5 條候選，三人面板 15 票全投完、零漏投。通過 4 條、否決 1 條，研究員 2 派 2 回、零失敗。

### F1 → U4-1 🔴 回呼頁可以被塞進程式碼（高，3:0）

**在哪**：`api/cloud_integration/routes/google_drive_integration_route.py:115-140`（`_callback_html`），入口 `:86-112`。

**白話說明**：使用者在 Google 同意授權後，Google 會把人帶回我們的 `/api/1.0/integrations/google-drive/callback`。這個頁面**本來就不能要求登入**（進來的是 Google 轉址，不是我們的前端），網址上的 `error` 欄位誰都能填。程式把它丟進 `json.dumps` 處理後，直接插進頁面裡一段 `<script>` 程式碼。

`json.dumps` 會把雙引號、反斜線處理好，但**不處理 `<`、`>`、`/`**。瀏覽器讀網頁時，只要看到 `</script>` 就會把那段程式碼結束掉，不管它在不在引號裡。所以攻擊者在 `error` 塞進 `</script><script>任意程式</script>`，後半段就會變成一段新的、會被執行的程式碼。

**runner 實測**（不打真伺服器，用 Flask 測試用戶端掛上**真正的 route 類別**）：送出 `error=</script><script>alert(document.domain)</script>`，回應 200、`text/html`、**沒有任何安全標頭**，回應原文包含 `error: "</script><script>alert(document.domain)</script>"`。再用 Python 內建的 HTML 解析器解析這段回應，確認**被切成兩個 script 元素**，其中一個的內容正是 `alert(document.domain)`。

**為什麼偷得到登入憑證**：
- 落地版的 nginx 把前端和 `/api/1.0` 放在**同一個網址底下**（前端 repo `nginx.onprem.conf:88-91`），所以這段程式在「我們的網站」上執行。
- 前端的登入憑證存在瀏覽器的 localStorage（前端 `src/utils/authPersist.js:4`），同網址下的程式讀得到。

**沿革**：2026-06-05 commit `99055ab00` 的 C12 修過這支。當時是從 `f'"{error}"'` 改成 `json.dumps`，commit 訊息也寫了「專案無 CSP 兜底」。程式碼註解 `:118-119` 至今仍寫著「用 json.dumps 跳脫所有注入…防 Reflected XSS」，**這句話不成立**。我查過跨 arc 總表，沒有這一條。

**觸發前提**：受害者在同一個瀏覽器裡已登入，並點了攻擊者的連結。**攻擊者不需要任何帳號**，不需要這家公司接了雲端硬碟，也不需要知道任何編號。開發環境前端和 API 不同埠號，偷不到憑證，但頁面一樣會執行程式。

**建議修法**：
1. `error` 只接受固定幾種錯誤代碼（例如 `access_denied`、`missing_params`），其他一律換成通用代碼，不要把使用者給的字串寫回頁面。`:110` 的 `str(e)` 也一樣。
2. 最少的修法：在 `json.dumps` 之後把 `<`、`>`、`&` 換成 `<`、`>`、`&`。
3. 這支回應加上 `Content-Security-Policy`，只允許頁面內這一段程式。

**嚴重度**：高（面板三票都評高）。唯一的門檻是「受害者要點連結」。

### F3 → U4-2（＝U5-1／U6-1，不另計）

工具從 U4 這一側再撈到一次「重建專案資料夾」的跨公司問題。從入口看，病灶有兩處：
- `api/cloud_integration/routes/google_drive_sync_route.py:108-110` 只掛了「登入」與「公司有沒有買雲端整合」，**問的是「你有沒有買」，不是「這專案是不是你的」**。
- `app/cloud_integration/service/drive_sync_admin_service.py:120-123` 的註解寫「FE already gates the button to project owner / manager; no extra BE check」，也就是「前端已經把按鈕藏起來了，後端不再檢查」。這正是卡片「有人守了一半」列的那一型：註解說這裡刻意不查，但上一層根本沒人真的查（前端藏按鈕不算檢查）。

同一支 route 檔的隔壁 `verify-and-repair-folders`（`:126-156`）**有**在 service 層檢查專案管理者（`drive_project_verify_service.py:158-`）。所以修法有現成的樣板可以照抄。

### F4 → U4-3（＝U5-2／U6-2 後半段，不另計）

`api/cloud_integration/routes/google_drive_integration_route.py:32-49` 的註解說明這支 GET 刻意不掛能力點，理由是「專案頁要用它判斷要不要顯示雲端區塊」，這個理由成立。問題是回應整份 DTO（資料傳輸物件，就是回給前端的那包資料）都帶出去了，裡面有 `root_folder_id` 和 `google_account_email`（`app/cloud_integration/dto/google_drive_integration_dto.py:16`、`:47`）。前端判斷要不要顯示只需要 `status`。

**同一個模組裡守門不一致**：新方法 `get_project_folders` 只回**某一個專案**的資料夾編號，卻要求 `cloud_integration.read` 能力點（`google_drive_sync_route.py:175`）。範圍更大的根資料夾編號反而不用任何能力點。

### F2 → U4-4（＝CM-1631，不另計）

Google 應用程式密鑰（`GOCSPX-…`）明文出現在 `docs/conversation-history/2026-04-21-google-drive-integration/` 的多個對話紀錄檔裡。面板把嚴重度從高降為中（三張確認票都評中）。U5 已記過，屬於 CM-1631，不另計。

### 被否決的 1 條：權杖加密金鑰也寫在對話紀錄裡（0:3）

三位檢查員都確認 `DRIVE_TOKEN_ENCRYPTION_KEY` 確實出現在同一批對話紀錄裡，而且和本機開發環境的 `.env` 是同一把。否決理由是**只有金鑰沒用**，還要另外拿到資料庫裡的密文，而程式庫裡找不到任何一段真的密文。這一條在 CM-1631 的範圍內，否決不代表可以不處理。

## 卡片點名的疑點逐條回答

以下都是 runner 自行開檔核對，**沒有經過三人面板投票**。DEV 查詢時間 2026-09-25 13:49（隔離規則）與 14:17（筆數）。

### ① 權杖加密金鑰從哪來？解密失敗怎麼辦？換新時會不會寫錯公司？

- **金鑰從哪來**：主機環境變數 `DRIVE_TOKEN_ENCRYPTION_KEY`（`config/config.py:165`），經 DI 容器（`di_containers/cloud_integration/cloud_integration_containers.py:109`）交給 `FernetCrypto`。**每套安裝各自隨機產生一把**（`scripts/installer/install.sh:1260`），不是全產品共用。
- **換公司會不會共用一把**：**同一套安裝裡的所有公司共用同一把**，平台的 Google 應用程式密鑰也用這把（`google_drive_app_config_resolver.py:26`）。這是設計選擇：公司之間的權杖靠資料庫的隔離規則分開，不是靠金鑰分開。能讀到資料庫全部資料又拿到這把金鑰的人，可以解開所有公司的權杖。**不列為發現**，但要記住一件事：外洩一把金鑰，受影響的是全部公司，不是單一公司。
- **沒有換金鑰的機制**：`fernet_crypto.py` 只用單一把 `Fernet`，沒有用可以同時認新舊金鑰的 `MultiFernet`。換金鑰＝所有公司的授權一次失效。
- **解密失敗** → **成立，列 U4-5（非資安）**。我寫了一段小程式模擬「舊金鑰加密、新金鑰解密」，打 `GoogleDriveTokenManager.get_valid_access_token`，結果：
  - 拋出的是 `cryptography` 的 `InvalidToken`，**錯誤訊息是空字串**；
  - 權杖管理器只接「Google 說權杖失效」（`InvalidGrantError`，`:69`），**不接解密失敗**，所以連線狀態**維持 CONNECTED**，也不會被標成需要重新授權；
  - 背景工人把它當成暫時性錯誤（`drive_sync_worker.py:132-137`），重試 5 次後標失敗，存進去的錯誤訊息是 `str(e)`，也就是**空白**。

  結果：畫面顯示「已連線」、同步全部靜默失敗、失敗原因欄是空的。
- **換新權杖時會不會寫錯公司** → **不成立**。`get_valid_access_token(tenant_id)` 用公司編號查出那一列，就地更新同一列（`:49`、`:89-93`）；被撤銷時另開交易，也是用同一個公司編號重查（`:115`）。資料庫對 `tenant_id` 有唯一索引（DEV `tenant_drive_integrations_tenant_id_key`），一家公司只會有一列，不存在挑錯列的可能。

### ② 兩支 route 檔的守門，問的問題對不對？

逐個端點看「掛了什麼、問的是什麼、該問什麼」：

| 端點 | 掛了什麼 | 問的問題 | 對不對 |
|---|---|---|---|
| `GET /integrations/google-drive` 連線狀態 | 登入＋有買 | 你是這家公司的人嗎 | ⚠️ 問題對，但**回太多**（U4-3） |
| `DELETE` 斷開連線 | 登入＋有買＋`cloud_integration.delete` | 你是這家公司有權限的管理員嗎 | ✅ |
| `POST /auth-url` 產生授權網址 | 登入＋有買＋`cloud_integration.create` | 同上 | ✅（但見 U4-6） |
| `GET /callback` 回呼頁 | **無**（刻意，Google 轉址進來） | 靠一次性 state 認身分 | ✅ 身分部分對；❌ 頁面輸出沒跳脫（U4-1） |
| `POST /verify-credentials` | 登入＋service 層總部檢查 | 你是總部的人嗎 | ⚠️ 少問「你有沒有權限」（U4-7） |
| `GET /sync-jobs` 工作清單 | 登入＋有買＋`.read` | 你是這家公司有權限的管理員嗎；只查自家（`:55`） | ✅ |
| `POST /sync-jobs/<uid>/retry` | 登入＋有買＋`.update` | 同上 | ✅ 程式沒限公司，但**資料庫隔離規則擋得住**（見 ④） |
| `POST /projects/<uid>/init-folders` | 登入＋有買 | **只問「你有沒有買」** | ❌ 該問「這專案是不是你的、你是不是它的管理者」（U4-2＝U5-1） |
| `POST /projects/<uid>/verify-and-repair-folders` | 登入＋有買＋service 層專案管理者 | 你是這個專案的管理者嗎 | ✅ |
| `GET /projects/<uid>/folders` | 登入＋有買＋`.read` | 你是這家公司有權限的管理員嗎；只查自家 | ✅（見 ⑤） |
| `POST /tenant/rebuild-all` | 登入＋有買＋`.update` | 同上；只動自家 | ✅ |
| `POST /webhook/register` | 登入＋有買＋`.update` | 同上；只動自家 | ✅ |

`cloud_integration.*` 四顆能力點都不是平台級（`is_platform = FALSE`，`scripts/init/04-seed-core.sql:184-187`），只配給各家公司的管理員角色。DEV 實查 2026-09-25 13:49 看到的是各公司的「系統管理員」「Admin」類角色。對「只動自家公司資料」的操作來說，這個問題問得對。

### ③ Google 應用程式設定從哪讀？一般使用者能不能寫進去、把整家公司導到攻擊者的 Google 應用程式？

**不成立。**
- **沒有「公司層」這一層**。卡片推測的「公司→原廠→環境變數」三層並不存在，實際只有兩層：**總部（ROOT）那一列**（`google_drive_app_config_resolver.py:103-108`，固定讀 `SYSTEM_ROOT_TENANT_ID`）和**主機環境變數**（`:111-125`）。各家公司自己存的同名設定**不會被讀到**。
- **寫入**走泛用系統設定端點，由 `guarded_system_config_service.py:259-275` 在能力點之後**再加一道總部管理員檢查**（旗標 `DRIVE_APP_CONFIG_PLATFORM_ADMIN_ONLY = True`）。一般使用者和各公司管理員都寫不進去。
- 附帶一提（不列為發現）：旗標註解說「未來改 False 就開放客戶自填」，可是解析器只讀總部那一列，**就算把旗標改成 False，客戶自己填的也不會生效**。這是功能上的落差，不是資安問題，以後要開放時要一起改。

### ④ 手動觸發同步：能不能觸發別家公司的？

- **清單、全部重建、補登通知**：一律用登入者自己的公司編號（`google_drive_sync_route.py:55`、`:216`、`:252-257`）。**不成立**。
- **重試單一工作**（`:97` → `drive_sync_job_repo_impl.py:175-191`）：程式只用工作編號查，**沒帶公司條件**。但 `drive_sync_jobs` 開了資料庫隔離，DEV 實查 2026-09-25 13:49：UPDATE 規則是「總部身分，或這筆資料的公司在你的可見範圍內」；API 連線用的 `cm_app` 帳號**不能繞過**隔離（`rolbypassrls = f`）。別家公司的工作編號更新到 0 筆，回 `retried: false`。**不成立**。唯一例外是總部的人本來就看得到全部，這是平台層設計。
- **重建專案資料夾** → **成立**，即 U5-1／U6-1（見上方 F3）。

### ⑤ 新方法 `get_project_folders`（掃描後才新增的）

**不成立，沒有問題。** 2026-09-18 commit `a0ae4f378`（CM-1940）新增的這支：
- 有 `@transaction`（`drive_project_verify_service.py:115`），查詢在正確的交易範圍內；
- 用**登入者的公司編號**加上專案編號查（`drive_folder_mapping_repo_impl.py:53-65`，三個條件都有），資料庫隔離又再擋一層。拿別家公司的專案編號來查，只會回兩個 `null`，不會回別家的資料夾編號；
- 前面掛了 `cloud_integration.read`。

唯一值得一提的是 U4-3 講的「守門不一致」：這支範圍小卻要能力點，範圍大的根資料夾編號反而不用。

### ⑥「有人守了一半」逐型對照

| 常見型 | 這一棒有沒有 |
|---|---|
| 只驗「你是誰」沒驗「這是不是你的」 | **有**：init-folders（U4-2） |
| 列表有查、單筆沒查 | 重試單一工作程式沒查，但資料庫隔離擋住，**實際不成立** |
| 守門寫在 route 層、第二支路由沒掛 | 沒有：`__init__.py` 註冊的端點都逐一核過 |
| `or` 串起來其中一個永遠成立 | 沒有 |
| 「查不到」和「沒權限」混成同一個回應 | 回呼頁「公司已被刪」與「state 失效」回同一個錯（`google_drive_integration_service.py:121-124`），這是**刻意的**，避免外人探出內部狀態，屬於正確做法 |
| 背景／系統身分假設「呼叫者一定是自己人」 | 本棒範圍內只有回呼頁用機器身分，且公司編號取自自己寫下的一次性 state，**不成立**（背景工人那段屬 U5／U6） |
| 防重放只在單一進程內有效 | state 存在 Redis，多台機器共用，**不成立**；但「讀取」和「刪除」分兩步做（`:107-110`），同一個 state 同時送兩次理論上兩次都會通過。要先拿到 state 才打得到，併入 U4-6 一起修 |
| 註解說「刻意不檢查」但上一層沒人真的檢查 | **有**：init-folders 的「FE 已擋」（U4-2）；狀態查詢「讀有沒有連線不是敏感資訊」，但實際回的不只這些（U4-3） |

## 人工補的三條（未經三人面板）

### U4-5 🟢 加密金鑰換掉後，系統不報錯，一直顯示「已連線」（低，非資安）

- **在哪**：`domain/cloud_integration/service/google_drive_token_manager.py:60`、`:66` 呼叫解密，只接 `InvalidGrantError`（`:69`）。
- **怎麼驗的**：runner 寫了一段模擬程式，用兩把不同的金鑰分別加密和解密，打權杖管理器，結果見上方 ①。
- **影響**：主機換過金鑰、弄丟金鑰、或把別台的資料庫還原過來時，這家公司的同步全部停擺，畫面卻顯示「已連線」，失敗原因欄是空的。一線人員看不出是金鑰問題。
- **建議修法**：在解密的兩處接住 `InvalidToken`，比照 `_mark_revoked` 把連線改成需要重新授權，並寫一句白話原因（例如「加密金鑰與資料不符，請重新連接」）；背景工人把它當成終止錯誤，不要重試。
- **嚴重度**：低。這不是資安問題（攻擊者得不到任何東西），是「出事時查不出原因」。`google_drive_app_config_resolver.py:30-34` 的註解已經刻意要求「解不開就拋」，這個方向是對的，問題只在拋出來之後沒人接。

### U4-6 🟢 授權網址沒有綁定按下「連接」的那個瀏覽器（低）

- **在哪**：`app/cloud_integration/service/google_drive_integration_service.py:90-94` 產生授權網址時，把「公司編號：使用者編號」存進 Redis，以一次性的 state 當鑰匙；回呼時（`:107-116`）**只看 state**，不核對「完成授權的人是不是當初按連接的那個瀏覽器」。
- **怎麼被利用**：某家公司有「建立連線」權限的管理員取得授權網址，轉寄給別人（例如別家公司的人），說「幫忙按一下」。對方在 Google 同意畫面用**自己的** Google 帳號按下同意，對方的雲端硬碟就被接進攻擊者的公司：之後系統會在對方的雲端硬碟裡建資料夾、讀取變動。
- **門檻**：受害者要在 Google 的同意畫面上，對一個要求「完整雲端硬碟存取」的應用程式按同意。畫面上寫得很清楚，所以列為低。state 10 分鐘就失效。
- **建議修法**：產生授權網址時，把 state 也寫進發起者瀏覽器的 cookie，回呼時比對兩邊一致（OAuth 標準 RFC 6749 §10.12 的建議做法）。讀取和刪除改成一次完成（Redis `GETDEL`），順便把「同一個 state 同時送兩次」這個小縫補起來。
- **未實測**：要真的 Google 帳號走完同意流程，本機做不到。

### U4-7 🟢「驗證憑證」的守門比註解寫的少一道（低）

- **在哪**：`app/cloud_integration/service/drive_app_credential_verify_service.py:71` 只呼叫 `require_platform_admin()`；route（`drive_app_credential_verify_route.py:30`）只掛登入。
- **落差**：註解（`:68-69`）說這支「與這組設定的讀寫端點同一道門」。但讀寫端點是**先**檢查 `storage-config` 能力點，**再**檢查總部管理員（`guarded_system_config_service.py:259-275`）。這支只做了後半段。「總部管理員」的判定只看「是不是總部這家公司的成員」（`jedi_iam/authz/platform.py:15-22`，路徑只有一層就算），**不看角色**。
- **影響**：總部任何一個登入帳號（不必是管理員）都能：① 不帶參數按下去，知道目前存的憑證能不能用，並拿到回呼網址；② 拿伺服器去試任意一組 Google 憑證（目標網址寫死是 Google，不會變成可以任意打內網的跳板）。**拿不到密鑰本身**。
- **建議修法**：比照讀寫端點，先檢查 `storage-config.update` 再檢查總部。
- **嚴重度**：低。

## 可信度分兩層

1. **工具正式清單（經三人面板投票）**：4 條通過，全部 3:0；1 條以 0:3 否決。驗證章 `verified`，15 票零漏投，研究員 2 派 2 回。U4-1 我另外親手實測重現了。
2. **runner 自己追的（沒經過面板）**：U4-5 有實測；U4-6、U4-7 是開檔推論，U4-6 沒有實測。卡片點名的 6 個疑點都逐支開檔回答了，13 支範圍檔也都逐支通讀過。

**打折處**：
- U4-1 的實測用的是 Flask 測試用戶端掛**真正的 route 類別**，不是打在跑的伺服器（本機 BE 沒開），也沒有開真的瀏覽器執行，是用 HTML 解析器確認 script 會被切開。「偷登入憑證」這一步是依前端程式碼推論的（同網址＋localStorage），沒有端到端實跑。
- 資料庫隔離的結論是在 DEV 查規則與帳號屬性得出的，沒有實際用 `cm_app` 身分跑一次跨公司的 UPDATE。

## 執行概況

| 項目 | 數值 |
|---|---|
| 範圍 | 13 檔／1,786 行（開跑前 `git ls-files | xargs wc -l` 核對相符） |
| 工具參數 | `--effort low`、focus `attack-surface`、scanRoot＝BE repo |
| run ID | `wf_a828ae6e-597` |
| 報告目錄 | `CLAUDE-SECURITY-20260925-053527/`（不進版控） |
| 版本 | `159a2fff977c`（工作區有平行線未 commit 改動，stamp 標 `-dirty`） |
| 驗證章 | `verified`，5 候選、15 票、零漏投 |
| 研究員／面板 | 研究員 2 派 2 回；agent 17 派 17 回、0 失敗 |
| 耗時 | 約 35 分鐘 |
| 子 agent 用量 | 約 118 萬 token |
| 結果 | 工具通過 4 條（高 1、中 3），3 條為既有重現；runner 另補低 3 條 |
