---
title: O9b 檢查結果：接線總裝、稽核階段掛鉤、人員對帳
---

# O9b 檢查結果：接線總裝、稽核階段掛鉤、人員對帳

> 檢查日期 2026-09-22｜對應卡片 CM-2070｜FR-113 最後一棒
> ⚠️ **本次投票階段未跑完**（決策者因額度中止），可信度說明見第五節

---

## 🔴 一句話結論

**掃完了，淨新增 1 個問題（中低度）：推進或退回稽核階段時，「是誰做的」這個名字是前端送什麼就記什麼，可以冒用別人的名義。另外三條是前面棒次已經報過的同一批問題，這一棒只是再次撞到，不重複計數。**

**卡片指定要查的最高優先項——「權限判斷程式的五個依賴有沒有全部接上」——答案是：有一個沒接，但這不是漏洞，前面九棒的結論不需要重新評估。** 理由見第三節，這是本棒最重要的結論。

---

## 這一棒在檢查什麼

系統裡有一層「把所有東西接起來」的程式：哪個網址對到哪支功能、哪個物件由誰建立、稽核流程推進時會回頭改哪些資料、以及「把 Word 文件裡的人名對到系統使用者」的比對邏輯。這一棒掃的就是這 17 支檔（1,903 行）。

之所以排在最後，是因為它是**驗證層**：前面各棒查的是「這支功能有沒有檢查權限」，但檢查權限的那支程式本身是靠「依賴注入」組起來的——如果組裝時漏接一個零件，那支程式寫得再完整也不會執行，而且不會報錯。只有這一棒問得出這個問題。

---

## 🔴 最高優先項的答案：漏接一個，但不是漏洞

卡片點名要查 `SspPermissionChecker`（判斷「你能不能碰這份系統安全計畫」的那支程式）的依賴有沒有全部接上。

**查證結果**：

| | 數量 | 明細 |
|---|---|---|
| 程式宣告需要的零件 | 4 個 | `ssp_project_resolver`／`project_participant_domain_service`／`ssp_context_resolver`／`ssp_context_factory` |
| 組裝時實際接上的 | **3 個** | 缺 `ssp_context_resolver` |

- 宣告處：`common/authz/ssp.py`（`__init__`，四個參數預設值都是 `None`）
- 組裝處：`di_containers/oscal/oscal_containers.py:300-309`
- 全專案 grep `ssp_context_resolver` 在 `di_containers/` 下只命中 import 與註解，**沒有任何一處實際接線**

### 為什麼這不是漏洞

漏接是**已知且刻意**的——組裝處第 305-307 行的註解自己寫明了：「注意 `ssp_context_resolver` 至今未 wire，故 checker 走相容分支」。

關鍵在於**漏接之後走的那條路比正常路更嚴格**，不是更鬆：

| | 正常路（零件接上時） | 實際走的路（零件沒接） |
|---|---|---|
| 讀取專案的計畫 | 要求你是這個專案的成員 | 要求你是這個專案的成員（相同） |
| 讀取公用範本 | **放行**（只要登入就行，範本是共用資源） | 當成專案計畫處理 → **要求你是成員**（更嚴） |
| 修改專案的計畫 | 要求你是負責人 ＋ 稽核計畫還能改 | 相同 |
| 修改公用範本 | 要求你是系統管理員 | 當成專案計畫處理 → **要求你是負責人**（更嚴） |

所以漏接的後果是「處理公用範本的那條新路徑沒有啟用」，不是「權限檢查失效被繞過」。

**結論：前面九棒的守門結論不需要重新評估。**（另註：`ssp_context_factory` 若也缺，程式會直接拋出帶明確訊息的錯誤，不是靜默失效——但它有接上，不成立。）

---

