# FR-088.3 P1：BPMN 流程圖解析與範本讀寫——資安掃描報告

- **卡片**：CM-1672（FR-088 第 3 棒）
- **範圍**：jedi-flow-engine 套件 30 支檔（BPMN 解析器／產生器／拓撲驗證器 ＋ 流程範本從 DTO 到 repository 的讀寫鏈）
- **掃描時間**：2026-09-12（UTC 12:01 起，跑了約 7 小時 15 分）
- **掃描版本**：`151c5f88c2b012e0368f0e47014d25300cbaf76a`（branch `feature/FR-075`）
- **工具**：Claude Code `claude-security` plugin v0.11.0，effort `low`、focus `attack-surface`
- **驗證章**：`verified`（三個獨立檢查員對 3 條發現各投一票，9 票全數投出，沒有漏投，三條都是三比零一致通過）
- **只掃不修**：本棒沒有改任何程式碼，也沒有動任何環境（只對 DEV 資料庫做了唯讀查詢）

---

## 1. 🔴 一句話結論

**掃完了，範圍內找到 3 個真問題，最嚴重的是「一張惡意流程圖能把伺服器卡死」——存進去之後，之後每個讀它的人都會把伺服器的一條工作執行緒佔住不放，多打幾次整個系統就不回應了。** 好消息是：這一棒最擔心的 XML 老問題（讀走伺服器本機檔案、用小檔案撐爆記憶體）**實測全部被擋住**，不成立。

---

**現況（以 `docs/security-report/M06*.md` 為準）**：F1（惡意流程圖卡死，M06-3）→ ✅ 已修（CM-2059）；F2（套件庫走 http，原 CM-1634）→ ✅ 已修（版本鎖定檔入版控，連線維持 http 為決策者裁定）；F3（空字串範本，M06-8）→ ✅ 已修（CM-2059）；三支死碼方法（M06-14）→ 🗑️ 裁定刪除，已刪（1.21.0）。

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

流程圖（BPMN）是一種 XML 文字檔。使用者在畫面上畫好稽核流程存進系統，後端要「讀懂」這張圖，才知道這個階段做完之後下一棒是誰、要做什麼。

這一棒檢查的就是**「讀懂流程圖」那段程式**，問三件事：

1. **一張惡意的流程圖，能不能把伺服器弄壞？**（卡死、當掉、吃光記憶體）
2. **能不能騙伺服器去讀不該讀的檔案？**（XML 有個惡名昭彰的功能，可以叫解析器去開本機檔案，例如密碼檔）
3. **流程範本讀取的時候，有沒有分客戶？**（A 客戶能不能讀到 B 客戶的流程圖）

---

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

### 3.1 範圍內（本棒的發現，共 3 條）

