---
title: U6 檢查結果：雲端硬碟整合——八支檔案搬移處理器
---

# U6 檢查結果：雲端硬碟整合——八支檔案搬移處理器

> 檢查日期 2026-09-25｜對應卡片 CM-2157（母卡 CM-2152）｜檢查範圍 10 個檔案 1,758 行｜工具 run ID `wf_6fa5dc78-d4f`｜驗證章 `verified`

## 🔴 一句話結論

**這八支背景程式拿到「任務編號、資料夾編號、檔案編號」時，一律不核對它屬於哪家客戶，而它們寫的證據表又沒有資料庫隔離，所以程式這一關確實是唯一防線，而且這道防線不存在。** 我在 DEV 用客戶 131 的身分實測，把一筆證據寫進了客戶 102 的任務，也把客戶 102 的一筆證據刪掉了（實測整段回滾，沒有殘留）。

這一棒共三件：

1. **🟠 高（runner 自行實測，未經三人面板）**：客戶 A 的使用者只要知道客戶 B 某個專案的編號，就能讓系統把 B 的整棵專案結構（輪次、控制項、任務名稱）建進 A 自己的雲端硬碟，之後 A 往那些資料夾丟檔，就會被當成 B 那張任務的證據匯進去。
2. **🟡 中（工具報、三人面板 3 票全數確認）**：系統幫每個證據資料夾開的分享權限是「任何知道連結的人都能編輯」，連最上層的根資料夾也是。而根資料夾的編號，客戶內任何登入帳號都能從 API 拿到，不必是專案成員。
3. **🟡 中（runner 自行實測，未經三人面板）**：處理 Google 變更清單、軟刪證據、對帳的程式，都用「全系統唯一」的資料夾／檔案編號去查資料，不限定客戶，所以一家客戶的背景工作可以改動另一家的證據與資料夾對應。第 1 件就是踩在這個缺口上。

## 這一棒在檢查什麼

客戶把公司的 Google 雲端硬碟接上系統後，系統會在客戶的硬碟裡按「專案 → 輪次 → 控制群組 → 控制項 → 檢核項目 → 任務」建一整棵資料夾。之後客戶把檔案丟進任務資料夾，系統會自動抓回來存成那張任務的「證據」。這一棒的 10 支程式就是做這件事的背景工人：

| 檔案 | 行數 | 做什麼 |
|------|-----:|------|
| `app/cloud_integration/service/handlers/init_project_folders_handler.py` | 449 | 為一個專案建整棵資料夾樹 |
| `app/cloud_integration/service/handlers/process_drive_changes_handler.py` | 275 | 讀 Google 送來的變更清單，決定要匯入、軟刪還是重建資料夾 |
| `app/cloud_integration/service/handlers/import_drive_file_handler.py` | 264 | 把雲端的一個檔案抓回來，寫成證據 |
| `infra/cloud_integration/repository/drive_folder_mapping_repo_impl.py` | 171 | 「雲端資料夾 ↔ 系統裡的哪個東西」對應表的查詢與寫入 |
| `app/cloud_integration/service/handlers/archive_drive_file_handler.py` | 165 | 使用者在系統裡刪證據時，把雲端的檔案搬到封存資料夾 |
| `domain/cloud_integration/service/drive_folder_mapping_domain_service.py` | 133 | 對應表的業務邏輯（新增或更新、標記失聯） |
| `app/cloud_integration/service/handlers/reconcile_task_folder_handler.py` | 129 | 任務被退回後，比對雲端與系統，補匯入或軟刪 |
| `app/cloud_integration/service/handlers/create_folder_handler.py` | 71 | 建單一資料夾 |
| `app/cloud_integration/service/handlers/rename_folder_handler.py` | 59 | 系統裡改名時同步改雲端資料夾名 |
| `app/cloud_integration/service/handlers/soft_delete_evidence_handler.py` | 42 | 雲端檔案被刪時，把對應證據標成已刪 |

**先釐清三件事實**（2026-09-25 12:40～13:05 +08 在 DEV 唯讀查證）：

- **證據表 `compliance.job_evidences` 沒有客戶欄位，也沒開資料庫隔離**（`relrowsecurity = f`）。這張表要靠程式自己檢查。
- **資料夾對應表 `public.drive_folder_mappings` 有開隔離**，但這八支程式跑在排程的「系統身分」底下（`core/scheduler.py:177` 的 `system_context("drive_sync_worker")`），系統身分會直接繞過隔離，**所以對這八支程式而言，資料庫隔離等於沒開**。
- 資料夾對應表的唯一約束是「同一家客戶內，同一個東西只能有一個資料夾」（`uq_drive_folder_mappings_scope` 是 `(tenant_id, scope_type, scope_uid)`），**不同客戶可以各有一列指到同一個任務**。

