# W4 掃描報告：任務層直接查問卷／設備資料表（CM-2093）

> 範圍：3 檔，共 1,629 行，分別是 `infra/flow_control/repository/flow_control_job_repo_impl.py`（1,216 行）、`infra/flow_control/repository/job_import_lookup_query.py`、`infra/readmodel/tasks/job_export_query.py`。
> 掃描工具：Claude Code 官方 `claude-security` plugin，effort low，只看正式程式碼。
> 掃描基準 commit：`9abe5e64e895`。掃描時工作區有其他 session 還沒 commit 的改動。
> 驗證章：**verified**。6 條候選全部經過三人面板投票，18 票都投了，6 條都成立，其中 1 條被面板降級。只掃不修。
> 資料庫隔離現況：2026-09-23 22:34～22:41（台北時間）在 DEV 用唯讀查詢確認，見第 5 節。

---

## 1. 一句話結論

卡片擔心的「任務把別家客戶的問卷或設備拼進來」**不成立**：這批查詢碰到的問卷表、設備表、填答表、問卷指派表都有開資料庫隔離，別家客戶的資料進不來（詳見第 5、6 節）。

**真正的問題出在同一家公司內部、不同專案之間。** 任務相關的網址都帶「專案編號」和「任務編號」，系統只檢查「你在這個專案裡的身分」，**從來沒有核對這筆任務到底屬不屬於這個專案**。資料庫隔離只分客戶，不分專案，所以兩層都擋不住。

工具提報 6 條，三人面板全部判定成立。runner 自己開檔又追出 2 條。依總表現況分成三類：

- **新發現，要進總表（4 條）**：
  - W4-3：批次匯入任務時，可以順便改掉別的專案的任務。
  - W4-5：任務清單不檢查你是不是這個專案的成員。
  - W4-6：匯出任務 Excel 時，檢查的專案和實際匯出的專案可以是不同的兩個。
  - W4-R2：匯入的「驗證」和「重新驗證」兩步完全沒有檢查身分。
- **已登記的總表第 63 項，這次補了新細節（3 條）**：
  - W4-1：修改任務。
  - W4-2：刪除任務。這次另外發現，**稽核輪次凍結的保護也一起被繞過**。
  - W4-4：查看任務詳細。
- **W4-1 的連鎖後果（W4-R1）**：跨專案改任務時，會留下一筆假的「這個任務屬於哪個專案」紀錄，之後好幾個判斷會被它帶偏。

---

## 2. 這一棒在檢查什麼

「規劃頁」上每個查核項目底下都有若干任務。專案管理者可以在頁面上新增、修改、刪除任務，也可以把任務匯出成 Excel、改好再匯回來。

這三支檔案是這些功能的資料存取層：

- `flow_control_job_repo_impl.py`：任務的讀取、新增、修改、刪除。
- `job_import_lookup_query.py`：Excel 匯入時，查人員、部門、設備的名稱。
- `job_export_query.py`：Excel 匯出時，把任務與指派資料組成一張表。

這三支都**不透過問卷套件、設備套件的服務**，而是直接查它們的資料表。所以套件自己的權限檢查在這條路上不會生效，能擋的只有兩層：

1. **資料庫隔離**：只擋「不同客戶」。
2. **呼叫這三支的服務方法**（`job_service.py`、`job_import_service.py`）裡的權限檢查。

資料存取層本身**本來就不該做權限檢查**，這一層 19＋7＋2 個方法都沒有權限檢查是正常的。這一棒要回答的是：**守門的那一層有沒有守對東西。**

---

## 3. 掃到什麼：總覽

