---
title: FR-088 流程引擎（jedi-flow-engine）資安掃描 — 收口 SUMMARY
status: ✅ 六棒全部掃完並驗收（2026-09-14）；範圍內 12 條發現已登記跨 arc 總表；已全數處理（M06 23 件已修隨 1.21.0 出貨、1 件裁定刪除）
relates: [FR-075, FR-079, FR-083, FR-086, FR-087, FR-094]
---

# FR-088 流程引擎（jedi-flow-engine）資安掃描 — 收口 SUMMARY

> **母卡 CM-1669** ｜ 收口日 2026-09-14 ｜ 六棒 CM-1670～1675，只掃不修
> 沿革（每棒 commits／決策／教訓）見 `../../FR-075-2609-jedi-package-security-audit/handoff/security-scan-LOG.md` 與 `git log --grep='FR-088'`；此檔只寫終局。

## 1. 一句話結論

**流程引擎的套件本體加上主專案接它的那半邊，共 157 支程式檔分六棒掃完，範圍內確認 12 條問題（登記為跨 arc 總表 11 條資安＋4 條非資安真 bug，算法見下），最嚴重的三件是：① 任何登入者知道一個任務編號就能讀走別家客戶的稽核證明清單（第 49 條）；② 一張惡意流程圖能讓整個系統對所有客戶停止回應（第 55／57 條，兩條同一組病）；③ 流程留言不檢查你是不是這個專案的人，可以假冒同事發訊息釣魚（第 51 條）。修正卡本線不開：程式層「這筆是不是你的」那批併進 FR-086 由該側首腦開卡，資料庫層隔離歸 FR-094 八棒接。**

「12 條」的算法：以每棒 runner 報的範圍內條數加總（3＋4＋2＋2＋1＋0），對應跨 arc 總表 §3.1 第 49～59（11 列，H1 的一列是兩位研究員各報一次的同一個洞）；非資安 bug 另登記 §3.2 第 13～16（4 條）。各處數字口徑不一之處見本檔末段「數字對帳備註」。

## 2. 這個 arc 在檢查什麼

- **流程引擎在產品裡做什麼**：每個稽核專案都照一張「流程圖」（BPMN 格式的 XML 文字檔）在跑——規劃 → 稽核 → 改善 → 結案。誰能按「推進」、按了之後系統要建什麼任務、任務完成後換誰、留言與證明文件存哪裡，全由這個引擎決定。
- **為什麼要掃**：它從沒掃過，而且同時有兩種風險密度最高的東西——「讀 XML」（歷來有讀本機檔案、撐爆記憶體的老問題）與「授權缺口」（網址只帶一個編號，後端要主動反查才知道是不是你的專案）。跨 arc 總表排它為下一支第一名。
- **套件與宿主怎麼切**：套件（jedi monorepo 的 `jedi-flow-engine/`，72 檔）是「零件供應商」，自己沒有任何對外網址；主專案（宿主，92 檔）接線後才有 19 條真正的 API 入口，**所以「有沒有檢查權限」這件事全在宿主那半邊**。六棒按業務功能垂直切（任務／階段／流程圖解析／範本管理／執行宿主／執行核心），不按程式層切，讓每棒的問題能一次看完。

## 3. 六棒總表

「面板」＝工具派出的三個獨立檢查員對每條候選各投一票（不是儀表板）；「`verified`」＝工具自己蓋的章，代表每條候選都投完、沒有漏投；「範圍外」＝工具的密鑰專項順手掃全 repo 撈到的，不在該棒檔案清單內。

| 棒 | 卡 | 檔數 | 面板票數與狀態 | 範圍內發現 | 首腦改判 | 報告 |
|---|---|---:|---|---|---|---|
| 1 H2 任務完成／回退／留言／證明 | CM-1670 | 23 | 候選 11 去重 10，三人各投一票共 30 票全投出，`verified` | **3 條**（HIGH 1／MEDIUM 2；其中第 51 條後被裁升 HIGH） | 無改判；人工補一條「流程定義 GET 任何登入者可讀」併 P1；證偽卡片一項（`is_admin` 是平台超管不是租戶管理員） | [`../scan-H2-task-comment-evidence.md`](../scan-H2-task-comment-evidence.md) |
| 2 H1 稽核階段推進與回退 | CM-1671 | 21 | 候選 12 去重 11，33 票全投出，`verified` | **4 條**（MEDIUM 3／LOW 1） | 無改判；卡片兩大疑點一個「讀成立、寫不成立」、一個 0:3 否決 | [`../scan-H1-stage-advance-rollback.md`](../scan-H1-stage-advance-rollback.md) |
| 3 P1 流程圖範本與 BPMN 解析 | CM-1672 | 30 | 候選 4 去重 3，9 票全投出且每條 3:0，`verified` | **2 條**（MEDIUM 2；第 55 條後被裁升 HIGH） | runner 報 3 條，**F2（套件庫走明文 http）改判範圍外＋重複 CM-1634** | [`../scan-P1-bpmn-parsing-and-templates.md`](../scan-P1-bpmn-parsing-and-templates.md) |
| 4 H3 流程範本管理 | CM-1673 | 13 | 候選 10，30 票全投出且每條 3:0，5 條被檢查員降級，`verified` | **2 條**（MEDIUM 1／LOW 1；第 57 條後被裁回 HIGH） | 無改判；**runner 沒交付報告，首腦補寫** | [`../scan-H3-flow-template-management.md`](../scan-H3-flow-template-management.md) |
| 5 H4 流程執行宿主服務與接線 | CM-1674 | 21 | 候選 9，27 票全投出（8 條 3:0、1 條 0:3），`verified` | runner 報 3 條 → **1 條新**（MEDIUM） | **2 條改判重複**：留言零守門（＝第 51 條且檔案不在 scope）、`projects` 表隔離關（三處已記） | [`../scan-H4-execution-host-service.md`](../scan-H4-execution-host-service.md) |
| 6 P2 流程執行核心 | CM-1675 | 49 | 候選 3 去重 2，6 票全投出（1 條 3:0、1 條 0:3），`verified` | **0 條新**（runner 報 1，自標重複第 51 條） | 同意 runner；最有價值的是「套件公開方法 vs 宿主守門」對照表，帶出 §3.2 第 16 未來陷阱 | [`../scan-P2-execution-core.md`](../scan-P2-execution-core.md) |
| **合計** | | **157** | **135 票** | **12 條** | | |