每支處理器拿到的編號是誰給的：

| 處理器 | 拿到的編號 | 從哪裡來 | 有沒有核對屬於這家客戶 |
|---|---|---|---|
| 初始化專案資料夾 | 專案編號 | 使用者打 `POST /integrations/google-drive/projects/<project_uid>/init-folders`，網址上填的 | ❌ 沒有（見 U6-1） |
| 處理變更清單 | 雲端檔案與父資料夾編號 | 這家客戶自己的 Google 帳號送來的變更清單 | ❌ 查對應表時不限客戶（見 U6-3） |
| 匯入檔案 | 任務資料夾對應編號 | 上一步排入的 | ❌ 不核對任務屬於哪家客戶（見 U6-3） |
| 軟刪證據 | 雲端檔案編號 | 變更清單或對帳排入的 | ❌ 查證據時不限客戶（見 U6-3） |
| 對帳 | 任務編號 | 任務退回時系統自己排入，客戶編號取自操作者 | ⚠️ 上游有先核過任務，但本支不再核 |
| 封存 | 證據編號 | 使用者刪證據時系統排入 | ⚠️ 上游有核過，本支用證據反查任務，不再核客戶 |
| 建資料夾／改名 | 任務或其他節點編號 | 建立、改名任務時系統排入，客戶編號取自操作者 | ⚠️ 上游有核過，本支查對應表時有帶客戶編號 |

## 掃到什麼（總覽）

| # | 這是什麼問題 | 出事會怎樣 | 要先有什麼才打得到 | 該補檢查的位置 | 嚴重度＋為什麼 | 來源 |
|---|---|---|---|---|---|---|
| U6-1 | 「初始化專案資料夾」拿網址上的專案編號就開工，不核對這個專案是不是呼叫者自家公司的 | ① B 公司的專案名、輪次、控制項、任務名稱被建成資料夾，出現在 A 公司的雲端硬碟裡（**外洩**）。② A 往這些資料夾丟的檔，會被匯進 B 那張任務當證據（**竄改稽核證據**） | A 公司有接雲端硬碟、有這項授權；A 的任一登入帳號**知道 B 某個專案的編號**（隨機長碼，猜不到，要從別處外洩） | `app/cloud_integration/service/drive_sync_admin_service.py:108`（排入前先確認專案屬於呼叫者公司＋呼叫者是專案管理者）；`app/cloud_integration/service/project_tree_loader.py:86`（系統身分載專案時帶公司編號過濾） | 🟠 高：跨公司讀＋寫稽核證據，門檻只剩「知道一個專案編號」，與總表第 7 項同級 | runner 開檔＋DEV 分段實測（未經三人面板） |
| U6-2 | 系統建的每個資料夾（含根資料夾）都設成「任何拿到連結的人都能編輯」 | 拿到任一資料夾連結就能看、下載、改、刪底下全部證據，不必登入 Google 也不必登入系統；丟進任務資料夾的檔會被自動匯成正式證據 | 自家公司已接雲端硬碟＋拿到任一資料夾連結（公司內任何登入帳號都能從系統要到根資料夾編號） | `infra/cloud_integration/google_drive/google_drive_api_client.py:285`（改成只分享給公司網域或指定帳號）；`api/cloud_integration/routes/google_drive_integration_route.py:41`（根資料夾編號不回給沒有讀取權限的人） | 🟡 中：影響大，但這是 FR-016 設計時刻意選的做法，要決策者裁 | 工具 F1，面板 3:0；runner 開檔核對。**與 U5-2 同一項** |
| U6-3 | 背景處理器用「全系統唯一」的資料夾編號、檔案編號查資料，查詢不限定公司 | 一家公司的背景工作可以把證據寫進別家公司的任務、把別家的證據標成已刪、把別家的資料夾標成失聯（之後那個資料夾收到的檔都不會匯入） | 這家公司自己的 Google 變更清單裡，出現一個父資料夾或檔案編號屬於別家的項目。**有兩種非惡意情境會自然觸發**：兩家公司接同一個 Google 帳號（系統沒擋），或踩著 U6-1 建出來的對應 | `infra/cloud_integration/repository/drive_folder_mapping_repo_impl.py:67`、`:77`；`infra/flow_engine/repository/job_evidence_repo_impl.py:136`、`:147`、`:155`（查詢都帶公司編號，或由處理器查完核對任務所屬公司） | 🟡 中：實測確認會跨公司寫與刪，但單獨觸發要靠「同一 Google 帳號接兩家」或其他缺口配合 | runner 開檔＋DEV 實測（未經三人面板） |