| # | 發生在哪個畫面、誰做什麼 | 出事會怎樣 | 要先有什麼才打得到 | 該補檢查的位置 | 嚴重度 | 與總表的關係 |
|---|---|---|---|---|---|---|
| **W4-1** | **規劃頁「編輯任務」**。某個專案的管理者送出修改時，網址填自己的專案，任務編號填別的專案的 | 別的專案的任務被改名、改說明、改類型、換問卷、換部門與設備，**還可以把自己加成指派人**，然後就能去執行那個任務 | 同公司內任一專案的管理者（自己開一個專案就是），並且知道目標任務的編號 | `app/flow_control/service/job_service.py:266`（`update_job` 開頭，第 287 行查完身分後要補「任務屬於此專案」）<br>`api/flow_control/routes/job_route.py:104` | 中 | **總表第 63 項後半**（已登記「改與刪只驗人、沒驗物」），不另計 |
| **W4-2** | **規劃頁「刪除任務」**。同上，網址填自己的專案，任務填別人的 | 別的專案的任務**永久刪除**，連同指派、部門、設備、問卷、留言。🔴 **新細節**：「輪次凍結後不能刪任務」這道保護查的也是網址上的專案，所以**對方已經開跑、已凍結的稽核輪次裡的任務也刪得掉** | 同公司內某個還在規劃期專案的管理者，並且知道目標任務的編號 | `app/flow_control/service/job_service.py:211`（`delete_job`，凍結檢查要改成查任務真正所屬的專案；`project_uid` 預設 `None` 要改成必填）<br>`api/flow_control/routes/job_route.py:147` | 中 | 總表第 63 項後半，**但凍結被繞過是新的後果**，要加進修正卡的驗收項 |
| **W4-3** | **規劃頁「匯入任務 Excel」的確認步驟**。管理者上傳的 Excel 裡，任務編號填別的專案的 | 一次**批次改掉別的專案的任務**：換指派人、換類型、換部門與設備。程式註解寫著「防止未經驗證的確認」，但那份白名單只確認「任務存在」，不確認「在這個專案裡」 | 同公司內任一專案的管理者，並且知道目標任務的編號 | `app/flow_control/service/job_import_service.py:397`（`confirm_import`，第 413 行的白名單要改用這個計畫底下的任務）<br>`infra/flow_control/repository/job_import_lookup_query.py:86` | 中 | **新** |
| **W4-4** | **任務詳細頁**。任何登入的人打開一筆任務 | 看得到任何一個專案的任務完整內容：指派人、部門、設備、問卷、檢測工具設定 | 同一家公司的登入帳號，並且知道任務編號 | `app/flow_control/service/job_service.py:139-140`（`get_job`，連專案編號這個參數都沒有）<br>`api/flow_control/routes/job_route.py:82` | 中 | **總表第 63 項前半**，不另計 |
| **W4-5** | **規劃頁的任務清單**。任何登入的人帶別的專案的編號來查 | 列出別的專案每個查核項目底下的任務，**包含任務編號**。W4-1～W4-3 需要的任務編號就是從這裡來的 | 同一家公司的登入帳號，並且知道目標專案的編號。查核項目的編號是公開標準的條號（例如 `AC.L1-3.1.1_obj.1`），可以用猜的 | `app/flow_control/service/job_service.py:243-244`（`list_jobs` 開頭補成員檢查）<br>`api/flow_control/routes/job_route.py:52` | 中 | **新**（總表第 63 項只把清單當成「看到編號的地方」，沒有把清單本身列成缺口） |
| **W4-6** | **規劃頁「匯出任務 Excel」**。網址上有兩個編號：專案編號和計畫編號。系統檢查你是不是前者的成員，匯出的卻是後者指向的專案 | 某個專案的一般成員（連管理者都不必是）可以把**別的專案的整張任務表**匯出來，內容包括任務編號、指派人的登入帳號、部門、設備 | 同公司內任一專案的成員，並且知道目標專案的編號 | `app/flow_control/service/job_import_service.py:103-104`（`export_excel`，要核對計畫編號解析出來的專案等於網址上的專案）<br>`infra/readmodel/tasks/job_export_query.py:79` | **低**（面板 2:1 從中降為低，理由見 4.6） | **新** |
| **W4-R1** | W4-1 的連鎖後果：跨專案加指派人時，系統會寫一筆「這個任務屬於**你的**專案」的指派紀錄 | 有好幾個地方用「指派紀錄」反推任務屬於哪個專案，包括凍結判斷、存流程圖時查輪次狀態、「我的任務」清單，**都可能被這筆假紀錄帶偏** | 同 W4-1 | 修 W4-1 就一起消失。受影響的讀取點：`flow_control_job_repo_impl.py:327`（`_is_round_frozen_for_job`，第 339 行取第一筆、沒有排序）、`:970`（`current_round_status_by_workflow_execution` 第二條路） | 中（與 W4-1 一起修） | 與**總表 M10 第 6 條**是同一種病（「塞一筆假指派，騙過歸屬判斷」），這次在另一個入口再出現一次 |
| **W4-R2** | **匯入任務 Excel 的「驗證」與「重新驗證」兩步**。任何登入的人都可以呼叫 | 可以拿來試探「某個任務編號屬不屬於某個計畫」，也可以試探公司內有哪些帳號、部門、設備名稱。不會改到資料 | 同一家公司的登入帳號 | `app/flow_control/service/job_import_service.py:197`（`validate_import`）、`:314`（`revalidate_items`）開頭補成員檢查 | 低 | **新**。與 W4-6 是同一組入口，建議併同一張卡 |