每棒驗收的固定步驟（六棒一致）：① 核工具蓋的章與票數，對照 runner 自述；② 範圍內每條首腦自己開檔核對行號，需要時到 DEV 資料庫唯讀實查（隔離開關、規則數、筆數）；③ 越界檢查——runner 報的每條對照 scope 檔案清單與跨 arc 總表既有條目，越界或重複的改判、不另計；④ 核「交付四件」（報告／commit／Notion 回寫含 hash 與 run ID／狀態）；⑤ 跑 `git diff <掃描版本> HEAD -- <下一棒 scope>` 看平行改動有沒有咬到下一棒。

檔數說明：母卡標題寫「150 檔」是開卡時的數字；P2 開掃前因平行重構（`plugin.py` 拆成五檔、新長出 `api/`／`domain/ports.py`）由 42 重算為 49，六棒實際合計 157。

## 4. 12 條發現一覽

嚴重度以決策者 2026-09-13 裁定後為準；標 **裁升** 的是檢查員原評 MEDIUM、決策者改 HIGH。「歸屬」是修正卡的落點。行號以跨 arc 總表登記為準（各棒掃描期間程式有漂移，同一支函式在不同報告的行號不同，見末段備註）。完整的「出事會怎樣／怎麼修」在總表與各棒報告，這裡一條一段只寫到能判斷急不急。

### 4.1 資安類（跨 arc 總表 §3.1 第 49～59）

**第 49 條｜HIGH｜併 FR-086 程式層那批**
- **現況**：✅ 已修（M06-1，CM-2059）
- 問題：任何登入者只要知道一個任務編號，就能讀走別家客戶的稽核證明清單（檔名、描述、雲端硬碟連結、上傳者帳號、檔案指紋；拿到檔案編號還能接第 34 條下載本體）。
- 為什麼 HIGH：只要登入＋一個編號，不需任何權限；同一支檔的新增／更新／刪除都有檢查、只有「讀」漏掉，是漏寫不是設計；資料庫層零兜底（表的隔離關、沒有客戶欄位）。
- 在哪：`app/flow_engine/service/job_evidence_service.py:42-59`，修法是 `:48` 之後補一行既有的 `assert_project_participant(...)`。

**第 50 條｜MEDIUM｜併 FR-086 程式層那批（與 49 必須同卡）**
- **現況**：✅ 已修（隨 M06-1，CM-2059）
- 問題：同一支服務「查單一筆證明」也沒檢查歸屬，目前靠一個序列化 bug 擋住（route 把單筆餵進「多筆」序列化器、回傳前先炸）。
- 為什麼 MEDIUM：守門缺口是真的、外洩目前不成立；**那個 bug 一修好缺口就變真外洩**，所以兩者要同卡。
- 在哪：同檔 `:61-70`；route `api/flow_engine/routes/job_evidence_route.py:70-71`。

**第 51 條｜HIGH（裁升）｜併 FR-086 程式層那批**
- **現況**：✅ 已修（M06-2，CM-2035）
- 問題：流程留言的讀與寫只驗登入，任何登入者可讀走別家客戶的稽核討論、用自己名義塞留言，留言還會即時推播給該專案成員——可假冒同事發訊息釣魚。留言存 JSON 陣列無筆數與長度上限。
- 為什麼裁升：檢查員原評 MEDIUM（前提與 49 相同、外洩的是留言不是證明本體），決策者以「可假冒同事釣魚」裁 HIGH。P2 從套件側證實套件層也一行檢查都沒有，守門只能加在主專案。
- 在哪：`api/flow_engine/routes/flow_engine_route.py:44-64`（讀）／`:66-121`（寫）；套件 `element_variable_service.py:26/:50`。

