檢查日期 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-309ssp_context_resolver 在 di_containers/ 下只命中 import 與註解,沒有任何一處實際接線漏接是已知且刻意的——組裝處第 305-307 行的註解自己寫明了:「注意 ssp_context_resolver 至今未 wire,故 checker 走相容分支」。
關鍵在於漏接之後走的那條路比正常路更嚴格,不是更鬆:
| 正常路(零件接上時) | 實際走的路(零件沒接) | |
|---|---|---|
| 讀取專案的計畫 | 要求你是這個專案的成員 | 要求你是這個專案的成員(相同) |
| 讀取公用範本 | 放行(只要登入就行,範本是共用資源) | 當成專案計畫處理 → 要求你是成員(更嚴) |
| 修改專案的計畫 | 要求你是負責人 + 稽核計畫還能改 | 相同 |
| 修改公用範本 | 要求你是系統管理員 | 當成專案計畫處理 → 要求你是負責人(更嚴) |
所以漏接的後果是「處理公用範本的那條新路徑沒有啟用」,不是「權限檢查失效被繞過」。
結論:前面九棒的守門結論不需要重新評估。(另註:ssp_context_factory 若也缺,程式會直接拋出帶明確訊息的錯誤,不是靜默失效——但它有接上,不成立。)
現況(2026-10-01):本棒各條後來的處理結果如下(過程紀錄保留,不改)。
- 唯一淨新增那條(推進/退回階段的操作者顯示名稱由前端填)=總表第 128 項/M06 第 12 條,✅ 已修。
- 確認權限判斷少接一個零件那件:結論本來就是「不是漏洞」,無修正卡。
| # | 嚴重度 | 這是什麼問題 | 出事會怎樣 | 要先有什麼才打得到 | 在哪裡 |
|---|---|---|---|---|---|
| 1 | 🟡 中低 | 推進或退回稽核階段時,「操作者是誰」這個顯示名稱是前端送什麼就存什麼,不是用登入身分 | 有權推進階段的人,可以讓稽核歷程時間軸上顯示是別人做了這個動作(例如把自己的操作掛到主管名下)。真正的帳號名稱另有欄位仍記錄正確,所以查得出來,但畫面上給人看的那一欄不可信。同一包資料還夾帶一個「強制通過」開關,可以跳過規劃完成度的檢查 | ① 你得是這個專案的參與者 ② 你的角色要通過該階段本來就有的角色檢查 (換句話說:不能讓外人冒名,只能讓有權限的人冒用另一個有權限的人) |
存進去:app/flow_engine/service/stage_advance_service.py:450(operator_nickname)與 :413(留言作者)來源: api/flow_engine/routes/stage_advance_route.py:58-59(用 setdefault,前端有送就用前端的)退回那條同樣寫法: api/flow_engine/routes/stage_rollback_route.py:38-39 |
為什麼是中低不是高:打得到的人本來就有權推進階段,不是外人;而且真實帳號仍被記錄,事後查得出來。它傷的是稽核軌跡的可信度,不是資料的存取權。但對一套合規稽核系統來說,「誰批准了這一關」被偽造是有份量的——稽核軌跡本身就是產品賣點。
怎麼修:在兩支路由把身分欄位改成覆寫而不是補預設:
# 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() 取。
掃描也撞到下面三條,但都是前面棒次已經報過的同一批問題。列在這裡是讓你知道「這一棒獨立驗證後同意前面的判斷」,不要當成新發現加總:
| 撞到的問題 | 已報於 | 一句話 |
|---|---|---|
| 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 位檢查員派出後零票回收。
所以這份報告沒有驗證章。與前面各棒(有投票背書)不是同一個標準。建議修正前再由人複核一次那兩行。
| 項目 | 數值 |
|---|---|
| 範圍 | 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 卡片內。
實證有效:第二次研究階段零重啟一次跑完,而第一次同一範圍被砍三次。這條經驗值得沿用到其他「接縫發散」型的棒次。