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

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

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


§1

🔴 一句話結論

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

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


§2

這一棒在檢查什麼

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

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


§3

🔴 最高優先項的答案:漏接一個,但不是漏洞

卡片點名要查 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 條,✅ 已修。
  • 確認權限判斷少接一個零件那件:結論本來就是「不是漏洞」,無修正卡。
§4

找到什麼

淨新增 1 條

# 嚴重度 這是什麼問題 出事會怎樣 要先有什麼才打得到 在哪裡
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() 取。

另外 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)也有一份,修的時候要一起看。


§5

順帶查證的幾件事(都沒問題或不成立)

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

§6

這份結果可信到什麼程度

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

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

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

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

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

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

§7

執行概況

項目 數值
範圍 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 卡片內。

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