**第 52 條｜MEDIUM｜併 FR-086 程式層那批**
- **現況**：✅ 已修（M06-5，CM-2035）
- 問題：任何登入者知道一個輪次編號，就能讀走別家客戶的稽核階段歷程——每次推進／退回的操作者、從哪階段到哪階段、退回理由全文（管理者自由填寫，通常直接寫明稽核哪裡出問題）。網址上的專案編號從頭到尾沒被用。
- 為什麼 MEDIUM：前提需取得輪次編號（亂數 UUID，猜不出來）；但外洩的是稽核判斷的敘事文字，總表註明可視需求升 HIGH。
- 在哪：`api/flow_engine/routes/stage_rollback_route.py:52-67`；下游 jedi-compliance-audit `audit_round_app_service.py:867-887` 只查輪次存不存在。

**第 53 條｜MEDIUM｜併 FR-086 程式層那批（附規劃相依）**
- **現況**：✅ 已修（M06-6，CM-2035）
- 問題：「查目前階段」用你填的專案編號查你的角色、用你填的輪次編號撈資料，兩者不核對；角色不符也不擋，只把「可否推進」標 false 照樣回整包（含流程執行編號，那是另一批 API 的鑰匙）。
- 連帶：「推進」「退回」兩支同樣寫法，目前靠下游套件用輪次自己的專案再查一次角色擋住（七條寫入路徑首腦逐一核對）——**修時一併補，讓本層自己站得住**。
- 在哪：`app/flow_engine/service/stage_advance_service.py:84/:118/:120`；連帶 `:224` 與 `stage_rollback_service.py:95`。

**第 54 條｜LOW｜一行修法，隨 53 同批順手改**
- **現況**：✅ 已修（M06-12，CM-2059）
- 問題：推進／退回的操作者顯示名稱可由呼叫端自填（程式用「沒填才補真名」的寫法）；真實帳號欄由伺服器填不受影響，只影響畫面顯示。
- 為什麼 LOW：攻擊者本來就要有權推進，是「自己人竄署名」不是外人打得到的洞。
- 在哪：`api/flow_engine/routes/stage_advance_route.py:59`、`stage_rollback_route.py:40`。

**第 55 條｜HIGH（裁升）｜與 57 合一張「BPMN 輸入強固化」卡**
- **現況**：✅ 已修（M06-3，CM-2059）
- 問題：程式沿流程圖分岔點往下走時不記得走過哪裡，還把同一個分岔點算兩次——「分岔點繞回自己」的圖每次讀都 500；「一路串 40 個分岔點」的圖工作量翻 40 次倍、永遠算不完、不報錯、log 沒東西，一條工作執行緒卡死，多打幾次全站對所有客戶不回應。
- 為什麼裁升：存進去要有存流程圖的權限（一次性），**但之後任何正常使用者打開那輪稽核就替攻擊者觸發**；決策者裁「一個帳號讓全站對所有客戶不回應不可接受」。同檔另外兩支走訪都有記路，只有這支沒有。
- 在哪：套件 `jedi_flow_engine/common/utils/bpmn_uilts.py:276/:294/:295/:361`。

**第 56 條｜MEDIUM｜與 55／57 同批**
- **現況**：✅ 已修（M06-8，CM-2059）
- 問題：讀範本時無條件解析 XML 且沒防呆；寫入端用「有沒有值」判斷，空字串反而跳過驗證——一筆空字串範本讓所有人的清單頁都 500、不會自己好，要進資料庫修那筆才恢復。
- 為什麼 MEDIUM：要有 `module-frame.update` 權限；runner 誠實標「寫入當下交易能否 commit」未實測，可信度較低。
- 在哪：套件 `infra/mapper/workflow_template_mapper.py:21`；寫入端 `workflow_template_repo_impl.py:52/:61`；宿主入口 `api/module_frame/routes/module_frame_item_route.py:100`。

**第 57 條｜HIGH（裁升）｜與 55 合一張「BPMN 輸入強固化」卡**
- **現況**：✅ 已修（M06-4，FR-114.1-8）
- 問題：任何登入者丟一份特製流程圖給「檢查流程圖」API，就卡住一條工作程序 120 秒；後端預設只有 4 條工作程序，連打四次全站對所有客戶不回應，重複送就能一直維持。
- 為什麼裁升：研究員報 HIGH、檢查員 3:0 降 MEDIUM，決策者裁回 HIGH——同第 55 條理由，**而且門檻更低：不用存進去，打一次就炸**，不需要任何流程範本權限，全站也沒有請求頻率限制。
- 在哪：`api/flow_engine/routes/flow_template_route.py:130-144`；schema `serializers/flow_template.py:28-29` 無長度；驗證器 jedi-flow-engine `bpmn_topology_validator.py:168/:271` 線性掃描。