| # | 這是什麼問題 | 出事會怎樣 | 要先有什麼才打得到 | 在哪裡 | 怎麼修 |
|---|---|---|---|---|---|
| **F1**<br>🟡 MEDIUM | 程式沿著流程圖的「分岔點」往下走的時候，**不會記得自己走過哪裡**，也沒有設步數上限 | 一張「分岔點繞回自己」的流程圖會讓程式無限打轉 → 每次讀都 500。更糟的是「一路串接 40 個分岔點」的圖：程式會做 2 的 40 次方次計算、**連錯誤都不會報**，請求永遠不回來，一條工作執行緒就此卡死。多打幾次把執行緒吃光，整個系統對所有客戶都不回應 | ① 一個能存流程圖的帳號（`module-frame.update` 權限，或流程範本的新增／修改權限）<br>② 之後有人讀到這張圖——**不需要是攻擊者自己，正常使用者打開那一輪稽核就會中** | `jedi_flow_engine/common/utils/bpmn_uilts.py:361`（`_get_jobs_from_exclusive_gateway`）<br>與 `:276` `get_next_jobs` 互相呼叫成環 | 在 `get_next_jobs` / `_get_jobs_from_exclusive_gateway` 之間傳一個「走過的節點清單」，走過就不再展開，並加步數上限。另外 `:294` 和 `:295` 把同一份清單算了兩次（`_get_exclusive_gateway_default_job` 在 `:340` 又算一遍），改成算一次傳進去，就能把「每過一個分岔點工作量翻倍」這件事直接消掉。存檔時也應該擋掉有環的流程圖 |
| **F2**<br>🟡 MEDIUM | 套件安裝來源用的是**沒有加密的 http 連線**，而且鎖定版本雜湊的 `poetry.lock` 沒有進版控 | 跟我們在同一個內網、且能夠插在中間的人，可以在我們安裝套件時**把套件掉包**，讓惡意程式在開發機和打包機上執行。打包機產出的正是要交給客戶的映像檔 | ① 攻擊者已經進到 192.168.50.0/24 這個內網，而且能做網路中間人（例如 ARP 欺騙）<br>② 有人跑 `poetry install` / `poetry update` | `pyproject.toml:72`（`priority = "primary"` 在 `:73`，且是唯一來源） | Nexus 改走 https；自簽憑證就把 CA 憑證發到各台機器，不要退回 http。另外把 `poetry.lock` 納入版控，讓雜湊比對成為第二道防線，不要只靠連線本身 |
| **F3**<br>🟡 MEDIUM | 每次從資料庫讀出流程範本，程式都會**無條件去解析那段 XML，而且沒有防呆**；寫入端的檢查又剛好會被「空字串」繞過 | 一旦有一筆範本的 XML 被寫成空字串，**之後每一次讀到這筆資料的清單或詳細頁都會 500**，而且不會自己好，要進資料庫修那筆資料才會恢復。受害的是其他正常使用者，不是寫壞它的人 | ① `module-frame.update` 權限<br>② 送出時 `xml` 欄位留空或整個不帶<br>③ 該語系的翻譯列已經存在 | `jedi_flow_engine/infra/mapper/workflow_template_mapper.py:21`（解析動作在 `bpmn_uilts.py:34`）<br>繞過點：`workflow_template_repo_impl.py` 的 `if entity.xml:` | 兩邊都補：① 寫入端驗 XML，空的或不是合法 BPMN 就回 400，不要用「有沒有值」當判斷；② `to_entity` 遇到空字串就跳過解析，解析失敗就退成「這張圖 0 個任務」，不要讓一筆壞資料拖垮整個查詢 |

### 3.2 範圍外

**這一棒沒有範圍外發現。** 工具因為設了 `focus` 會額外跑一次「找密碼金鑰」的專項掃描（掃全 repo、不受 30 檔範圍限制），但這次沒有撈到任何東西——jedi-flow-engine 這個套件目錄裡沒有寫死的憑證。

---

## 4. 每條發現的詳述

### F1 — 沿分岔點走訪流程圖時不記路，一張惡意流程圖能卡死伺服器（MEDIUM，可信度 高）

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

流程圖裡有一種節點叫「分岔點」（exclusive gateway），意思是「看條件決定往左還是往右」。後端要算出「這一棒做完，下一棒是誰」，就得從目前位置沿著線往下走，碰到分岔點再往它的每條出線繼續走。

問題是這段走訪程式**不會記得自己走過哪些節點**。正常的流程圖從頭走到尾就結束了，所以平常沒事；但只要圖的形狀不正常，就會出事。

**出事會怎樣**

兩種形狀，兩種死法：

- **分岔點繞回自己**（A 指向 B、B 又指回 A）：程式無限互相呼叫，直到 Python 喊「遞迴太深」，該請求回 500。每次有人讀這張圖都會 500。
- **一路串接很多分岔點**（不繞圈，就是一長串）：這個更糟。程式在 `:294` 和 `:295` 對同一個分岔點算了**兩次**——第一次直接算，第二次在算「預設走哪條」時又整份重算一遍。所以每多一個分岔點，工作量就翻一倍：40 個分岔點就是 2 的 40 次方次計算（一兆多次）。**程式不會報錯，它只是永遠算不完**，請求不回來，處理它的那條工作執行緒就此卡住。