> **現況（2026-10-01）**：本棒各條後來的處理結果如下（過程紀錄保留，不改）。
>
> - U6-1（初始化資料夾跨公司）＝總表第 192 項，✅ 已修（CM-2201，commit `73f5f90af`，1.21.0 出貨）。
> - U6-2（資料夾任何人可編輯）＝總表第 193 項，🚫 裁定不修（決策者 10-01）。
> - U6-3（背景處理器不分公司）＝總表第 194 項，✅ 已修（CM-2204，commit `e00f5ed04`，1.21.0 出貨）。

**工具報的：一條（U6-2），三人面板 3 票全數確認。** U6-1、U6-3 是我依卡片「每支處理器問編號是誰給的」逐支追出來、再到 DEV 實測的，沒有經過三人面板投票。

## 工具報的逐條

### F1 → U6-2 🟡 每個雲端資料夾都「任何拿到連結的人都能編輯」（中，3:0）

> **與 U5-2 同一項。** U5 從「建資料夾」那一側報到，U6 從「建樹」與「把檔案匯成證據」這一側報到，是同一個根因。總表只登記一項。

- **在哪**：`app/cloud_integration/service/handlers/init_project_folders_handler.py:448`（建整棵樹時每個資料夾都呼叫）；同一個呼叫也在 `create_folder_handler.py:59`、`archive_drive_file_handler.py:157`。權限內容寫死在 `infra/cloud_integration/google_drive/google_drive_api_client.py:285-301`：`{type: anyone, role: writer, allowFileDiscovery: false}`。
- **白話說明**：「anyone／writer」的意思是**任何人拿到連結都能編輯**。`allowFileDiscovery: false` 只讓它搜尋不到，不擋拿到連結的人。根資料夾 `GuidantAI` 也是這樣設，而子資料夾會繼承上層的分享。所以只要拿到根資料夾連結，全公司每個專案的證據都打得開。
- **根資料夾編號怎麼拿到**：`GET /integrations/google-drive` 只要登入加授權，不看能力點，回傳內容就有 `root_folder_id`（`api/cloud_integration/routes/google_drive_integration_route.py:36-49`、回傳格式 `api/cloud_integration/serializers/google_drive_integration.py:13`）。程式註解說刻意不掛能力點，理由是一般成員的頁面要判斷「有沒有連線」。但判斷有沒有連線不需要根資料夾編號。
- **U6 這一側多出來的影響**：丟進任務資料夾的檔，下一輪同步會被 `import_drive_file_handler.py:184-226` 匯成那張任務的正式證據。既有證據的檔如果被改寫（修改時間變了），系統存的檔也會被換掉（`:202-208`）。匯入時雖然記下了「最後修改者信箱」（`drive_last_modifying_user_email`），但**沒有任何地方核對這個人是不是專案成員**。
- **是設計不是漏寫**：`docs/features/FR-016-2604-google-drive-sync/design.md:52`、`:297-303` 明寫「統一用 anyoneWithLink + writer」「拿到連結＝拿到編輯權，請提醒管理者保管連結」。所以這條要決策者裁要不要改，不是單純修 bug。
- **建議修法**：分享改成只給公司的 Google Workspace 網域（`type: domain`），或只分享給專案成員的帳號；根資料夾編號從狀態查詢拿掉，或改成要有讀取權限才回；匯入前核對最後修改者是不是專案成員。
- **HIGH 級自行核對**：工具評為中，不屬 HIGH。以上檔案行號我逐一開檔確認過。

## runner 自行追出的兩條

### U6-1 🟠 知道別家公司的專案編號，就能把它的整棵結構建進自己的硬碟，再往裡面塞證據（高）