> **現況（2026-10-01）**：本棒各條後來的處理結果如下（過程紀錄保留，不改）。
>
> - 唯一淨新增那條（推進／退回階段的操作者顯示名稱由前端填）＝總表第 128 項／M06 第 12 條，✅ 已修。
> - 確認權限判斷少接一個零件那件：結論本來就是「不是漏洞」，無修正卡。

## 找到什麼

### 淨新增 1 條

| # | 嚴重度 | 這是什麼問題 | 出事會怎樣 | 要先有什麼才打得到 | 在哪裡 |
|---|---|---|---|---|---|
| 1 | 🟡 中低 | **推進或退回稽核階段時，「操作者是誰」這個顯示名稱是前端送什麼就存什麼**，不是用登入身分 | 有權推進階段的人，可以讓稽核歷程時間軸上顯示**是別人做了這個動作**（例如把自己的操作掛到主管名下）。真正的帳號名稱另有欄位仍記錄正確，所以查得出來，但**畫面上給人看的那一欄不可信**。同一包資料還夾帶一個「強制通過」開關，可以跳過規劃完成度的檢查 | ① 你得是這個專案的參與者<br>② 你的角色要通過該階段本來就有的角色檢查<br>（換句話說：**不能讓外人冒名，只能讓有權限的人冒用另一個有權限的人**） | 存進去：`app/flow_engine/service/stage_advance_service.py:450`（`operator_nickname`）與 `:413`（留言作者）<br>來源：`api/flow_engine/routes/stage_advance_route.py:58-59`（用 `setdefault`，前端有送就用前端的）<br>退回那條同樣寫法：`api/flow_engine/routes/stage_rollback_route.py:38-39` |

**為什麼是中低不是高**：打得到的人本來就有權推進階段，不是外人；而且真實帳號仍被記錄，事後查得出來。它傷的是**稽核軌跡的可信度**，不是資料的存取權。但對一套合規稽核系統來說，「誰批准了這一關」被偽造是有份量的——稽核軌跡本身就是產品賣點。

**怎麼修**：在兩支路由把身分欄位改成**覆寫**而不是**補預設**：

```python
# api/flow_engine/routes/stage_advance_route.py:58-59
ctx = dict(body.get("ctx") or {})
ctx["user_nickname"] = user_ctx.nickname   # 原本是 ctx.setdefault(...)
ctx["force"] = force                        # 同理，別讓前端從 ctx 夾帶
```

`stage_rollback_route.py:38-39` 同樣處理。更徹底的做法是不要讓身分資訊走 `ctx` 這包自由格式的資料，改在寫入資料庫那一刻直接從 `get_user_context()` 取。

### 另外 3 條是舊識，不重複計數

掃描也撞到下面三條，但都是前面棒次已經報過的同一批問題。列在這裡是讓你知道「這一棒獨立驗證後同意前面的判斷」，**不要當成新發現加總**：

| 撞到的問題 | 已報於 | 一句話 |
|---|---|---|
| Excel 匯入可清空覆寫任何看得到的範本，無權限檢查 | **O3a 問題 1、O9a 問題 1** | `ssp_excel_import_app_service.py:478` |
| Word 匯入同一個病，且確認時可換目標 | **O9a 問題 2** | `ssp_docx_import_app_service.py:384` |
| 刪程序書時驗的是網址上的計畫、刪的是另外給的編號 | **O1、O6（O6 明文點名要一起修）** | `ssp_control_implementation_service.py:890` |

第三條這一棒補了一個細節：同一個寫法在 `app/oscal/service/ssp_document_pool_service.py:129`（`delete_from_pool`）也有一份，修的時候要一起看。

---

## 順帶查證的幾件事（都沒問題或不成立）