**第 58 條｜LOW｜一行 decorator，可隨程式層那批順手**
- **現況**：✅ 已修（M06-11，CM-2035）
- 問題：流程範本列表與單筆讀取沒掛 `flow_template.read` 能力點（「能力點」＝系統裡「有沒有權限做這件事」的開關），前端選單擋住、API 敞開，同租戶內任何登入者能讀走全部範本的完整流程圖。
- 為什麼 LOW：跨租戶讀不到（這張表是本 arc 唯一隔離開著的）；是「讀端點漏守門」第四次出現。
- 在哪：`api/flow_engine/routes/flow_template_route.py:29-45`／`:58-66`；可抄 `api/flow_control/routes/project_route.py:41` 的做法。

**第 59 條｜MEDIUM｜併 FR-086 程式層那批（決策者已裁方向）**
- **現況**：✅ 已修（M06-7，FR-114.1-1／FR-114.1-2，CM-2119）
- 問題：專案裡定義為「只能看、沒有待辦」的 viewer，可以把別人的稽核任務標成完成、或把流程退回上一關，稽核紀錄的經手人變成他，連帶改 job／問卷／workflow 狀態。
- 為什麼 MEDIUM：前提是該專案參與者（任何角色）＋知道流程與任務編號（列表 API 就有）。守門政策檔自陳「任一角色皆通過，per 2026-06-05 決策」而 viewer 的定義寫「純瀏覽，無待辦」——產品承諾與守門政策矛盾，決策者已裁（見 §5）。
- 在哪：`app/flow_engine/service/workflow_execution_service.py:655`（`complete_job`）／`:833`（`revert_job`）；政策自述 `common/authz/workflow.py:15-16`。

### 4.2 非資安但是真 bug（跨 arc 總表 §3.2 第 13～16）

**第 13 條｜非資安 bug｜與第 54 條同種寫法一起清**
- **現況**：🚫 裁定不修（M06-13，非資安，決策者 10-01）
- 問題：`force` 旗標有兩個來源（外層參數與 `ctx` 裡的），守門看外層、執行看 `ctx`。三個檢查員 0:3 判非漏洞、首腦同意——能跳過的只有兩項「軟提醒」（程式自己註明「軟提醒非硬擋」），真正守門一個都跳不掉；但同一個意思兩個來源是異味。
- 在哪：`stage_advance_service.py:209`／`oscal_stage_handlers.py:41`。

**第 14 條｜非資安｜建議刪**
- **現況**：🗑️ 裁定刪除，已刪（M06-14，1.21.0）
- 問題：三支吃檔案路徑的方法直接開檔讀寫，套件與主專案兩邊零呼叫者（死碼）。今天不是洞，日後有人把使用者字串接上去就是任意檔案讀寫。
- 在哪：套件 `bpmn_generator.py:963/1220/1241`。

**第 15 條｜非資安 bug**
- **現況**：🚫 裁定不修（M06-15，非資安，決策者 10-01）
- 問題：啟動流程時把每個任務的執行 uid 寫回「傳進來的那份範本」——輪次流程傳凍結快照沒事，module-frame 匯入傳的是共用母版，多流程共用時會互相覆蓋。寫進去的是系統自產 uid 不是使用者輸入，所以不算資安。
- 在哪：`workflow_execution_service.py:429-441`／`module_frame_service.py:241`。

**第 16 條｜未來陷阱｜加陷阱註解或拔掉 provider**
- **現況**：🗑️ 已拆除（M06-16，CM-2215，1.21.0）
- 問題：接好了但沒人用的插頭——套件 `JobExecutionService`（任務增刪改 6 支公開方法，零守門零租戶檢查）在 DI 容器（程式啟動時把各元件組裝起來的登記簿）建了 provider 但全 codebase 零取用；套件 `WorkflowExecutionService`（含 `complete_job`／`revert_job`）主專案根本沒 import，宿主整支重寫了自己的。今天無害，哪天有人接上就是一組現成的無守門端點。
- 在哪：`di_containers/flow_engine/workflow_excution_containers.py:113`。

## 5. 決策者已裁（2026-09-13）

1. **viewer 唯讀、只能留言**：viewer 的本意是開放觀看權限給外部顧問或其他單位同事，不可完成／退回任務（除非他就是被指派的人）。第 59 條修法定案：完成／退回兩支補「呼叫者是該任務負責人或該專案 manager 才放行」，並改掉 `common/authz/workflow.py:15-16` 的政策自述。
2. **第 51／55／57 三條升 HIGH**：留言可假冒同事釣魚；惡意流程圖與「檢查流程圖」API 讓一個帳號就能把全站對所有客戶弄到不回應，不可接受。
3. **程式層那批併 FR-086、現在開卡排修**：這批資料表根本沒有客戶欄位，資料庫想擋也擋不了，只能靠程式檢查「這筆是不是你的」——FR-088 的第 49／50／51／52／53／59 與 FR-086 的第 34／35／37／40／45／47 同一批，**開卡由 FR-086 側首腦負責**。
4. **資料庫層歸 FR-094**：有客戶欄位的 13 張表＋4 支 view 的隔離開關由 FR-094（母卡 CM-1766，八棒已開）接走，含本 arc 看到的 `workflow_executions`／`job_executions`／`workflow_templates`；`element_variables` 連欄位都沒有，只能靠程式層（即第 51 條那批）。
5. **問卷不加平台範本旗標**：問卷引用是複製進任務，不需要公版旗標；要修的是 survey 另 12 張裸表，FR-094 第 8 棒接走。