- **在哪**：
  - 入口 `api/cloud_integration/routes/google_drive_sync_route.py:101-122`（`POST /integrations/google-drive/projects/<project_uid>/init-folders`）：只掛了「已登入」與「有雲端授權」，公司編號取自呼叫者，專案編號直接取網址。
  - `app/cloud_integration/service/drive_sync_admin_service.py:108-131`：直接排入工作，不查專案。註解寫「前端已經只讓專案管理者看到按鈕，後端不另外檢查」。
  - 背景執行 `init_project_folders_handler.py:83-105` → `project_tree_loader.py:86-91`：用「系統身分＋管理者」載專案（`SYSTEM_USER_ID = 0`、`SYSTEM_IS_ADMIN = True`），而排程本身就在系統身分底下，**資料庫隔離也被繞過**。
  - 寫對應表 `init_project_folders_handler.py:365-407` 的 `_ensure_folder`：以「呼叫者的公司編號＋別家的任務編號」寫一列。資料庫的唯一規則是「同一家公司內不重複」（`uq_drive_folder_mappings_scope`），所以擋不住。
- **白話說明**：這個按鈕是「幫這個專案在雲端硬碟重建資料夾」。後端收到專案編號後，沒問「這是你們公司的專案嗎」就交給背景工人，而背景工人用的是系統最高身分，什麼都查得到。結果是把別家公司的專案結構抄進呼叫者自己的雲端硬碟。
- **出事會怎樣**：
  1. **外洩**：B 公司的專案名、每一輪的名稱、控制群組與控制項、檢核項目標題、每張任務名稱，全變成 A 公司硬碟裡的資料夾名。
  2. **竄改證據**：對應表裡現在有一列「A 的資料夾 → B 的任務」。A 往那個資料夾丟檔，A 自己的同步流程查到這列對應，就用任務編號去找任務（`import_drive_file_handler.py:103-105`）。找到的是 B 的任務，證據就寫進 B 的任務（`:210-226`）。**B 公司的稽核人員會在自己的任務裡看到一份來源不明的證據。**
- **DEV 實測（2026-09-25 12:55～13:05 +08，整段回滾）**：
  - 用系統身分、以客戶 131 的立場，載客戶 102 的專案 `c93f3dbb-…`：**成功載出專案名與輪次**。證實背景載入完全不分公司。
  - 用客戶 131 的身分跑匯入處理器，對應編號指向一個任務資料夾，而那個任務屬客戶 102（任務 `ba6a3d00-…`，內部編號 14795）：**證據成功寫進客戶 102 的任務 14795**（類型「連結」、網址是我填的假網址）。
  - 「建對應表那一步」沒有實跑：DEV 的客戶 131 沒接雲端硬碟，而這一步必須呼叫 Google。這一步是依程式碼加資料庫唯一規則判斷的，見文末「環境打折」。
- **要先有什麼**：A 公司已接雲端硬碟、有雲端授權；A 的任一登入帳號知道 B 的某個專案編號。專案編號是隨機長碼、猜不到，要從別處取得，例如總表第 7 項那類「知道一個編號就能讀別家資料」的缺口、截圖、客服轉寄。**同一家公司內**，這支入口也沒檢查呼叫者是不是該專案的管理者。前端只是把按鈕藏起來，任何有授權的成員都能直接打。
- **建議修法**：在 `trigger_init_project_folders` 排入前，用既有的專案守門（`common/authz` 的專案角色軸）確認「專案屬於呼叫者公司＋呼叫者是專案管理者」。`ProjectTreeLoader.load` 載專案時也帶上工作所屬公司的編號，查到別家就停，作為第二道。
- **HIGH 級自行核對**：上列每個行號都開檔確認過；後半段（匯入寫進別家任務）有實測。沒實跑的只有「建對應表」這一步，已在上面標明。

### U6-3 🟡 背景處理器用全系統唯一的編號查資料，不限定公司（中）

- **在哪（查詢都沒帶公司編號）**：
  - 資料夾對應表：`drive_folder_mapping_repo_impl.py:67-75` `get_by_drive_folder_id`、`:77-83` `get_by_uid`；`drive_folder_mapping_domain_service.py:108-119` `mark_unlinked_by_drive_id`。
  - 證據表：`infra/flow_engine/repository/job_evidence_repo_impl.py:136-145` `get_by_drive_file_id`、`:147-153` `…_including_deleted`、`:155-166` `soft_delete_by_uid`。