第二種特別難察覺：沒有錯誤訊息、log 裡什麼都不會有，從外面看只是「這頁一直轉圈」。多打幾次把工作執行緒吃光，整個後端就對所有客戶停止回應了。

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

- 一個能把流程圖存進系統的帳號：`module-frame.update` 權限（打 `PUT /module-frame/item/xml/<uid>`），或流程範本的新增／修改權限
- 存進去之後，**要有人去讀它**才會觸發。但這一點門檻極低——不需要攻擊者自己去讀，**任何正常使用者打開那一輪稽核的現況、或按下「完成任務」，就會踩到**

**在哪裡**

- 遞迴的點：`jedi_flow_engine/common/utils/bpmn_uilts.py:361`（`_get_jobs_from_exclusive_gateway` 裡呼叫 `self.get_next_jobs`）
- 回頭的點：同檔 `:276` `get_next_jobs` → `:294` 呼叫 `_get_jobs_from_exclusive_gateway`
- 翻倍的來源：`:295` 呼叫 `_get_exclusive_gateway_default_job`，它在 `:340` 又把同一份清單重算一次

**首腦核對註記（我自己開檔看過）**

屬實，而且有一個佐證讓我更確定這是「漏寫」不是「刻意設計」：**同一個檔案裡另外兩支走訪流程圖的函式都有記路**——`get_job_execution_order`（`:392`，`visited = set()` 在 `:395`）與另一支分層走訪（`:454` 起，`visited` 在 `:470`／`:476`／`:485`）。三支同類函式，兩支有防護、一支沒有，這是遺漏的典型形狀。

另外我確認了**寫入端不會幫忙擋**：`WorkflowTemplateService.update_workflow_template_xml`（`workflow_template_service.py:158`）只是把字串塞進 entity 就存檔，沒有做任何拓撲檢查。套件裡確實有一支拓撲驗證器（`bpmn_topology_validator.py`），但它是**宿主的 flow-template 路徑才會呼叫**（`app/flow_engine/service/flow_template_app_service.py:143`／`:251`），`module-frame` 那條寫入路徑完全繞過它；而且我讀了那支驗證器的檢查項目（起始階段、已知階段碼、後繼、結束階段、閘道條件、可達性），**它沒有一項在檢查「圖裡有沒有環」**。所以即使走 flow-template 那條路，這個洞一樣打得到。

**怎麼修**

主修法是給走訪加記路：在 `get_next_jobs` 與 `_get_jobs_from_exclusive_gateway` 之間傳遞一個已展開節點的集合，走過的分岔點不再展開，並設一個深度上限當保險。

順手要做的第二件事：把 `:294` 算好的 `gw_jobs` 直接傳給 `_get_exclusive_gateway_default_job`，不要讓它在 `:340` 重算——光這一項就能把「每過一個分岔點工作量翻倍」消掉，讓長串形狀從指數變成線性。

第三，存檔時擋掉有環的流程圖（拓撲驗證器加一項環偵測），並讓 `module-frame` 那條寫入路徑也走驗證。

---

### F2 — 套件來源走沒加密的連線，安裝時可能被掉包（MEDIUM，可信度 中）

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

我們的 Python 套件不是從公開的 PyPI 抓，而是從公司內網自架的 Nexus 抓。設定裡寫的網址是 `http://`——**沒有加密**。沒加密的意思是：跟我們在同一個內網、而且有辦法「站在中間」的人，可以在套件送過來的路上把內容換掉。

正常情況下還有第二道防線：`poetry.lock` 這個檔案會記下每個套件的指紋（sha256），裝的時候比對一下，被掉包就會發現。但**這個 monorepo 把 `poetry.lock` 排除在版控之外**，所以新 clone 下來的環境根本沒有指紋可比對，等於第二道防線沒跟著出貨。