另有沿用的舊裁定：修正卡一律不派、由 PM 統一安排（早期修正卡 09-21 作廢，後改照內化版問題總表派工，已全數修完）。

## 6. 開卡時的疑點對帳

每張子卡開卡時首腦讀碼列了「重點看什麼」，runner（或首腦補寫時）逐項回答。**證偽與證實同樣有價值**——證偽了才知道是什麼擋住、擋的東西哪天會不會消失。

| 棒 | 疑點數 | 成立 | 證偽（不成立） | 機制屬實但無入口／影響低 | 沒問題／只記錄 |
|---|---:|---|---|---|---|
| H2 | 8 | 5 | 2 | — | 1 |
| H1 | 8 | 4 ＋ 1 部分成立（讀成立寫不成立） | 1 | — | 2 |
| P1 | 8 | 2 ＋ 錨點 1 | 5 | — | — |
| H3 | 7 | 4 | — | 1 待補查 | 2 |
| H4 | 8 | 全答（兩處修正了卡片的猜測） | 見報告 §5 | | |
| P2 | 9 | 3（含對照表做完） | 3 | 3 | — |

合計 48 項疑點（H4 一棒的分類材料只寫「八項全答」，未細分）。各棒重點：

- **H2**：留言讀寫零守門、證明「讀」零守門、兩張表隔離關且無租戶欄——三項成立即 F1／F8／F9。證偽的是「租戶管理員可動別租戶」：守門檔註解寫 `is_admin` 是租戶管理員，追到源頭是平台超級管理員，所以不成立，但註解錯了、它宣稱的兜底也不存在。「反查鏈恆回 None 導致誤擋」屬實但沒引發繞過。
- **H1**：階段歷程 GET 零守門、階段資訊 GET 對非參與者也回資料、`ctx` 整包往下傳（找到署名可自填）、三張表隔離全關——成立。「專案編號與輪次編號從不核對」讀成立、寫不成立。「`force` 雙版本」三個檢查員 0:3 否決、首腦同意。
- **P1**：分岔點走訪不記路、讀範本無條件解析——成立；錨點「流程定義 GET 任何登入者可讀」答完（守門責任在宿主，套件提供的隔離規則「4 條、開關關」等於沒有）。五項證偽：讀本機檔案（XXE）回 None、實體炸彈與五萬層巢狀正常回傳、三支檔案路徑方法零呼叫者、整個套件零 `eval`／`exec`。
- **H3**（首腦自答）：`validate` 端點可卡死、範本讀取漏能力點、宿主 mapper 不解析 XML（壞 XML 讀不炸、發布會擋）、內建範本守門四處無遺漏——四項有答；DTO 無敏感欄位、守門無遺漏——兩項沒問題；「凍結副本不帶租戶」查出機制（租戶靠 jedi-common 在寫入前從登入身分自動填，沒身分就留空），且 DEV 實查修正 FR-087 戊組：191 筆裡只 5 筆快照、186 筆原廠母版，根因待補查。
- **H4**：儀表板三支查詢**不是 `**kwargs` 形狀**（卡片猜錯成因）但零租戶過濾是真的，併 FR-083「27 支零守門」；啟動流程改寫範本 XML 的對象**視呼叫者而定**（記 §3.2 第 15）；通知不帶留言原文、收件人從專案往下查；`known_project_id` 唯一來源是伺服器端解析；輪次凍結檢查三種情況預設放行（fail-open，即「出錯時預設放行」）屬實但影響低於預期。
- **P2**：對照表做完（套件三支 service 共 25 支公開方法，主專案真正接來用的只有 2 支 service，且只有留言那 2 支方法有對外網址）；blueprint 是空的、套件本身沒有對外網址；四張表隔離全關屬實。證偽：「完成任務的條件參數使用者可控」（宿主沒 import 那支 service）、「query entity 傳 None 等於不過濾」（用到它的兩支是死碼）、「repo 注入可被覆寫」（機制存在但無請求期觸發路徑）。「流程變數是 JSONB 任意寫」（JSONB＝資料庫裡可存任意 JSON 結構的欄位）機制屬實，但唯一外部入口就是第 51 條那支留言 API，不另計。

**最值得記的三個證偽**：