| 卡片要我查的 | 結果 |
|---|---|
| 被註解掉的兩支匯入入口，程式是不是還通得到 | **通得到，但不是漏洞**——那兩支是 scoped 版的預覽／確認，前端改走非 scoped 版（`/ssp-excel-import/<id>/confirm`，有註冊）。真正的問題是改走的那支沒有權限檢查，即上表舊識第一條 |
| 稽核階段掛鉤三支有沒有權限檢查 | **一處都沒有，但屬設計**——它們不由網址直接呼叫，是流程引擎依 `round.status` 查表觸發的內部回呼；守門應該在「推進／退回」那個入口做，而那個入口的問題就是上面新增的第 1 條 |
| 匯入包還原那支有沒有權限檢查 | **沒有，但沒有網址通得到它**——只被 Excel 匯入管線內部呼叫，權限由管線入口承擔 |
| 人員對帳會不會自動建使用者 | **不會**——`person_reconciler.py` 全檔沒有任何建立／寫入動作 |
| 人員對帳會不會跨公司比對 | **簽章有收「哪家公司」這個參數，但查詢條件裡沒用它**（`person_reconciler.py:26、41`）。實際隔離靠資料庫那層的租戶機制。**這一條我標為未完成查證**——要坐實得去看資料庫隔離規則實際涵蓋哪些表，本棒因投票中止未及完成 |
| 三支資料表有沒有「屬於哪家公司」的欄位 | 未及查證（同上，中止） |

---

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

**分兩層講，這一棒兩層都有折扣，要照實說。**

### 第一層：「這些問題存在嗎」→ 可信，但沒有投票背書

新增的那 1 條是研究階段直接開檔讀出來的，證據鏈完整（檔名行號都核得到，我自己也開檔確認過 `setdefault` 那兩行）。**但三個獨立檢查員的投票沒有跑完**——決策者因額度中止，27 位檢查員派出後零票回收。

所以這份報告**沒有驗證章**。與前面各棒（有投票背書）不是同一個標準。建議修正前再由人複核一次那兩行。

### 第二層：「只有這些嗎」→ 這層折扣更大

- **範圍內漏掉兩項查證**（人員對帳的跨公司比對、三支資料表的公司欄位），上表已標明。
- 本棒第一次掃描（02:28 起）跑了 12.4 小時、研究員被砍三次、零產出，撞的是已知的工具缺陷（自動壓縮期間無輸出被誤判為當機）。第二次重跑才成功。**第一次的 12 小時完全沒有產出，不是「掃過了沒問題」。**
- 密鑰掃描（找有沒有把密碼寫死在程式裡）中途也被砍過一次，第二支跑完，但結果併入投票階段一起中止，**未取得結論**。

---

## 執行概況

| 項目 | 數值 |
|---|---|
| 範圍 | 17 檔／1,903 行 |
| 第一次嘗試 | 02:28–14:51（12 小時 23 分），研究員被砍 3 次，**零產出**，已停止 |
| 第二次嘗試 | 15:57 起 |
| ├ 研究階段 | 1 小時 52 分，**零重啟**（撐過第一次的死亡點），交出 4 條候選 |
| ├ 密鑰掃描 | 約 2 小時 14 分，中途被砍 1 次，第二支完成 |
| └ 投票階段 | 27 位檢查員派出，**0 票回收即中止**（決策者額度考量） |
| 淨新增問題 | **1 條**（中低度） |
| 重複計數已扣除 | 3 條（O1／O3a／O6／O9a 已報） |
| 驗證章 | **無**（投票未完成） |

### 為什麼第二次能跑完研究階段

第一次失敗後，依既有經驗（`feedback_scan_tool_stall_180s_context_budget_per_batch`）判定本棒屬「本體小、接縫發散」型——研究員為了判斷守門有沒有接上，會跑去全專案搜呼叫鏈，上下文因此堆爆。解法是**開工前先把接縫事實查好寫進卡片**（誰呼叫誰、守門在哪、租戶怎麼隔離），讓研究員不必出去挖。這批事實已 append 在 CM-2070 卡片內。

實證有效：第二次研究階段零重啟一次跑完，而第一次同一範圍被砍三次。這條經驗值得沿用到其他「接縫發散」型的棒次。