**W4-1～W4-6 經過三人面板投票；W4-R1、W4-R2 是 runner 自己開檔核對的，沒有經過三人面板投票。**

---

## 4. 每條發現的詳述

### 4.1 W4-1：管理者可以改掉別的專案的任務

**場景**：小明在公司裡自己開了一個專案 A，所以他是 A 的管理者。他從任務清單（W4-5）或匯出表（W4-6）拿到專案 B 的某個任務編號。在規劃頁按「編輯任務」送出時，他把網址改成 `/grc/project/<A>/job/<B 的任務>`，指派人填自己。

**會發生什麼**：系統先確認「小明是不是 A 的管理者」，是，就放行。接著用任務編號把那筆任務撈出來直接改，**從頭到尾沒有比對這筆任務是不是 A 的**。專案 B 的任務就被改名、換問卷、換設備，小明也變成指派人。

**為什麼擋不住**：
- 程式這一關：`job_service.py:287` 只呼叫 `_require_manager(project_uid)`，查的是網址上的專案。
- 資料庫這一關：任務表的隔離規則是 `app_tenant_allowed_for_session(tenant_id)`，**只分客戶**，同一家客戶的任務全部看得到（第 5 節）。

**與總表的關係**：`docs/security-report/M20-tenant-isolation.md` 第 3 條已寫明「補上的守門仍不核對這筆任務到底屬不屬於那個專案」，也就是總表第 63 項的後半。**這條是既有案，不另計。** 修正卡是 FR-114 卡 2-7。

### 4.2 W4-2：管理者可以刪掉別的專案的任務，而且繞過凍結

**場景**：同上，小明按「刪除任務」，網址專案填 A，任務填 B 的。

**會發生什麼**：B 的任務被**永久刪除**：指派、部門、設備、問卷連結、留言都一起清掉，流程圖上的方塊也被移除（`flow_control_job_repo_impl.py:777` 起）。

**新細節（總表還沒寫到）**：規劃頁「稽核輪次開跑後不能新增或刪除任務」這道保護（`job_service.py:100` `_assert_round_in_planning`），吃的也是**網址上的專案**。所以只要小明自己的 A 還在規劃期，就算 B 的稽核已經開跑、任務清單已經凍結，**B 的任務還是刪得掉**。凍結保護的就是「稽核範圍不能中途改變」，這等於它對跨專案攻擊完全無效。

**建議**：併入總表第 63 項的修正卡（FR-114 卡 2-7），並在那張卡的驗收項加一條：「凍結檢查要查任務真正所屬的專案」。

### 4.3 W4-3：批次匯入時順便改掉別的專案的任務

**場景**：小明是 A 的管理者。他在規劃頁用「匯入任務 Excel」，上傳前把 Excel 的「任務識別碼」欄換成 B 的任務編號，指派人填自己。

**會發生什麼**：
1. 「驗證」那一步會報「任務不屬於此稽核計畫」，但**「確認」那一步不重跑那個比對**。
2. 確認時用的白名單是 `get_job_control_uid_map(all_job_uids)`（`job_import_lookup_query.py:86`），它只查「這些任務存不存在、有沒有對應的控制項」，**不限定專案，也不限定計畫**（第 115 行只有 `JobExecution.uid.in_(job_uids)`）。
3. 名單上的每一筆都會丟進 `update_job(project_uid=A)`，一次批次改掉 B 的任務。