**出事會怎樣**

攻擊者把 `jedi-common`（我們自己的核心套件，管資料庫連線和登入身分）換成帶惡意程式的版本。下次有人在開發機或 188 打包機上跑 `poetry update`，惡意程式就在那台機器上執行了。**打包機產出的正是要交給客戶的映像檔**，所以污染會一路流到客戶端。

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

- 攻擊者已經進到 192.168.50.0/24 這個內網（不是從外面直接打得到的）
- 而且能做網路中間人（例如 ARP 欺騙、DNS 欺騙、或控制了某台網路設備）
- 然後要等到有人跑套件安裝／更新

門檻不低——要先有內網立足點。但一旦成立，拿到的是**打包機**，等級很高。

**在哪裡**

`pyproject.toml:72`，`url = "http://192.168.50.171:8082/repository/pypi-group/simple"`，下一行 `:73` 標著 `priority = "primary"`。因為 Poetry 一旦設了 primary 來源就會把公開 PyPI 停用，所以**所有依賴都走這條明文連線**。

**首腦核對註記（我自己開檔看過）**

行號與內容逐字屬實。這條嚴重度是 MEDIUM 而不是 HIGH，理由是「要先進內網」這個前提相當實質——它不是一個外部人能直接打的洞。但它也不是純理論：內網本來就會被當成「已經半信任」的區域，而這條剛好落在「開發機與打包機」這種高價值目標上。

**怎麼修**

- Nexus 改走 https。如果用自簽憑證，就把 CA 憑證發到各台開發機與打包機，**不要因為憑證麻煩就退回 http**
- 把 `poetry.lock` 納入版控。這樣即使連線被動手腳，指紋比對也會擋下來——兩道防線不要只剩一道

---

### F3 — 讀範本時無條件解析 XML 且沒有防呆，一筆壞資料能讓整個清單頁壞掉（MEDIUM，可信度 低）

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

每次從資料庫讀出一筆流程範本，程式都會順手解析那段 XML，好算出「這張圖有幾個任務」。問題是這段解析**沒有任何防呆**——XML 壞掉就直接拋例外，整個查詢跟著失敗。

而寫入端的檢查剛好有個縫：它用的判斷是「這個欄位有沒有值」（`if entity.xml:`），而空字串在 Python 裡算「沒有值」，所以**送一個空的 XML 進去，反而會跳過驗證直接存下來**。

**出事會怎樣**

一旦某筆範本的 XML 變成空字串，之後**每一次讀到這筆資料的清單頁或詳細頁都會 500**。而且它不會自己恢復——要有人進資料庫把那筆資料修掉才會好。受害的是其他正常使用者（他們只是想看流程範本清單），不是寫壞它的那個人。

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

- `module-frame.update` 權限
- 送出 `PUT /module-frame/item/xml/<uid>` 時，`xml` 欄位留空或整個不帶（例如 body 直接送 `{}`）
- 該語系的翻譯列已經存在（這樣寫入時的另一次解析會拿到舊的主表 XML，交易才會順利 commit）

**在哪裡**

- 炸掉的點：`jedi_flow_engine/infra/mapper/workflow_template_mapper.py:21`，`bpmn_obj = BpmnUtils(workflow_template.xml)`，沒有 try/except
- 實際解析動作：`jedi_flow_engine/common/utils/bpmn_uilts.py:34`
- 繞過驗證的點：`jedi_flow_engine/infra/repository/workflow_template_repo_impl.py` 的 `if entity.xml:`
- 進入點：宿主 `api/module_frame/routes/module_frame_item_route.py:100`，`payload.get("xml", "")`，**沒有掛任何欄位驗證 schema**

**首腦核對註記（我自己開檔看過）**

程式碼的形狀屬實：`to_entity` 確實是所有查詢（依 uid、依 id 清單、依欄位全查）的**唯一**轉換點，確實沒有 try/except；進入點也確實沒有 schema 驗證。我也實測確認了空字串會讓解析器拋錯：`xmltodict.parse("")` 回 `ExpatError: no element found`，`"   "` 同樣。