1. **「完成任務的條件參數使用者可控」→ 套件那支 service 宿主根本沒 import**（P2 疑點 7）。卡片假設「套件零守門、宿主包一層」，實況是宿主整支重寫了自己的 `WorkflowExecutionService`，套件那 13 支公開方法目前沒有任何外部入口。這一條同時是 §3.2 第 16「接好沒人用的插頭」的來源。
2. **XML 老問題全擋**（P1 五項）：讀本機檔案回 None、實體炸彈與五萬層巢狀正常回傳、整個套件零 `eval`／`exec`。**首腦驗收時各重跑一次**（不是重讀 runner 的敘述），一分鐘就確認。
3. **跨專案推進／退回「讀成立、寫不成立」**（H1 疑點 ①）：兩支 GET 確實不核對專案與輪次；但「寫」被下游 jedi-compliance-audit 拿輪次自己的專案再查一次角色擋住，七條寫入路徑首腦逐一開檔核對。**擋住它的是別人不是本層**——登記為規劃相依（總表 §2.9 🅘），修第 53 條時要連寫入面一起補。

## 7. 這個 arc 教會我們的事

1. **runner 對「scope 清單」與「既有卡」的比對是最常漏的一步**：P1 的 F2 與 H4 的兩條重複，runner 開檔核對都正確、錯在沒對照檔案清單與總表既有條目。驗收第 3 步「越界檢查」不能省。
2. **平行 session 重構同一套件會咬到後續棒的 scope**：P1 掃描期間 jedi HEAD 從 `151c5f88` 走到 `45f14ad9`，P1 範圍只動註解與死碼，但 P2 範圍的 `plugin.py` 拆成五檔，scope 42 → 49、卡片要重寫。往後每棒驗收跑一次 `git diff <掃描版本> HEAD -- <下一棒 scope>`。
3. **密鑰專項吃掉大半票數是結構性的**：H2 18/30、H3 24/30、H4 18/27 票投在範圍外的舊憑證案（工具設了 focus 就會掃全 repo）。「不要讀已掃過的路徑」對 scope 內有效、對密鑰專項無效，不必再記，但要知道「真正投在範圍內的票」比帳面少很多。
4. **「交付四件缺一不可」與「開掃前先回報四件落點」兩句都寫才有效**：只寫前一句，H3 仍漏交付（本 arc 第一次、全計畫第四次）；加了後一句，H4、P2 都做滿。推翻「寫了就有效」——它只是降低機率。
5. **首腦補寫報告時要自己答卡片疑點**：H3 最有價值的產出（FR-087 戊組「191 筆凍結副本」實為 5 筆快照＋186 筆原廠母版）是答疑點五時查出來的，不是工具那兩條。
6. **被否決的候選也有資訊量**：P2 那條 0:3（「`element_variables` 沒掛租戶 mixin、比姊妹表危險」）被否決的理由是「姊妹三張的隔離開關也全關」——反差不存在，反而坐實四張流程表資料庫層一道隔離都沒有。
7. **證偽要「重跑」不要「重讀」**：P1 三題 XXE 首腦各跑一次只花一分鐘，比讀 runner 的敘述可靠。
8. **排序用「可利用門檻」比嚴重度標籤好用**：第 1 棒選 H2 而非疑點最「大」的 H1，因為 H2 只要登入＋猜編號。同一判準也解釋了為什麼第 57 條比同組的第 55 條更急：不用存進去、打一次就炸。

## 8. 可信度兩層

**「這些存在嗎」——高。** 六棒面板全 `verified`、135 票全投出、無漏投；範圍內每一條首腦都自己開檔核對（不信 runner 自述），H2／H1／H3 另在 DEV 資料庫唯讀實查隔離開關、規則數與筆數，P1 五項證偽首腦重跑實測。runner 抓到工具原文一處錯（宣稱 `compliance.projects` 有隔離、實查關但有 4 條沒生效的規則），首腦複核屬實。

**「只有這些嗎」——中等，不可宣稱乾淨。** 原因四點：① 工具沒留逐檔閱讀帳本（P2 runner 自陳 `coverage.research` 空），「49 檔只有這一條」的信心停在中等；② 六棒都以 low 檔次（工具的 effort 等級，子卡標題可見）單輪跑完，沒有第二輪交叉；③ 四棒（H2／H1／H3／H4）票數大半投在範圍外的舊憑證案，真正投在範圍內的票比帳面少（例如 H2 30 票只有 12 票落在 23 檔）；④ 總表 §3.4 登記了本 arc「等於沒查」的項目：留言在前端怎麼渲染（儲存型 XSS）、問卷 uid 進 jedi-survey 的租戶檢查、`project_audit_rounds`／`round_stage_transitions` 這類無租戶欄的表在 FR-087 之外還有幾張。

## 9. Commits 清單（BE `feature/FR-075`，未 push）

依時間順序，`git log --oneline --grep='FR-088'` 抄出，再補 LOG 第十任 block 列的兩筆跨 arc 裁定 commit；標 ※ 的是不屬本 arc 掃描作業、但與 FR-088 登記或裁定有關的 commit。