**為什麼是新的**：總表第 63 項講的是單筆編輯；批次匯入是**另一個入口**，而且那段「防止未經驗證的確認」的註解讓人以為已經守住了。這正是「同一件事兩個入口兩套門」的形狀。

**建議**：白名單改用「這個計畫底下的任務」，也就是驗證步驟用的 `get_export_data(ap_uid)`，並且核對計畫解析出來的專案等於網址上的專案。**要和 W4-1 放在同一張卡，否則修好一個入口，另一個還開著**（見 memory「平行 runner 兩入口只改一邊」）。

### 4.4 W4-4：任何人都能開任務詳細頁

即總表第 63 項前半，不另計。本棒只再確認一次現況：`job_service.py:140` `get_job(self, uid)` 的簽名裡**仍然沒有專案編號**，整支沒有任何權限檢查。

### 4.5 W4-5：任務清單不檢查成員

**場景**：同一家公司的任何登入的人，在規劃頁的任務清單 API 帶上別的專案的編號，再把查核項目的編號換成標準條號（例如 `AC.L1-3.1.1_obj.1`），一條一條查。

**會發生什麼**：每一條查核項目底下的任務都列出來，**包含任務編號、指派人、部門、設備、問卷**。W4-1～W4-3 都需要任務編號，**這裡就是它們的來源**。

**為什麼擋不住**：`job_service.py:244` `list_jobs` 沒有任何權限檢查。資料存取層（`flow_control_job_repo_impl.py:90` 那段 SQL）確實用網址上的專案縮小了範圍，所以**不會看到別家客戶的**，但沒人問「你是不是這個專案的人」。

**為什麼是中不是高**：要先知道目標專案的編號。專案編號是隨機產生的長串，不能用猜的；不過同一家公司內的人，從其他畫面拿得到的機會不低。

### 4.6 W4-6：匯出時，檢查的專案和匯出的專案可以不同

**場景**：小華只是專案 A 的一般成員。她在「匯出任務 Excel」的網址 `/grc/project/<A>/ap/<X>/jobs/export` 裡，把 X 換成專案 B 的編號。

**會發生什麼**：
1. 系統檢查「小華是不是 A 的成員」，是。
2. 接著 `get_export_data(ap_uid)` 會依序把 X 當成專案編號、輪次編號、計畫編號去解析（`job_export_query.py:109`，實際在 `jedi-compliance-audit` 套件的 `_resolve_ssp`），**解析到 B 就匯出 B**。
3. 小華拿到 B 的整張任務表。

**面板為什麼降成低**：三票裡兩票給低，理由是：匯出內容大多是規劃資訊，不含證據本體或稽核結論；而且只限同一家公司。我同意降級，但要提醒一點：**它吐出的任務編號正是 W4-1～W4-3 的攻擊材料**，所以修正時要跟那幾條一起排。

**附帶**：`jobs/export` 與 `jobs/import` 的網址是 `jedi-task-platform` 套件宣告的，套件那邊只掛了「登入＋授權模組」，真正的權限檢查交給宿主的服務。**宿主的服務只守了四個入口中的兩個**（匯出、確認），另外兩個見 W4-R2。這就是卡片說的「套件以為宿主守，宿主只守一半」。

### 4.7 W4-R1：跨專案加指派人會留下假的歸屬紀錄（runner 自行核對）

**場景**：接續 W4-1，小明把自己加成 B 的任務的指派人。

**會發生什麼**：`update_job` 寫進指派表的那一筆，`project_id` 用的是**網址上的 A**（`flow_control_job_repo_impl.py:589` 用網址專案查 `project_id`，第 614 行寫入）。於是 B 的任務在指派表裡多了一筆「屬於 A」的紀錄。

**為什麼要緊**：系統有好幾個地方是用「指派表」反推「這個任務屬於哪個專案」：
- `_is_round_frozen_for_job`（`:339`）取**第一筆**指派紀錄、沒有排序。拿到的若是 A 那筆，B 的任務能不能「退回」就改看 A 的輪次狀態。
- `current_round_status_by_workflow_execution`（`:995` 起的第二條路）一樣是 JOIN 指派表去找輪次。
- 「我的任務」清單（資料庫檢視表 `vw_user_job_queue`）顯示的專案欄位取自指派表，這個任務會以「A 專案的任務」出現在小明的清單上。