**可信度標低的原因**：整條路徑是讀程式碼推出來的，**我沒有真的送那個請求去驗**（本棒紀律是只掃不修、不動環境）。中間有一段我只能從程式碼判斷——寫入時 `to_entity` 會先被呼叫一次，此時若翻譯列已存在，它拿到的是舊的主表 XML（解析得動），交易才 commit 得成功；若這個前提不成立，寫入當下就會炸，那這條就只是「送出者自己收到 500」，不構成對別人的影響。**這個前提沒有實測過，是這條可信度低的唯一原因，不是面板有人反對**（三票一致通過）。

**怎麼修**

兩邊都要補，缺一不可：

1. **寫入端**：在 `module_frame_item_route.py` 加欄位驗證，`xml` 空的或不是合法 BPMN 就回 400。repo 層的 `if entity.xml:` 也改成明確驗證，不要用「有沒有值」當判斷
2. **讀取端**：`to_entity` 遇到空字串直接跳過解析；解析失敗就 try/except 退成 `job_count=0` / `jobs=[]`。**一筆壞資料不該讓整個查詢失敗**

---

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

卡片列了 8 項思考起點，逐項交代。**其中 5 項證偽**——不成立也是結果，而且知道「什麼擋住了」比知道「沒事」更有用。

| # | 卡片上的疑點 | 結論 | 是什麼擋住了／為什麼成立 |
|---|---|---|---|
| 1 | 🔴 解析器沒有大小與深度上限 | **不成立**（部分成立，但打不動） | 三件事分開講：**① 外部實體（讀本機檔案）已擋**——我實測送 `<!ENTITY x SYSTEM "file:///etc/passwd">`，回來的是 `{'r': None}`，檔案內容沒進來，因為 xmltodict 0.14.2 的 `parse()` 預設就帶 `disable_entities=True`，且四處 parse 沒有一處把它關掉。**② 實體炸彈（小檔案撐爆記憶體）已擋**——同一個預設值；我實測三層巢狀實體展開後長度只有 11 個字元，沒有展開。**③ 深度巢狀不會炸**——我實測 50000 層巢狀，解析器正常回傳、沒有 RecursionError，因為 expat 是迭代式的不是遞迴式的。超大文件的記憶體壓力受宿主 50MB body 上限保護，屬於既有的 CM-1587 範疇 |
| 2 | 產生器用兩個解析器，有沒有 `ET.fromstring` 漏網 | **不成立** | 我 grep 過 `bpmn_generator.py`、`bpmn_uilts.py`、`bpmn_topology_validator.py`、`common_utils.py` 四個檔的 `ET.fromstring` / `ET.parse` / `ElementTree.parse` / `XMLParser`，**零命中**。`ET` 的用途全部是產生端（`Element` / `SubElement` / `tostring` / `register_namespace` / `indent` / `ElementTree()` / `write`），三處解析全走 `safe_parse`（defusedxml）：`:1541`、`:1563`、`:1982`。檔頭自陳與實況相符 |
| 3 | 🔴 三支吃檔案路徑的方法是死碼還是殘留 | **成立（是死碼），但目前無法觸發** | `save_bpmn_to_file`（`:963`）、`export_xml`（`:1220`）、`import_xml`（`:1241`）確實用 `open(file_path)` 直接讀寫。我在 jedi monorepo 全域與 BE repo 全域各 grep 一次呼叫者，**兩邊都是零**。所以現在不是洞——**但它是一個裝好的陷阱**：只要日後有人把使用者送來的字串接上去，就是任意檔案讀寫。**建議刪除，或移進測試專用模組**。這條沒有獨立開卡的價值，併進 F1 的修正卡順手處理即可 |
| 4 | 條件參數是拼字串比對，不是 eval | **確認安全** | 我對整個 `jedi_flow_engine/` 目錄 grep `eval(` / `exec(` / `pickle` / `os.system` / `subprocess`，**全部零命中**。`_parse_condition_param_to_string`（`:370`）只是把 dict 拼成 `${key == 'value'}` 字串，然後與流程圖裡的條件做**完全比對**，沒有任何動態求值。至於「key/value 含特殊字元造成錯配到預設分支」——這個**可能發生**（字串比對對 typo 和型別不合都很脆弱），但後果是「流程走錯支線」屬於**正確性問題不是資安問題**，而且程式碼在 `:311` 已經留了 warning log 會把錯配記下來 |
| 5 | 圖有環時會不會無限遞迴 | **✅ 成立 → F1** | 見上方 F1。而且比卡片預期的更糟：除了「繞圈無限遞迴」，還有「長串指數爆炸」這個**不報錯的**變體，後者更難察覺 |
| 6 | 範本讀取無租戶檢查、資料庫隔離關閉 | **屬實，但責任在宿主不在套件**（見下節專段） | 見第 6 節 |
| 7 | 建立範本時不解析，惡意 XML 之後每次讀取才炸 | **✅ 成立 → 正是 F1 與 F3 的共同放大器** | 我核對過 `update_workflow_template_xml`（`workflow_template_service.py:158`）：確實只把字串塞進 entity 就存，沒有拓撲檢查。這就是為什麼 F1 和 F3 的影響面這麼大——**壞東西存進去是一次性動作，但之後每一個讀它的人都會中**，包含完全無辜的其他使用者 |
| 8 | `__main__` 區塊讀本機檔、三個 `.bpmn` 範例檔有沒有內網位址或憑證 | **已消失（掃描期間被另一棒刪掉）** | 掃描當下這些還在，但**掃描進行中有平行 session 對這個套件 commit**（見第 7 節）：`__main__` 區塊（原 `:715-720`）連同三個 `.bpmn` 範例檔一起被刪了。我在 HEAD 確認：`jedi_flow_engine/common/utils/` 底下現在只剩四支 `.py`，範例圖不存在了，所以「範例檔裡有沒有內網位址」這題**在 HEAD 上已經沒有對象** |

