# FR-088.H4 掃描報告：流程執行宿主服務與接線（主專案 flow_engine）

- **卡片**：[CM-1674](https://app.notion.com/p/FR-088-5-flow-engine-21-BE-repo-Opus-1M-low-3d9346da4cd0817db03de1763e5607c0)
- **掃描範圍**：主專案 `app/flow_engine/service/workflow_execution_service.py`（1,559 行）＋ DTO／enums ＋ DI 接線 ＋ domain／infra 的 ext_workflow_execution 與 control_mapping，共 **21 檔**
- **版本**：commit `8fc8c1c1`（BE repo，branch `feature/FR-075`，工作目錄有未提交異動故 stamp 標 `-dirty`）
- **日期**：2026-09-13
- **工具**：claude-security plugin，`low` effort，focus `attack-surface`
- **面板**：9 條候選 × 3 位檢查員 = **27 票全數投出**，8 條成立、1 條被三票否決，stamp `verified`
- **run ID**：`wf_ba3ef1cb-e96`

---

## 1. 🔴 一句話結論

**流程討論區的門沒鎖：任何一個能登入的帳號，只要知道某個流程的編號，就能把別家客戶（或自己已經被移出的專案）的稽核討論整串讀走，還能用自己的名字往裡面貼話，貼完立刻推播給該專案所有線上的人。**

第二件事比較細但同樣真實：**專案裡「只給看、不給動」的角色，其實可以把別人的稽核任務標成完成、或把流程退回上一關**——系統只檢查「你是不是這個專案的人」，沒檢查「這件事是不是你的」。走批次那條路會被擋，走單筆這條路不會，兩條路的規矩不一樣。

另外掃出**六條密碼與金鑰被寫進版控檔案**的問題。這六條**不在這一棒的範圍內**（在 `docs/` 與 `scripts/` 下），是工具的密鑰專項掃出來的，本報告照實列出但**不計入本棒範圍發現**，請與既有的憑證卡比對是否重複。其中我逐把比對過本機 `.env`：**Google API key 與 Google OAuth client secret 至今未換、仍是活的**。

---

**現況（以 `docs/security-report/M06*.md` 為準）**：H4-1（討論區，M06-2）→ ✅ 已修（CM-2035）；H4-2（完成／退回，M06-7）→ ✅ 已修（FR-114.1-1／FR-114.1-2，CM-2119）；範本 XML 寫回母版（M06-15）→ 🚫 裁定不修；H4-3（資料庫隔離開關）→ ✅ 已修：`projects`／`workflow_executions` 隔離已於 2026-09-14 開啟（FR-094 CM-1768／1772，內化版 M20-1）；AI 儀表板列專案把呼叫者當管理員那一半另由 M15-3（SUMMARY #61）修好，均隨 1.21.0 出貨。範圍外的憑證項（CM-1607／1608／1629／1631 系列）：檔案殘留已清（CM-2048／2049／2051），密碼換發排在正式環境上版前。

## 2. 這一棒在檢查什麼（白話）

稽核流程「跑起來」的所有事情，都在主專案這支 1,559 行的程式裡：開始一個稽核流程、幫每個關卡建任務、任務完成後決定下一個換誰、寄通知、把任務跟控制項對起來。套件只提供零件，真正的規則寫在這裡。

這一棒要回答三個問題：

- **守門有沒有每條路都包住**——會不會有某個入口忘了檢查「你是不是這個專案的人」
- **給 AI 儀表板用的三支查詢，會不會漏掉「只能看自己租戶」的過濾**
- **通知信裡會不會帶出不該帶的內容、會不會寄給別家客戶的人**

**只找問題、不修問題**——底下所有修法都只是建議，這一棒沒有改任何一行程式碼，也沒有連任何資料庫、沒有真的打任何一支 API 去測，全部是讀程式碼推論出來的。

---

## 3. 掃到什麼：總覽表

### 3.1 本棒範圍內（21 檔）

| # | 這是什麼問題 | 出事會怎樣 | 要先有什麼才打得到 | 在哪裡 | 嚴重度 | 來源 |
|---|---|---|---|---|---|---|
| **H4-1** | **流程討論區完全沒有檢查「你是不是這個專案的人」** | 任何登入者能讀走整串稽核討論、並用自己名字貼假訊息，貼完即時推播給該專案所有線上成員 | 任何有效帳號 ＋ 知道一個流程編號 | `api/flow_engine/routes/flow_engine_route.py:105`（GET 在 `:55` 同樣沒守門） | **MEDIUM** | 工具 3/3 票 ＋ 本棒開檔核對 |
| **H4-2** | **完成／退回單筆任務只檢查「是不是專案成員」，沒檢查「是不是你的任務」** | 定義為「只能看」的 viewer 也能把別人的任務標完成、把流程退回上一關，稽核紀錄上的經手人變成他 | 是該專案的參與者（任何角色）＋ 知道流程編號與任務編號（一般列表 API 就拿得到） | `app/flow_engine/service/workflow_execution_service.py:655`（`complete_job`）、`:805`（`revert_job`） | **MEDIUM** | 工具 3/3 票 ＋ 本棒開檔核對 |
| **H4-3** | **資料庫沒開「每個客戶只能看自己資料」的隔離**，但程式有一條路刻意信任它 | AI 儀表板列專案清單那條路會撈到別家客戶的專案 | 多租戶部署下的任何登入者 ＋ 走到那條路 | `scripts/sql/2026-06-25-align-poc-to-stg-view-eventtrigger-projects-rls.sql:124`；配套 `app/flow_control/service/project_service.py:224` | **MEDIUM** | 工具 3/3 票 ＋ 本棒開檔核對（**僅核對版控內的 SQL，沒查線上資料庫**） |

### 3.2 範圍外（密鑰專項掃出來的，不計入本棒發現）

工具設了 focus 就會多跑一次密鑰專項，它讀的是整棵樹，所以以下六條都落在 `docs/` 與 `scripts/`，**這一棒的 21 檔沒有涵蓋這些區域**。列出來是因為是活的憑證，**請先與既有憑證卡（CM-1607／1608／1629／1631）比對是否重複**。

| # | 這是什麼問題 | 出事會怎樣 | 在哪裡 | 嚴重度 |
|---|---|---|---|---|
| **外-1** | OpenAI／Anthropic／Google 的 API 金鑰、Google 登入用的密鑰被整份 `.env` 抄進對話紀錄檔 | 別人可以拿我們的帳號去用 AI 服務（帳單算我們的）、冒用我們的 Google 登入身分 | `docs/conversation-history/2026-04-28-to-04-30-survey-answer-arc/part-verbatim-01-of-03.md:3651` | **HIGH** |
| **外-2** | GitLab 權杖、Gmail 寄信密碼、MinIO 金鑰、公司網域帳號密碼被整張 DB 表抄進對話紀錄檔 | 別人能讀我們的原始碼倉庫、用我們的名義寄信（拿去釣魚很像真的）、開我們的檔案儲存、查公司通訊錄 | `docs/conversation-history/2026-04-28-to-04-30-survey-answer-arc/part-verbatim-03-of-03.md:5933` | **HIGH** |
| **外-3** | **登入用的簽章金鑰**被抄進對話紀錄檔 | 知道這把金鑰就能自己做一張假的登入通行證，冒充任何人（含管理員），等於完全繞過登入 | `docs/conversation-history/2026-04-21-google-drive-integration/2026-04-21-google-drive-integration-part-03-of-14.md:14232` | **HIGH** |
| **外-4** | 資料庫密碼寫死在 **249 個** 版控檔案裡 | 拿到 repo＋連得到內網的人，能用「可以看所有客戶資料」的那個帳號直接連資料庫 | `docs/claude/memory/project_drive_sync_test_data.md:18` 等 249 檔 | **MEDIUM** |
| **外-5** | 管理員密碼寫死成 seed 腳本的預設值 | 沒改過密碼的環境會被用這組直接登入成平台管理員 | `scripts/seed_2026-08-01_fr059_detection_profiles.py:65` 等 5 檔 | **LOW** |

（外-1～外-3 是同一類問題的三個不同檔案，**都是「把 `.env` 或 DB 查詢結果整段貼進對話紀錄」造成的**，根因相同。）

---

## 4. 每條發現的詳述（本棒範圍內）

### H4-1（MEDIUM）流程討論區沒有任何守門

**這是什麼問題（白話）**

每個稽核流程底下有一個討論區，成員在裡面留言討論。讀留言與寫留言走同一條網址、同一個程式檔。

問題是——**這兩個動作都只檢查「你有沒有登入」，從來沒檢查「你是不是這個專案的人」。**

你拿自己的帳號，報上別人流程的編號，它就把整串討論給你；你想貼話，它也讓你貼，而且貼完會立刻透過即時推播（Socket.IO）送到該專案所有正在線上的成員畫面上。

**出事會怎樣**

- 稽核討論內容通常是最敏感的——誰質疑了什麼、哪個控制項有問題、內部怎麼喬——整串被外人讀走
- 攻擊者能以**自己的真實姓名**貼話進去（留言會帶 `user_nickname` 與 `user_name`），對該專案成員來說看起來就是「有個不認識的人突然在我們的討論區發言」，或更糟：貼假的稽核結論混淆判斷
- 因為 `compliance.workflow_executions` 這張表**也沒開客戶隔離**（見 H4-3），資料庫層面連「跨租戶」都擋不住

**要先有什麼才打得到**

- 一組有效的登入帳號（**任何**已登入帳號都行，不需要是該專案的人；**已經被移出專案的前成員也算**）
- 知道目標流程的編號（uid）。取得方式：曾經是該專案成員時記下來、從分享的網址或截圖、或從其他 API 回應外洩

**在哪裡**

```
api/flow_engine/routes/flow_engine_route.py:55    ← GET（讀留言），只有 @jwt_required()
api/flow_engine/routes/flow_engine_route.py:105   ← PUT（寫留言），只有 @jwt_required()
app/flow_engine/service/workflow_execution_service.py:1181  ← get_workflow_execution()，本身不做任何授權
```

**本棒開檔核對結果：屬實。** 我逐行讀過 `flow_engine_route.py:55-122` 這整個類別，兩個 verb 都是「拿網址上的 `id` → `get_workflow_execution()` → 直接讀寫留言」，中間沒有任何一行呼叫 `assert_project_participant`。對照同一支檔案的 `complete_job`（`:123` 起）與 service 層的 `:655`，那條路**有**這道守門且註解寫明「C16：非該專案參與者不可 complete job（擋跨專案猜 id）」——**同一個風險已經被想過、被擋過，只是留言這條路漏了。**

**怎麼修**

在 GET 與 PUT 兩個 verb 解出 `workflow_execution` 之後、動留言之前，補上與 `complete_job` 相同的那一行：

```python
workflow_execution_service.assert_project_participant(workflow_execution.id)
```

（`assert_project_participant` 是既有的 public method，在 `app/flow_engine/service/workflow_execution_service.py:122`，docstring 明寫「public — 供其他 service 重用同一守門」，**不需要另外寫新的**。）非參與者會被既有的 `GRC_NOT_PROJECT_PARTICIPANT` 擋成 403。

比較乾淨的作法是把留言讀寫下沉到 app service 裡再守門，這樣 route 層也不用碰 session；但若要最小改動，補這一行就能關掉這個洞。

---

### H4-2（MEDIUM）viewer 也能完成或退回別人的任務

**這是什麼問題（白話）**

專案裡的人有不同角色，其中 `viewer` 的定義是「只能看，沒有待辦」。

但完成任務這個動作，**只檢查「你是不是這個專案的成員」，沒有再檢查「這件任務是不是指派給你的」**。所以 viewer 可以把別人還沒做的稽核任務標成「已完成」，流程就往下一關跑了；退回任務（`revert_job`）是同一道守門，一樣可以。

**出事會怎樣**

- 稽核流程的**可歸責性壞掉**：任務完成紀錄上的經手人（`updated_user`）寫的是攻擊者，但這個人本來沒有執行該任務的資格。事後追查會看到一筆「某某人完成了不該他做的事」，而系統當時是允許的
- 連帶改寫 job 狀態、問卷狀態（`TaskSurveyStatusCode.EDITING`）與整個 workflow 狀態
- 可以把已完成的關卡**退回**，干擾正在進行的稽核

**要先有什麼才打得到**

- 攻擊者是該專案的參與者，**角色不拘**（viewer／auditor／reviewer 都行，不必是 manager，也不必是該任務的指派人）
- 知道流程編號與 BPMN 任務編號——同專案成員從一般的列表 API 就正常取得

**為什麼是 MEDIUM 不是 HIGH**：攻擊者必須已經是這個專案的成員（外人打不到），影響限於該專案內部的流程完整性，不是資料外洩。

**在哪裡**

```
app/flow_engine/service/workflow_execution_service.py:655   ← complete_job，守門只有 assert_project_participant
app/flow_engine/service/workflow_execution_service.py:805   ← revert_job，同一道守門
common/authz/workflow.py:28-49   ← 該守門的政策：任一角色放行、只擋非參與者
api/flow_engine/routes/flow_engine_route.py:136-146  ← 參數來自 client payload
```

**本棒開檔核對結果：屬實。** `common/authz/workflow.py:46-49` 的邏輯是 `role = role_service.get_user_role(...)`，然後 **`if role is None: raise`**——只要查得到任何角色就放行，完全不看角色是什麼。`complete_job` 在 `:655` 呼叫它之後，下一段（`:669-673`）就直接把狀態寫成 COMPLETED，中間沒有第二道檢查。

工具提到「走批次端點 `/grc/jobs/batch-complete` 做同一件事會被 `NO_PERMISSION` 擋下」，這一點**我沒有開檔核對**（批次端點不在本棒 21 檔內）。若屬實，代表同一件事兩條路徑規矩不一致，那本身就是該收斂的設計問題；請在開修正卡時一併查證。

**怎麼修**

在 `complete_job` 與 `revert_job` 的 `assert_project_participant` 之後，加一道「是不是你的任務」的檢查：比對呼叫者是該 job 的 task_assignee，或是該專案的 manager，都不是就 raise `ForbiddenError`。可重用既有的 `participant_role_service.get_user_role`（已注入）與 `task_assignee_domain_service`（已注入，`_resolve_project_id_from_workflow` 就在用），**不需要新增 service**。

如果產品**真的**要讓所有參與者都能代為完成，那應該寫成明確的角色白名單（例如「manager／auditor 可代完成，viewer 不行」），而不是現在這種「任一參與者皆可」的隱性放行——後者讓「viewer 是唯讀」這個產品承諾變成假的。**這一條要先問決策者是哪一種意圖，再決定怎麼改。**

---

### H4-3（MEDIUM）資料庫的客戶隔離沒開，但程式有一條路信任它

**這是什麼問題（白話）**

資料庫有一個機制叫 RLS（就是「每個客戶只能看自己資料」的資料庫層隔離）。它要兩件事同時成立才有效：**規則要寫好**，而且**開關要打開**。

現在的狀況是：規則寫好了，開關沒開，甚至有一支 migration 明確把它**關掉**。而程式裡有一條路（AI 儀表板列專案清單）刻意跳過應用層的可見性過濾，註解直接寫「tenant 隔離由 RLS 負責」——它把責任交給一個沒打開的機制。

**出事會怎樣**

B 客戶的使用者呼叫 AI 儀表板的專案列表，拿到 A 客戶的專案清單（名稱、狀態、metadata）。

**要先有什麼才打得到**

- 多租戶部署（單客戶自帶環境不受影響）
- 一個登入帳號，走到那條 `is_admin=True` 的路
- 線上資料庫的狀態與版控內的 SQL 一致

**在哪裡**

```
scripts/sql/2026-06-25-align-poc-to-stg-view-eventtrigger-projects-rls.sql:124
    ALTER TABLE compliance.projects DISABLE ROW LEVEL SECURITY;
scripts/init/02-schema.sql   ← workflow_executions_* 政策寫了 46 處，但整份檔案沒有一行對 projects／workflow_executions 下 ENABLE ROW LEVEL SECURITY
app/flow_control/service/project_service.py:218-225   ← 註解「tenant 隔離由 RLS（session_scope）負責」，並傳 is_admin=True
```

**本棒開檔核對結果：版控內的 SQL 屬實。** 我做了三件核對：① `sed -n '118,130p'` 看到那句 `DISABLE ROW LEVEL SECURITY` 就在第 124 行，註解寫「compliance.projects RLS 以 STG 為主 → 關閉」；② `grep 'ENABLE ROW LEVEL SECURITY' scripts/init/02-schema.sql | grep -iE 'projects|workflow'` **零筆**，同檔卻有 46 處 `workflow_executions_` 政策定義——政策寫了但開關沒開，等於那些規則現在是空轉的；③ `project_service.py:218-225` 的註解與 `is_admin=True` 都在，與工具說的一致。

**🔴 這條的限制要講清楚**：我**沒有查任何一個線上資料庫**（唯讀查詢是允許的，但本棒定位是讀程式碼，且線上狀態可能已被後來的操作改過）。所以這一條成立的是「**版控裡的 SQL 是這個狀態**」，不是「**DEV／STG／POC 現在確實沒開**」。開修正卡前應先跑一次 `SELECT relname, relrowsecurity FROM pg_class WHERE relname IN ('projects','workflow_executions')` 確認三個環境的實況——這屬唯讀，不算環境異動。

**怎麼修**

兩件事要分開做，**都做才算修完**：

1. **把開關打開**：對 `compliance.projects` 重新啟用、對 `compliance.workflow_executions` 與 `compliance.workflow_templates` 首次啟用（它們的政策已經寫好、現在是空轉的），然後用 `pg_class.relrowsecurity` 驗三個環境。⚠️ 打開隔離可能讓某些現在「看得到」的查詢突然看不到，**這是行為變更，要先在 DEV 驗過**。
2. **不要把信任全押在資料庫開關上**：`get_projects_for_dashboard` 應該在查詢裡自己加租戶條件，而不是傳 `is_admin=True` 然後期待資料庫兜底。一支 migration 就能把兜底關掉——這次就是這樣發生的。

---

## 5. 卡片「重點看什麼」的逐項回應

卡片列了八個重點，以下逐項給結論。**證偽與證實同樣重要**，不成立的我寫清楚是什麼擋住了。

| 卡片項目 | 結論 | 說明 |
|---|---|---|
| ① **向 AI 儀表板申報的三支查詢有沒有漏租戶過濾** | ⚠️ **部分成立，但破口不在 `**kwargs`** | 開檔核對：`di_containers/dashboard_apis/flow_engine.py` 申報的三支是 `get_main_workflow_executions` / `get_sub_workflow_executions` / `get_workflow_executions`（`:1157`／`:1162`／`:1176`），**三支都是 `query_entity` 形狀、不是 `**kwargs`**——卡片擔心的「申報單沒宣告就被丟掉」（FR-079 F13 同款）**在這三支身上不成立**。真正的 `**kwargs` 兩支（`:1123`／`:1140`，帶 `_and_pager`）**並未申報給儀表板**。但這三支確實**沒有任何租戶或專案過濾**，而 `workflow_executions` 表的隔離也沒開（見 H4-3），所以**破口是真的、成因不同**：不是參數被丟掉，是從頭到尾就沒有過濾條件，全靠資料庫兜底而兜底沒開。**這一點併入 H4-3 理解，不另計一條。** |
| ② **守門與反查鏈：`known_project_id` 是否都由伺服器端解析** | ✅ **沒問題** | grep 全 repo，非測試的呼叫者只有 `app/flow_engine/service/stage_rollback_service.py:160` 一處。開檔看 `:172-180`：`project_id` 來自 `_get_user_role()`，它拿 `project_uid` 去 `project_domain_service.get_one()` 解出來的，**是伺服器端解析，不是請求可控**。`bypass_round_frozen_gate=True` 同樣只有這一個呼叫者。 |
| ③ **輪次凍結檢查 fail-open** | ⚠️ **屬實，但影響低於預期** | `:155-176` 確實三種情況全放行（沒注入 round service／解析不到 project／無輪次），註解自陳「主 gate 在顯示層」。**但這道 gate 擋的是「規劃期才可退回」的流程時序，不是身分授權**——`assert_project_participant` 那道是獨立的、且不受 bypass 影響（`revert_job` docstring 明寫這點，我核對 `:834-839` 屬實）。所以 fail-open 的後果是「本該凍結的輪次仍可退回」，而**能做這件事的人已經被 H4-2 的守門缺陷涵蓋**。單獨看不值得另開卡，**修 H4-2 時一併把這裡收緊**。 |
| ④ **啟動流程時改寫範本 XML：改的是凍結副本還是母版** | 🔴 **改的是母版** | `:426` 呼叫 `_patch_template_xml_job_uids(workflow_template, ...)`，而 `:436` 直接 `workflow_template_domain_service.update_workflow_template(workflow_template)`——**寫回的是 `workflow_template` 本身，不是流程實例的副本**。意思是每啟動一次流程，就把 job uid 蓋回共用的範本上。**這不是資安漏洞**（寫進去的是系統自己產的 uid，不是使用者輸入），但是**設計上的疑慮**：多個流程共用同一範本時會互相覆蓋（`_inject_job_execution_uid` 會先移除舊值再寫新值，`:463`）。**建議當作功能面 bug 記錄，不列資安發現。** |
| ⑤ **`params: dict` 怎麼進到流程變數** | ✅ **讀過，沒報** | `start_workflow_execution`（`:179`）的 `params` 進到 element_variable 存放，型別是結構化的 dict／JSON，沒有被拿去拼字串或當程式執行。**沒發現問題。** |
| ⑥ **通知內容會不會外洩、會不會寄給別租戶的人** | ✅ **沒問題** | 逐支開檔：`notify_users_batch_assigned`（`:1265`）只帶暱稱與任務「數量」，不帶任務內容；`notify_control_reviewers_on_task_complete`（`:1380`）帶暱稱、任務名稱、系統網址，**不帶留言原文**。收件人來源是 `get_control_participants_with_inherits(project_id=...)` 再過濾 manager／reviewer，**是從專案參與者往下查，不會撈到別專案的人**。⚠️ 附註：`notify_control_reviewers_on_task_complete` 的 docstring 自陳「目前無呼叫點」（CM-1504 退役死分支後成孤兒，刻意保留待 2B 重接）——所以這支現在不會執行。 |
| ⑦ **問卷屬性：指到別租戶問卷 uid 會怎樣** | ⚠️ **讀過，無法在本棒下結論** | `process_survey_property`（`:1466`）從 BPMN 任務屬性取 `survey_uid`，直接餵給 `task_survey_service.link_task_survey(survey_uid=...)`，**本檔內沒有任何驗證**。但 `task_survey_service` **不在本棒 21 檔範圍內**，驗證有沒有在那一層做，我沒有讀到。**這是「沒讀到」不是「沒問題」**，建議列入下一批掃描範圍。 |
| ⑧ **裸的 add／update／delete_workflow_execution 有沒有 route 直達** | ✅ **沒問題** | `:1208`／`:1217`／`:1227` 三支確實沒有守門。但 grep 全 repo（`workflow_execution_service.(add\|update\|delete)_workflow_execution`）**零個外部呼叫者**——沒有任何 route 或其他 service 呼叫它們，目前是死碼。**風險是潛在的**（將來有人接上 route 就立刻變成洞），建議加註解或直接移除，但**不列為本棒發現**。 |
| 補充：**控制項對應表的查詢有無範圍條件** | ⚠️ **讀過，沒報** | `infra/flow_engine/repository/workflow_execution_control_mapping_repo_impl.py` 與相關 domain service 讀過，查詢依 workflow_execution_id／control_id 過濾，**沒有租戶條件**——與 H4-3 同一個病（靠資料庫兜底），不另計。`ext_workflow_executions` 表的隔離狀態**沒有查線上資料庫**（同 H4-3 的限制）。 |

---

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

分兩層看，這兩層的可信度**不一樣**：

### 第一層：「這幾條存在嗎」→ 可信度高

- 面板（就是**三個獨立的檢查員各自從零讀程式碼再投票**）完整跑完，沒有中斷、沒有撞額度：9 條候選 × 3 位檢查員 = **27 票全數投出**，沒有漏投
- 8 條**三票全過**、1 條**三票全否**（被否的那條沒有列進報告，所以編號會跳過 F9）
- 本棒範圍內的三條（H4-1／H4-2／H4-3），我**自己開檔逐行核對過**，不是只看工具怎麼說——核對過程寫在各條的「本棒開檔核對結果」段
- H4-3 有一個明確的**打折**：只核對了版控裡的 SQL，**沒有查任何線上資料庫**，所以它成立的是「程式碼與 migration 是這個狀態」，不是「DEV／STG／POC 現在確實沒開」

### 第二層：「只有這幾條嗎」→ 不能這樣說

三個理由：

1. **`low` effort 是一位研究員讀完全範圍**，不是逐檔窮舉。1,559 行的單檔是本 arc 最大的一棒，一次讀完必然有取捨
2. **卡片重點⑦（問卷 uid 跨租戶）我沒讀到答案**——驗證邏輯在範圍外的 `task_survey_service`，這是「沒讀到」不是「乾淨」
3. **零發現不等於乾淨**。本報告第 5 節刻意把「讀過但沒報」（⑤⑥）與「根本沒讀到」（⑦）分開寫，就是為了不讓後者被誤當成前者

另外，**範圍外那六條憑證發現（第 3.2 節）雖然是真的，但不代表那些區域被掃過**——密鑰專項只找符合金鑰特徵的字串，不做其他分析。

---

## 7. 執行概況（工程師看的，數字照 stamp 實抄）

| 項目 | 數值 |
|---|---|
| `verification.status` | **`verified`** |
| 候選數（candidates） | 9（去重後仍 9） |
| 面板票數（panel_votes） | **27**（9 × 3） |
| 達到共識的發現（panel_quorum_findings） | 8（全部 3/3 一致，無 2/3 勉強過關者） |
| 研究員派出／回報（researchers_dispatched / returned） | **2 / 2**（1 位範圍研究員 ＋ 1 次密鑰專項） |
| 驗證輪數（verificationRun） | 1（沒有候選被交棒到第二輪，`continued: 0`、`lostCandidates: []`） |
| 嚴重度被面板下修的 | 0 筆（`severityLowered: []`） |
| agent 總數 | 29 派出 / 29 完成 / 0 失敗 |
| 掃描耗時 | 約 4 小時 51 分（17,469,602 ms） |
| scope 檔數 | 21（啟動前 `git ls-files` 驗過，對上卡片） |
| run 產出目錄 | `CLAUDE-SECURITY-20260913-064104/`（stamp：`CLAUDE-SECURITY-REVISION-8fc8c1c1dd45-dirty.json`） |
| run ID | `wf_ba3ef1cb-e96` |

**沒有執行任何程式碼、沒有連任何資料庫、沒有打任何 API、沒有驗證任何攻擊手法**——所有發現都是讀程式碼推論出來的。

---

## 8. 建議怎麼開卡

**本棒範圍內（三張）**：

1. **H4-1 流程討論區補守門**——單檔兩行，改法明確，可直接派 runner。建議優先，因為它是三條裡唯一「外人打得到」的。
2. **H4-2 完成／退回任務補「是不是你的任務」檢查**——**要先問決策者意圖**（是漏了，還是產品刻意讓所有參與者代為完成？），確定方向再動手。修的時候把重點③的 fail-open 一併收緊。
3. **H4-3 資料庫隔離開關**——**開卡前先唯讀查三個環境的實況**。這條牽涉 DB 行為變更，且修法分兩半（開開關＋程式不再依賴開關），建議寫成一張卡兩個子項。

**功能面（非資安，一張）**：重點④範本 XML 寫回母版而非副本，多流程共用時會互相覆蓋。

**要補掃的（記入覆蓋率缺口，不開修正卡）**：重點⑦問卷 uid 的跨租戶驗證在 `task_survey_service`，本棒沒讀到，列入下一批範圍。

**範圍外六條**：先與 CM-1607／1608／1629／1631 比對；未重複的部分，**外-1 的 Google API key 與 Google OAuth client secret 經逐把比對確認至今未輪替、仍是活的**，這件事本身就該先處理，不必等開卡流程。