**結論**：這不是獨立的洞，而是 W4-1 讓傷害擴散的方式。**修 W4-1 就會一起消失**，不另開卡；但修正卡的驗收要檢查 DEV 上**有沒有已經存在的這種錯位紀錄**。DEV 實查時間 22:38：165 個專案的指派紀錄中，**沒有**任何一個任務同時掛在兩個專案底下，目前乾淨。

### 4.8 W4-R2：匯入的驗證兩步沒有任何權限檢查（runner 自行核對）

**場景**：同一家公司的任何登入的人，直接呼叫匯入的「驗證」或「重新驗證」。

**會發生什麼**：
- `validate_import`（`job_import_service.py:197`）和 `revalidate_items`（`:314`）開頭都**沒有**成員檢查。
- 回傳的錯誤訊息可以當成試探工具，例如「任務 X 不屬於此稽核計畫」「使用者不存在：Y」「設備不存在：Z」。
- 不會寫進資料庫，所以沒辦法改資料。

**為什麼是低**：只能確認「某個編號或名稱存不存在」，同公司的人本來就從其他畫面看得到大部分這類名稱。**建議和 W4-6 併在同一張卡**，四個入口一起補成「計畫要屬於網址上的專案＋你要是成員」。

---

## 5. 資料庫隔離現況（卡片要求的對照表）

**查詢方式**：2026-09-23 22:34，用 DEV 的 `cm_app` 帳號（受隔離規則管）唯讀查 `pg_class.relrowsecurity` 與 `pg_policy`。22:38 另外開一個「唯讀交易＋最高權限變數」查資料分布，結束就回滾，沒有改任何東西。

「有隔離」指資料庫開了隔離，而且有查詢規則；「只分客戶」指規則只比對客戶，不比對專案。

| 表 | 有沒有隔離 | 隔離依據 | 這批哪裡碰到 |
|---|---|---|---|
| `survey.surveys`（問卷） | ✅ 有 | 客戶 | repo `:190`、`:193`、`:269`、`:869`、`:916` |
| `survey.task_surveys`（問卷指派） | ✅ 有 | 跟隨流程實例（流程實例再依客戶） | repo `:1094` |
| `survey.question_answers`（填答） | ✅ 有 | 跟隨題目（題目再跟隨問卷頁） | repo `:920` |
| `public.devices`（設備） | ✅ 有 | 客戶 | repo `:166`、`:289`、`:639`、`:770`；匯入 `:60`；匯出 |
| `public.org_units`（部門） | ✅ 有 | 客戶 | repo `:154`、`:281`、`:630`；匯入；匯出 |
| `public.users`（使用者） | ✅ 有 | 客戶＋本人 | repo `:595`；匯入 `:21`、`:120` |
| `compliance.job_executions`（任務） | ✅ 有 | **只分客戶** | 幾乎每個方法 |
| `compliance.workflow_executions`（流程實例） | ✅ 有 | 只分客戶 | repo `:210`、`:251`、`:373` |
| `compliance.workflow_templates`（流程範本） | ✅ 有 | 客戶；讀取另外放行系統範本與上層共享範本 | repo `:379`、`:436`、`:962` |
| `compliance.projects`（專案） | ✅ 有 | 客戶 | repo `:90`、`:589`、`:677` |
| `compliance.task_assignees`（任務指派） | ✅ 有 | 跟隨專案（專案再依客戶） | repo `:137`、`:301`、`:339`、`:600` |
| `compliance.project_audit_rounds`（稽核輪次） | ✅ 有 | 跟隨專案 | repo `:346`、`:986` |
| `config.job_execution_detection_tools`／`_agents`（任務綁的檢測工具） | ✅ 有 | 客戶 | repo `:1159`、`:1174` |
| `compliance.job_evidences`（任務佐證連結） | ❌ **沒有** | — | repo `:181`、`:259`、`:857`、`:885`、`:900` |
| `compliance.job_execution_devices`／`_org_units`／`_surveys`／`_comments`（任務關聯表） | ❌ **沒有** | — | repo `:154`、`:166`、`:626`～`:836` |
| `compliance.workflow_templates_trans`（範本翻譯） | ❌ **沒有** | — | repo `:524`、`:532`（**寫入**） |
| `public.workflow_execution_control_mapping`（流程↔控制項對位） | ❌ 沒有 | — | repo `:986`；匯入 `:111`；匯出 |
| `oscal.catalog_control_parts` 等 oscal 目錄表 | ❌ 沒有 | — | repo `:90`、`:106`、`:677` |
| `config.detection_tools`（工具清單，全系統共用） | ❌ 沒有 | — | repo `:1159` |