---

## 6. 專段回應：首腦追加的錨點（流程定義 GET 外洩）

卡尾追加的疑點是：**流程定義 GET 把整份範本 XML 回給任何登入者，沒有分客戶。** 本棒要答兩題。

### 第一題：套件該不該自己擋？

**答：不該，但套件也沒有提供讓宿主擋的手段，這才是套件側的缺口。**

租戶隔離的正確位置有兩層，套件側的責任是**第二層（資料庫）**不是第一層（守門）：

- **第一層是守門**：「這個人有沒有資格讀這張圖」——這需要知道業務脈絡（他是不是這個專案的參與者），**只有宿主知道，套件不該也無法判斷**。所以 route 沒掛守門是宿主的缺口，這一點責任確實不在套件。
- **第二層是資料庫隔離**：「就算守門漏了，資料庫也只讓他看到自己租戶的資料」——**這一層是套件的責任，而它是關著的**。

我實查了 DEV 資料庫（唯讀 SELECT）：

```
workflow_templates       | rls=false | force=false
workflow_templates_trans | rls=false | force=false
```

四條隔離規則（select／insert／update／delete）都好好地寫在那裡，**但總開關沒有打開，所以四條規則一條都不生效**。這正是 FR-087 T1 第 2 項已經記錄過的狀態，本棒獨立複查確認屬實。

套件側 model（`infra/models/workflow_template.py:11`）有掛 `TenantScopedMixinModel`，所以寫入時會自動填 `tenant_id`——資料是有租戶歸屬的。我查了資料量：**18267 筆範本，其中 18076 筆有 tenant_id，191 筆沒有**（那 191 筆正是 FR-087 記錄的凍結副本，宿主 snapshot service 建的，屬 H3 範圍）。