- **誰在用**：
  - 處理變更清單 `process_drive_changes_handler.py:164`：把資料夾標成失聯。`:222` 認資料夾搬家。`:256-262` 找任務資料夾。`:271-275` 判斷是不是封存資料夾。
  - 匯入 `import_drive_file_handler.py:91`：拿對應編號，只查「是不是任務資料夾」，不查是不是這家公司的。`:142`：用檔案編號找既有證據。
  - 軟刪 `soft_delete_evidence_handler.py:34-37`：整支只有檔案編號，沒有任何公司判斷。
  - 對帳 `reconcile_task_folder_handler.py:101`。
- **白話說明**：Google 的資料夾、檔案編號在全世界是唯一的，程式就假設「查到誰就是誰」。但背景工作是「以某家公司的名義」在跑，查到的資料卻可能屬於另一家。程式從頭到尾沒有比對「這筆資料的公司」和「這份工作的公司」是否相同。
- **DEV 實測（同上時段，整段回滾，Google 端用假物件）**：
  1. 以客戶 131 的名義處理一筆變更：「一個檔案，父資料夾是客戶 102 的任務資料夾」→ **排入了一張匯入工作**，帶的是客戶 102 那個任務資料夾的對應編號。
  2. 以客戶 131 的名義跑那張匯入 → **證據寫進客戶 102 的任務**（同 U6-1 實測）。
  3. 以客戶 131 的名義跑軟刪，檔案編號是客戶 102 唯一一筆雲端同步證據的 → **那筆證據從有效變成已刪**。
  - 事後唯讀查 DEV（13:06）：假檔案編號 0 筆，客戶 102 那筆證據 `is_deleted = f`，確認全數回滾，沒有殘留。
- **另外讀碼看到、沒實測的一條分支**：匯入時如果用檔案編號找到的既有證據屬於別家（`:142`），而且修改時間不同，程式會走「更新既有證據」（`:202-208`）。也就是把 A 公司下載的檔掛到 B 公司的證據上，檔案本體還存在 A 公司的儲存空間。
- **要先有什麼（為什麼評中不評高）**：這條的觸發點是「這家公司自己的 Google 變更清單裡，出現屬於別家的編號」。變更清單是 Google 依那個 Google 帳號看得到的檔案產生的，攻擊者不能自己亂填。會自然發生的情境有三種：
  - **兩家公司接同一個 Google 帳號**：資料庫只規定「一家公司一筆授權」（`tenant_drive_integrations_tenant_id_key`），沒規定「一個 Google 帳號只能接一家」（DEV 已查）。顧問公司替多家客戶代管就可能這樣接。接了之後兩家的變更清單互相看得到對方的資料夾，證據會互相串來串去，**不需要任何人有惡意**。這是依程式碼推論，DEV 只有一家接硬碟，沒辦法實跑。
  - **踩著 U6-1**：U6-1 建出了跨公司的對應列，之後就一路走這條。
  - **踩著 U6-2**：A 拿到 B 的資料夾連結，用自己的 Google 帳號往 B 的資料夾放檔，這個檔就會出現在 A 的變更清單裡。這是依 Google 行為推論，沒實測。
- **建議修法**：對應表的兩支查詢加上公司編號參數，處理器一律帶工作所屬的公司編號。證據表沒有公司欄位，所以匯入、軟刪在動手前要先「由證據反查任務 → 任務所屬公司」，和工作的公司比對，不同就停。長期根治是證據表補公司欄位並開資料庫隔離（與總表第 7 項同一張表）。另外考慮在接硬碟時拒絕「這個 Google 帳號已被別家接過」。

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

### ① 五支處理器寫證據前，有沒有確認「這個任務屬於這家公司」？——🔴 成立（U6-1、U6-3）

| 處理器 | 結果 | 依據 |
|---|---|---|
| 匯入 `import` | 🔴 不確認 | 對應編號進來只查類型與是否失聯（`:91-101`），任務用對應裡的編號直接查（`:103`），實測寫進別家任務 |
| 軟刪 `soft_delete` | 🔴 不確認 | 整支只有檔案編號（`:30-37`），實測刪掉別家證據 |
| 處理變更清單 `process_drive_changes` | 🔴 不確認 | 不直接寫證據，但決定要排哪張匯入／軟刪，查對應時不帶公司（`:256-262`），實測排出指向別家任務的匯入 |
| 對帳 `reconcile` | 🟡 部分 | 找任務資料夾時有帶公司編號（`:75`），所以只看得到自家的對應；但如果對應本身是 U6-1 造出來的跨公司列，後面就照單全收。排出的軟刪一樣走不分公司的軟刪處理器 |
| 封存 `archive` | ✅ 實際上擋得住 | 用證據反查任務後，以「工作的公司＋任務」找資料夾（`:105-112`）。證據如果是別家的，就找不到資料夾，直接判定失敗，不會搬檔。上游 `JobEvidenceService.delete_job_evidence` 的權限屬另一棒範圍，本棒沒追 |