**讀這張表的方法**：沒有隔離的表，只要**每一條查詢都 JOIN 到一張有隔離的表，或它的鍵值是從有隔離的查詢拿來的**，就不會漏。第 6 節逐條確認過這個前提。

---

## 6. 卡片點名要追的問題

### 6.1 第 190 行 `session.query(Survey).filter(Survey.uid.in_(ref_ids))`：`ref_ids` 從哪來？：排除

- `ref_ids` 取自任務佐證連結表的 `ref_id`。
- 這個欄位全專案**只有一個寫入點**：`add_survey_evidence`（`:890`），寫入的值是 `create_snapshot()` **在伺服器端新產生**的問卷快照編號（`survey_handler.py:191-195`）。
- 前端傳的是「來源問卷」的編號，會先經過問卷套件的 `get_by_uid` 讀一次（受問卷表的客戶隔離），再複製一份快照。
- **所以前端沒有辦法把任意問卷編號直接塞進 `ref_id`**，「把別家問卷拼進自己的任務」這個形狀不成立。
- 就算佐證連結表沒有隔離，第 190 行查的是問卷表，也會被客戶隔離擋掉。

同一家公司內，拿別的專案的問卷來建快照是可以的，但問卷本來就是客戶層級的共用題庫，**這是設計，不是漏洞**。

### 6.2 第 912、1090 行讀填答結果：有沒有綁定「這個任務」？：排除

- `_batch_fetch_task_surveys`（`:1072`）用 `TaskSurvey.task_id.in_(job_ids)` 過濾，**有綁定任務**。
- `has_survey_answers`（`:911`）用快照查所有填答。快照是每個任務各自複製一份，而且傳進來的快照編號是從 `get_job_survey_evidences(job_uid)` 拿到的（`survey_handler.py:167`），**也綁定在這個任務上**。
- 兩張表都有隔離（第 5 節）。

### 6.3 沒有隔離的表，有沒有哪條查詢「只查那張表」而漏掉？：排除，但記一個前提

逐條看過，每一條都 JOIN 到有隔離的表，或者鍵值是從有隔離的查詢拿來的：

- `list_jobs` 在沒帶專案編號時，會退回從 oscal 目錄表開始查（`:106`），但最後一定 JOIN 流程實例（有隔離）。**網址一定帶專案編號**，所以這條舊路其實走不到。
- 🔶 **前提要記住**：範本翻譯表**沒有隔離**，而 `_write_template_xml`（`:502`）會**直接寫入**它，只靠 `WHERE workflow_template_id = :tid`。目前所有呼叫端的 `tid` 都來自有隔離的查詢（任務本身的 `template_id`，或 `get_template_context_by_uid` 的結果）。DEV 22:38 實查：41,519 筆任務的範本編號與流程實例的範本**全部一致**；範本全部是「客戶自有」，而且與流程實例屬於同一客戶。**所以現在安全**。但哪天有人新增一個「前端直接帶範本編號」的入口，翻譯表就擋不住了。這一條不開卡，只記在這裡給之後改這支的人。

### 6.4 開工前四件事