也就是說：**資料齊了、規則寫好了、就差把開關打開。** 而 repository 這一側（`workflow_template_repo_impl.py`）全靠 `get_one_by_fields` / `get_all_by_fields` 這種泛用查詢，**Python 這一層完全沒有租戶條件**——這是對的做法（專案慣例就是租戶隔離交給資料庫 RLS，Python 不重複過濾），但前提是**那個開關要開**。開關關著的時候，這個設計等於沒有任何隔離。

**結論**：套件不該自己做「他是不是這個專案的人」的守門，但套件**應該確保它提供的租戶隔離是實際生效的**。目前它提供了一個看起來有、實際沒開的隔離機制——這比沒有更危險，因為宿主會以為有。

### 第二題：宿主有幾條路走到這支方法？

我在 BE repo grep 了 `get_workflow_template_by_uid` 的所有呼叫點，**共 18 處**，其中：

- **1 處是直接的網址入口**：`api/flow_engine/routes/flow_engine_route.py:36`，也就是錨點指的那支。這支只掛 `@jwt_required()`（`:32`），沒有任何專案歸屬檢查，拿到 uid 就回整份 DTO 含完整 XML。
- **其餘 17 處都是內部服務互相呼叫**（module_frame 系列 11 處、flow_engine 服務 2 處、flow_control 1 處、oscal 1 處、snapshot 1 處等），它們各自的入口有沒有守門要看各自的 route，**不在本棒 30 檔範圍內，我沒有逐一追**。

所以**直接暴露的路是一條**，但 `get_workflow_template_by_uid` 這支方法本身在宿主裡被大量複用，任何一個沒守門的入口都會再開一條路。**這一點屬 H2／H3 的範圍**，本棒只負責把套件側的事實釐清。

**建議**：這條不要開新卡，**併進 H2 既有的守門修正卡**（錨點來源就是 H2），另外**「打開 `workflow_templates` 的資料庫隔離開關」要獨立開一張卡**——它跨 H2／H3／本棒，且修法單純（一道 `ALTER TABLE ... ENABLE ROW LEVEL SECURITY` migration），但**必須先確認那 191 筆無租戶歸屬的凍結副本開了之後會不會變成誰都讀不到**，否則會打壞既有功能。

---

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

分兩層說，這兩層的答案不一樣，不要混在一起看。

### 第一層：「這幾條存在嗎」——**可信**

三條發現我都自己開檔核對過，行號逐一對上（**而且是對 HEAD 核對的，不是對掃描當下的版本**）。F1 額外有「同檔另外兩支同類函式都有記路」這個佐證；F3 的解析器行為我實際跑過確認；第 5 節那五項證偽也都是實測出來的，不是讀程式碼推的。

面板狀態：**三個獨立檢查員對 3 條發現各投一票，9 票全數投出，沒有漏投，三條都是三比零一致通過**，沒有任何一條的嚴重度被調降。驗證章 `verified`。

唯一的例外是 **F3 的可信度標「低」**——原因寫在該條的核對註記裡：整條路徑是讀程式碼推出來的，中間有一個前提（寫入當下翻譯列已存在、交易得以 commit）我沒有實測。**這是研究員與我自己對證據強度的誠實標示，不是面板有人反對。**

### 第二層：「只有這幾條嗎」——**這 30 支檔可以說掃過了，但有兩個保留**

面板完整跑完，這一點和 FR-077 R1 那種「面板全滅只能撈檔案」的情況不同，所以**這個範圍可以宣稱掃過了**。

但有兩個保留要說清楚：

1. **研究員沒有交出「我讀了哪些檔、哪些沒讀」的清單**。工具的 `coverage.research` 這次是空的，所以我無法證明 30 支檔真的每一支都被讀完。可以說的是「範圍完整交給了研究員」，不能說「每一支都被讀到底」。
2. **掃描期間套件的程式碼被改動了**（見下）。三條發現我都在 HEAD 重新核對過仍然成立，但**改動之後的版本沒有再跑一次完整掃描**，所以那些改動本身（雖然看起來只是註解與死碼清理）沒有被工具檢查過。