| hash | 一句話 |
|---|---|
| `d9d057e2` | 開卡：讀碼切六棒，母子卡 CM-1669～1675 一次建完 |
| `e9507d29` | H2 掃描報告（runner） |
| `306ef445` | 驗收 H2：三條「只驗身分不驗歸屬」全屬實，登記總表第 49～51 |
| `ceb455df` ※ | FR-089／FR-090 補母卡登記（README 登記列緊接 FR-088，故 grep 命中） |
| `1e95823b` | H1 掃描報告（runner） |
| `60d90cb1` | 驗收 H1：四條屬實、兩大疑點證偽複核，登記第 52～54＋§3.2 第 13 |
| `ebd3b1e1` | P1 掃描報告（runner） |
| `12d0d06f` | 驗收 P1：兩條屬實、五項證偽重跑實測、F2 改判範圍外；P2 scope 重算 49 檔 |
| `5b2165b5` | H3 runner 未交付，首腦補寫報告並驗收；修正 FR-087 戊組 191 筆描述 |
| `c6c12e68` | H4 掃描報告（runner） |
| `0dbe3586` | 驗收 H4：改判 1 新 2 重複、viewer 可完成別人任務登記第 59 條；第九任首腦交棒 |
| `4a4092f5` ※ | 總表 §6 重整：補 viewer 唯讀待裁項、FR-088 三條升 HIGH 待裁項（跨 arc 動作，含 FR-088 項） |
| `05e81f6e` ※ | 記帳決策者 2026-09-13 五項裁定（viewer 唯讀／問卷不加旗標／程式層併 FR-086／CM-1559 順序歸 FR-094／51、55、57 升 HIGH）——grep 不命中，從 LOG 抄 |
| `bf14cda8` ※ | 追記 `upload_files` 加租戶欄＋開隔離歸 FR-086 程式層那批——grep 不命中，從 LOG 抄 |
| `31c0d7f6` | P2 掃描報告（runner） |
| `17664009` | 驗收 P2：套件本體 49 檔零新發現、對照表證實宿主沒接套件執行 service；六棒收口 |

本 SUMMARY 自身的 commit 不在上表（寫檔時尚未 commit）。

## 10. Follow-up 與座標

**SUMMARY 之後的後續（現況）**：

1. **修正卡**（現況：已處理，由 FR-114 修正線修完，隨 1.21.0 出貨）：程式層歸屬檢查那批（第 49／50／51／52／53／59，與 FR-086 的第 34／35／37／40／45／47 同批）由 **FR-086 側首腦**開卡；55／57 合一張「BPMN 輸入強固化」卡；兩邊開完回報卡號後登記進總表 §2.2。
2. **FR-094 三張表**：`workflow_executions`／`job_executions`／`workflow_templates` 的隔離開關已接在 FR-094（CM-1770／1771／1772）；`element_variables`、`job_evidences`、`project_audit_rounds`、`round_stage_transitions` 無租戶欄，不在 FR-094，只能靠程式層。
3. **總表 §3.4 死角**：無租戶欄的表在 FR-087 13 張之外還有幾張，母卡收口時單獨盤一次（尚未盤）。
4. **下一支掃描**：jedi-oscal-v2 的宿主半邊（套件半邊已掃；順序依總表 §4，本檔未重讀該段）。
5. **手冊**：`security-scan-lead` 第一節「≤15 檔」與已裁的 ≤30／40 不一致，未改（收尾類，等令）。

**座標**：

- FR README（一頁看完＋六段驗收）：[`../README.md`](../README.md)
- 六份報告：[`../scan-H2-task-comment-evidence.md`](../scan-H2-task-comment-evidence.md)／[`../scan-H1-stage-advance-rollback.md`](../scan-H1-stage-advance-rollback.md)／[`../scan-P1-bpmn-parsing-and-templates.md`](../scan-P1-bpmn-parsing-and-templates.md)／[`../scan-H3-flow-template-management.md`](../scan-H3-flow-template-management.md)／[`../scan-H4-execution-host-service.md`](../scan-H4-execution-host-service.md)／[`../scan-P2-execution-core.md`](../scan-P2-execution-core.md)
- 跨 arc 總表（§3.1 第 49～59、§3.2 第 13～16、§6 裁定）：[`../../security-scan-consolidated/README.md`](../../security-scan-consolidated/README.md)
- STATE（living 現況）：[`../../FR-075-2609-jedi-package-security-audit/handoff/security-scan-STATE.md`](../../FR-075-2609-jedi-package-security-audit/handoff/security-scan-STATE.md)
- LOG（append-only 沿革）：[`../../FR-075-2609-jedi-package-security-audit/handoff/security-scan-LOG.md`](../../FR-075-2609-jedi-package-security-audit/handoff/security-scan-LOG.md)
- 母卡 CM-1669：<https://app.notion.com/p/FR-088-jedi-flow-engine-150-3d9346da4cd081c3ae47d794baa84a40>
- 首腦手冊：`.claude/skills/security-scan-lead/SKILL.md`

## 數字對帳備註（寫本檔時在材料裡看到的口徑差異）

以下數字各處說法不一，本檔採用的口徑已在各段標明；未裁前不改任何來源。