### ② 匯入檔案：檔名、大小、類型有沒有限制？——🟢 大致不成立，列兩個小觀察

- **大小**：先看 Google 回報的大小，超過上限（預設 20 MB，`config/config.py:252`）直接判失敗、不重試（`import_drive_file_handler.py:172-182`）。下載時邊下邊量，超過就停（`google_drive_api_client.py:262-282`）。✅ 有限制。
  - 小觀察：Google 套件每次下載一段的預設大小是 100 MB，檢查在「每段下完之後」才做。所以超過上限時，最多會先在記憶體裡放到約 100 MB 才停。同步工作最多 4 條同時跑，最壞約 400 MB。實際上大小先在前面擋過一次，要 Google 回報的大小與實際內容不符才會走到這裡，風險低，不列項。
- **檔名**：我拿 `../../etc/passwd`、`a./../../../tmp/pwn`、含空字元等惡意檔名，實跑上傳套件的存檔路徑產生器（`jedi_file_upload/common/utils/file_utils.py:10-32`）。磁碟上的檔名一律是「時間＋隨機碼＋副檔名」。副檔名取最後一個點之後的字，不可能含 `..`，所以**跳不出儲存目錄**。✅ 不成立。
  - 小觀察：副檔名可以含 `/`，會在儲存目錄底下多開一層子目錄，不會跳出去。這是上傳套件的共用行為，不是 U6 獨有，屬檔案上傳那塊。
- **類型**：沒有副檔名白名單，一律以 `application/octet-stream` 存。這跟一般上傳路徑一樣，而且系統本來就要收各種證據檔，不列項。Google 文件類只存連結、不下載內容。

### ③ 變更清單裡的檔案編號如果指到別家公司的資料夾，會不會照單全收？——🔴 成立（U6-3）

會。`_find_task_mapping_for_file`（`process_drive_changes_handler.py:256-262`）拿父資料夾編號查對應表，不帶公司，查到任務資料夾就排匯入。我已在 DEV 實測排出這張工作。移除事件更直接：只要檔案編號就排軟刪，同時把同編號的資料夾標成失聯（`:155-165`），兩件都不分公司。

### ④ 09-17 新增的兩個證據查詢，有沒有限定任務範圍？——✅ 有，不成立

- `get_active_by_job_and_file(job_execution_id, file_id)`（`job_evidence_repo_impl.py:168-178`）：以「任務＋檔案」查。
- `find_active_by_content_hash(job_execution_ids, content_hash)`（`:180-195`）：只在傳進來的任務清單裡查。

兩支都限定了任務範圍。**不限定公司**，公司範圍靠呼叫端 `core/plugins/evidence_classification.py:429`、`:470` 保證：後者的任務清單來自 `resolve_jobs(round_id)`，只取這一輪的任務。呼叫端已在 FR-115 W7 掃過，本棒只確認查詢本身沒問題。

### ⑤「有人守了一半」共通疑點

| 疑點樣式 | 本棒結果 |
|---|---|
| 只驗「你是誰」沒驗「這筆是不是你的」 | 🔴 **成立** → U6-1：入口只驗登入＋授權，不驗專案歸屬 |
| 列表有守、單筆沒守 | 不適用：背景處理器沒有列表／單筆之分 |
| 守門寫在前端或上一層，後端繞得過 | 🔴 **成立** → U6-1：程式註解明寫「前端已限制按鈕，後端不另外檢查」 |
| 守門條件用 `or` 串、其中一個恆真 | ❌ 不成立 |
| 「查不到」與「沒權限」混成同一回應 | 不適用：背景工作沒有回應給使用者 |
| 系統身分執行時假設呼叫者是自己人 | 🔴 **成立** → U6-1、U6-3：排程在系統身分下跑，資料庫隔離全失效，而程式假設「排進來的工作一定是這家公司自己的東西」 |
| 防重放／計次只在單一進程有效 | ❌ 不成立：重複匯入靠資料庫唯一規則擋 |
| 註解寫「這裡刻意不檢查」但上一層沒檢查 | 🔴 **成立** → U6-1（`drive_sync_admin_service.py:119-122`） |