**掃描期間的版本漂移**：掃描從 `151c5f88` 開始跑，跑到一半有平行 session 對 jedi monorepo commit，HEAD 移到了 `45f14ad9`。差異我逐行看過，**是註解改寫與死碼刪除，沒有行為變更**：`bpmn_uilts.py` 改了一個 logger 名稱、把三段施工日誌型註解換成精簡版、刪掉檔尾的 `__main__` 除錯區塊；另外刪了三個 `.bpmn` 範例檔，`infra/models/__init__.py` 與 `common/enum/error_code.py` 有小幅調整。**三條發現的程式碼邏輯都沒被碰到**，本報告引用的行號全部是 HEAD (`45f14ad9`) 的行號。驗證章上記的仍是掃描實際跑的版本 `151c5f88`（標 `dirty`，因為當時工作區有未提交變更）。

---

## 8. 執行概況（工程師看的）

| 項目 | 數字 |
|---|---|
| 掃描版本 | `151c5f88c2b012e0368f0e47014d25300cbaf76a`（dirty；HEAD 期間移至 `45f14ad9`） |
| 範圍檔數 | 30（27 支 `.py` ＋ 3 支 `.bpmn` 範例圖；`git ls-files` 原始輸出 31，多的一支是 `.DS_Store`） |
| effort / focus | `low` / `attack-surface` |
| 研究員 派出／回報 | 2 / 2 |
| 候選發現 | 4（去重後 3） |
| 面板票數 | 9（3 條 × 3 個檢查員），**全數投出** |
| 達到共識的發現 | 3（皆 3:0 一致通過） |
| 未經檢查的候選點 | 0 |
| 嚴重度被調降的發現 | 0 |
| 交給下一輪驗證的候選 | 0 |
| 中途遺失的候選 | 0 |
| 驗證輪數 | 1 |
| 驗證章 `status` | **`verified`** |
| 總耗時 | 約 7 小時 15 分（agent 12 個，11 個完成、1 個出錯） |
| 產物位置 | `jedi-flow-engine/CLAUDE-SECURITY-20260912-120138/`（`.gitignore` 內，不進版控） |
| run ID | `wf_c14982f8-67a` |

---

## 9. 建議的後續處置（給首腦裁決，本棒不自行開卡）

| 發現 | 建議 | 理由 |
|---|---|---|
| **F1** | 開修正卡，優先序高於 F2／F3 | 這是三條裡唯一「外部可觸發、後果是全系統不回應、且無辜使用者會替攻擊者觸發」的。修法明確（加記路 ＋ 消掉重算），範圍限縮在單一檔案的三支函式。**第 5 節第 3 項的三支死碼方法建議併進同一張卡順手刪掉** |
| **F2** | 開卡，但屬基礎建設不綁出貨 | 要先進內網才打得到，且修法（Nexus 上 https ＋ lock 進版控）牽涉部署與團隊流程，不是單一 repo 的程式修改。**注意這條不只影響 jedi-flow-engine，整個 monorepo 與主專案都是同一組設定**，應該當成跨 repo 的一張基礎建設卡 |
| **F3** | 開卡，可與 F1 合併 | 同一個套件、同一類根因（不信任存進來的 XML）、修改的檔案不同但相鄰。合併成一張「BPMN 輸入強固化」卡比拆兩張省事，**但寫入端的修改會動到宿主 route，要注意跨 repo 派工衝突** |
| **資料庫隔離開關** | **獨立開卡**，不要併進上面任何一張 | 跨 H2／H3／本棒，修法單純但有風險——**要先確認 191 筆無租戶歸屬的凍結副本在開關打開後不會變成誰都讀不到**。這是 migration ＋ 資料修補的組合，性質與上面三條都不同 |
| **流程定義 GET 守門** | **併進 H2 既有守門卡**，不開新卡 | 錨點來源就是 H2，責任層在宿主 route 不在套件 |