| 項目 | 結果 |
|---|---|
| ① 權限檢查次數 vs 對外方法數 | 三支檔的權限檢查次數都是 0，方法數分別是 19／7／2。**這是資料存取層，本來就該是 0**。真正的檢查在 `job_service.py`（改、刪、新增有；**看詳細、看清單沒有**）與 `job_import_service.py`（匯出、確認有；**驗證、重新驗證沒有**） |
| ② 同一套接線複製多份、某一份漏了 | 同一個「任務」有兩組入口：規劃頁單筆（`job_route.py`）和 Excel 批次（套件的 `job_import_route.py`）。**兩組都漏了同一件事**：沒核對任務或計畫屬於網址上的專案。另外，兩組裡的「讀」都沒守 |
| ③ 套件以為宿主守、宿主以為套件守 | **命中一處**：`jedi-task-platform` 的匯入匯出網址只掛「登入＋授權模組」，把權限交給宿主；**宿主只守了四個入口中的兩個**（W4-R2）。另外，DI 有把守門零件注入進去（`flow_control_containers.py:422`、`:439`），所以 `_require_manager` 開頭那條「零件沒給就放行」在正式環境走不到，**排除** |
| ④ AI 儀表板那條路 | `di_containers/dashboard_apis/` 沒有申報這三支或任務服務的任何查詢，**不適用** |

---

## 7. 這份結果可信到什麼程度

**「這幾條存在嗎」：W4-1～W4-6 很可信。**

- 三人面板的 18 票全部判定成立。
- W4-1～W4-4 三票一致。
- W4-5、W4-6 有一票給比較低的嚴重度。
- runner 開檔逐條核對過，行號都對得上。

**但全部是讀程式推論，沒有實際打 API 驗證。** 第 5 節的資料庫隔離狀態，以及 4.7、6.3 的資料分布，是在 DEV 唯讀實查的（附時間），沒有寫入任何資料。

**「只有這幾條嗎」：不保證。** 這次用的是最快的檔位（effort low）。W4-R1、W4-R2 是工具沒報、runner 自己追出來的。任務平台其他還沒掃的檔（`job_batch_complete_service.py` 等，見盤點檔第 131 行）不在本棒範圍內。

---

## 8. 執行概況（數字，給工程師看）

| 項目 | 數值 |
|---|---|
| 掃描範圍 | 3 檔／1,629 行 |
| 基準 commit | `9abe5e64e895`（工作區有平行 session 的改動） |
| 檔位 | effort low，只看正式程式碼（因此多跑一輪密鑰專項） |
| 研究員 | 派 2 支，回 2 支 |
| 原始候選 | 6 條，去重後 6 條 |
| 投票 | 三位檢查員 × 6 條 = 18 票，全部投出，沒有漏投 |
| 票型 | 6 條都是 3:0 成立；W4-6 的嚴重度由面板從中降為低（低、低、中） |
| 密鑰專項 | 零發現 |
| 驗證章 | **verified**（`CLAUDE-SECURITY-REVISION-9abe5e64e895-dirty.json`） |
| 工具 run ID | `wf_cc9b4a46-2c3` |
| 耗時 | 約 22 分鐘（20 個 agent，零失敗；其中 1 個回傳空結果，不影響投票） |
| 工具產出的原始報告 | `CLAUDE-SECURITY-20260923-143314/`（沒有進版控） |
| 報告編號對照 | 工具 F1～F6 對應本報告 W4-1～W4-6 |

---

## 9. 待首腦裁決

1. **W4-2 的「凍結被繞過」要不要加進 FR-114 卡 2-7 的驗收項？** 建議要。卡 2-7 目前寫的是「PUT／DELETE 補核對」，沒提到凍結檢查也要改查任務真正所屬的專案。只補核對其實會一起修好，但驗收時要有人刻意測「對方專案已凍結」這個情境。
2. **W4-3（批次匯入確認）要不要併進卡 2-7？** 建議併。同一件事有兩個入口，分開修容易只修好一邊。
3. **W4-5（清單）、W4-6＋W4-R2（匯出與匯入驗證）要開新卡，還是併進卡 2-7？** 這幾條是同一種病（只看網址上的專案、不核對資源歸屬），但入口在另外兩支檔。建議開一張「任務匯入匯出四入口＋清單補檢查」的新卡，和卡 2-7 放在同一批。
4. **總表登記**：建議新增 4 項，分別是 W4-3、W4-5、W4-6、W4-R2；第 63 項的說明補上「凍結被繞過」和「批次匯入是第二個入口」；W4-R1 記進第 63 項的附註，不另計。