## 可信度（分兩層）

| 層 | 內容 | 可信到什麼程度 |
|---|---|---|
| 工具正式清單 | U6-2 一條 | 研究員提出後，三位驗證者從「打不打得到」「影響多大」「有沒有防線」三個角度各自開檔，**3 票全數確認**，驗證章 `verified`。我另外逐一開檔核對行號 |
| runner 自行追查 | U6-1、U6-3 | **未經三人面板投票。** 每條都有開檔核對，核心結論有 DEV 實測：跨公司寫入證據、跨公司刪除證據、系統身分載別家專案、變更清單指向別家資料夾，四項都實跑成立。沒實跑的部分已逐一標出，見下方「環境打折」 |

**為什麼工具沒報 U6-1、U6-3**：工具這一棒是 low 等級，只有一位研究員讀全部範圍。U6-1 的缺口在範圍外的入口與服務（路由、`drive_sync_admin_service.py`、`project_tree_loader.py`），範圍內的處理器只是忠實執行。U6-3 要把「系統身分會繞過隔離」「證據表沒有公司欄位」「查詢不帶公司」三件事串起來才看得出來。這正是卡片要求逐支追「編號是誰給的」的原因。

## 環境打折（一定要看）

- **沒有兩家公司都接雲端硬碟的環境**：DEV 只有客戶 102 接了雲端硬碟，客戶 131 沒接。所以實測是**直接呼叫處理器**、Google 端用假物件，跳過了「這家公司有沒有接硬碟」與真正的 Google 呼叫。證明的是「我方程式與資料庫不會擋」，**沒有證明 Google 端的行為**（例如 U6-3 情境三「A 用自己帳號往 B 的資料夾放檔，會出現在 A 的變更清單」是推論）。
- **U6-1 的完整鏈沒有一次跑完**：「入口 → 載別家專案 → 建跨公司對應 → 丟檔 → 匯入寫進別家任務」五步中，第 2、5 步實跑成立。第 3 步「建跨公司對應」依程式碼與資料庫唯一規則判斷，沒實寫，因為要呼叫 Google 建資料夾。第 4 步由 U6-3 的實測 1 間接證明。
- **應用程式目前在 DEV 開不起來**：用正式的啟動流程組裝時失敗。錯誤是 `FlowControlJobRepoImpl` 缺 `resolve_project_id_for_job`，這個抽象方法定義在目前虛擬環境指向的 `jedi-task-platform`（`jedi-wt-fix-security` 工作樹），主專案還沒補實作。我改成直接組裝處理器需要的零件來實測，處理器本身的程式一行沒動。**這件事跟本棒無關，但會讓本機 BE 起不來，請首腦轉給修正線。**
- 所有實測都包在同一個資料庫交易裡，最後故意中止讓它整段回滾，事後唯讀查證無殘留。實測腳本放在個人暫存目錄，不進版控。

## 執行概況

| 項目 | 值 |
|---|---|
| 工具 run ID | `wf_6fa5dc78-d4f` |
| 報告目錄 | `CLAUDE-SECURITY-20260925-042332/`（不進版控） |
| 驗證章 | `CLAUDE-SECURITY-REVISION-fe7c471f3d7c-dirty.json`，`verification.status: verified` |
| 掃描版本 | `fe7c471f3`（工作區有平行線未 commit 的改動所以標 dirty；本棒 10 支範圍檔在掃描當下沒有未 commit 改動） |
| 範圍 | 10 檔／1,758 行，`--effort low`，focus `attack-surface` |
| 研究員 | 派 2（一位讀全部範圍、一位專找寫死的密碼金鑰），回 2，`failed` 0 |
| 候選 → 面板 | 1 候選 → 3 票全數確認，最終嚴重度中（未被調降） |
| 耗時／用量 | 約 25 分鐘；子代理約 37 萬 token |
| runner 自行追查 | 全部 10 支逐支通讀；越界讀了路由、`drive_sync_admin_service.py`、`drive_sync_orchestration_service.py`、`project_tree_loader.py`、`drive_sync_worker.py`、排程、上傳套件、`job_evidence_repo_impl.py`（只讀不報的接縫） |
| DEV 查證 | 唯讀查詢於 12:40～13:06 +08；實測於 12:55～13:05 +08，整段回滾 |

---

沿革見 `FR-120-LOG` 與 git log。