1. **「12 條」的算法**：README／LOG／STATE 都寫「範圍內 12 條（§3.1 第 49～59＋§3.2 第 13～16）」，但 §3.1 第 49～59 是 11 列、§3.2 第 13～16 是 4 條，加起來 15；12 是各棒 runner 報的範圍內條數加總（H1 把同一個洞的 F5／F6 算兩條）。本檔寫「12 條（11 條資安＋4 條非資安另計）」。
2. **檔數**：母卡標題與 README 前 matter 寫「六棒 150 檔」、CM-1675 卡名寫「42 檔」；P2 實掃 49 檔，六棒實際合計 157。
3. **`complete_job`／`revert_job` 行號**：總表第 59 條與 README 寫 `:655`／`:833`，H4 報告寫 `:655`／`:805`，P2 報告寫 `:611`／`:788`——同一支檔在三棒掃描期間有漂移。本檔採總表。
4. **H4 範圍外條數**：README 與 LOG 寫「6 條憑證裡 1 條新型態」，H4 報告 §3.2 文字寫「六條」但表格只列外-1～外-5 五列。
5. **DEV 筆數**：README「資料庫層實況」表（2026-09-12 首腦查）寫 `job_evidences` 782／`element_variables` 3,011／`job_executions` 10,783／`workflow_templates` 17,206；H2 驗收複核寫 848／3,236／11,065，P1 驗收寫 `workflow_templates` 18,267。同一庫兩組數字，可能是查詢時點不同。
6. **H2 疑點分類**：README 一頁看完寫「八個疑點五成立、兩證偽、一無問題」，H2 報告 §5 的 ⑥ 與 ⑧ 都標「沒問題」（兩項）、④ 標「部分推翻」、⑤ 標「屬實但沒引發繞過」。
7. **子卡狀態**：STATE 第 442 行與 LOG（工作區未 commit 的第十任 block）都寫「母卡＋六子卡已改 Done（決策者令）」，README front matter 六張子卡仍標 `修正待驗證`（未同步）。
8. **LOG 的 P2 block 尚未 commit**：已 commit 的 LOG（HEAD `17664009`）最後一個 block 是第九任首腦交棒；P2 驗收＋六棒收口＋第十任交棒的 block 在工作區（`git status` 顯示 LOG／STATE 均為 modified），寫本檔時一併讀了工作區版本。
9. **FR-064 SUMMARY 檔頭**：派工單說抄它前 5 行的 title／status／relates 三欄 front matter，實際該檔沒有 YAML front matter（是 H1 標題＋引言塊）；本檔改用 README 同款 YAML 三欄＋引言塊。

## 附錄：本檔用到的詞彙

| 詞 | 白話 |
|---|---|
| 套件／宿主 | 套件＝jedi-flow-engine 這個「零件供應商」，自己沒有對外網址；宿主＝主專案接它的那半邊，所有 API 入口與權限檢查都在這裡 |
| scope／範圍內／範圍外 | 每棒卡片列的檔案清單；工具的密鑰專項會掃全 repo，撈到的東西若不在清單內就是「範圍外」，不算該棒發現 |
| 面板／檢查員／票 | 工具對每條候選發現派三個獨立檢查員各投一票；「3:0」＝三人都認為成立，「0:3」＝三人都否決 |
| `verified`（stamp） | 工具跑完自己蓋的章，代表每條候選都投完、沒有漏投；不代表「沒有漏掉的問題」 |
| 候選／去重 | 研究員報上來的原始條目；同一個洞被兩位研究員各報一次會合併成一條 |
| 守門／歸屬檢查 | 後端在做事前檢查「你有沒有登入」「你是不是這個專案的人」「這筆資料是不是你的」；本 arc 多數問題是只做了第一項 |
| RLS／隔離開關／規則 | 資料庫層「每個客戶只能看自己資料」的機制；「開關關但有 4 條規則」＝規則寫好了但沒啟用，等於沒有 |
| 租戶欄／tenant 欄 | 資料表裡標「這筆是哪個客戶的」的欄位；沒有這個欄位的表，資料庫層想擋也擋不了，只能靠程式 |
| fail-open | 出錯（例如查不到資料、元件沒注入）時預設放行；相反是 fail-closed 預設擋下 |
| JSONB | 資料庫裡可存任意 JSON 結構的欄位型別，沒有形狀限制 |
| DI 容器 | 程式啟動時把各元件組裝、登記起來的登記簿；「建了 provider 沒人取用」＝登記了但沒人來拿 |
| 能力點（capability） | 系統裡「有沒有權限做這件事」的細粒度開關，例如 `flow_template.read` |
| 密鑰專項 | 工具設了 focus 就會額外跑一次「找寫死的密碼與金鑰」的掃描，掃全 repo 不受 scope 限制 |
| 交付四件 | runner 做完一棒必須交的：報告檔、commit、Notion 卡回寫（含 commit hash 與 run ID）、卡片狀態改「修正待驗證」 |
