範圍:15 檔/1,612 行,跨兩個 repo。
- 套件側
jedi-compliance-audit14 檔:接縫檔audit_round_app_service.py,階段歷程、回退標記兩組(entity/repo 介面與實作/domain service/mapper/model),加上planning_readiness_checker.py。- 主專案側 1 檔:
app/flow_control/service/prep_job_generation_service.py。掃描工具:Claude Code 官方
claude-securityplugin,effort low,只看正式程式碼。兩側各掃一次。 掃描基準:套件側eafc7ae511d5(monorepo 主 checkout,feature/review);主專案側8ac028f8ccd7(feature/review)。兩邊工作區都有其他 session 未提交的改動。 驗證章:兩次都是 verified。只掃不修(掃描當時紀錄;後續修正見 README)。
卡片要追的「歷程與回退兩張表只用輪次編號查」,實際情況是:寫入這兩張表的每一條路都先過了角色檢查,而且檢查的就是同一個輪次;讀取只有一條路(階段歷程),那條就是總表第 52 項。回退標記的三支讀取方法確認是死碼,整個系統沒有任何地方呼叫。
工具這次報的三條,和 C2a 套件側那三條是同一批(輪次清單、單筆輪次、階段歷程)。原因是接縫檔 audit_round_app_service.py 兩棒都在範圍內。本棒沒有新增漏洞。
規劃完成度檢查的「查不到資料就放行」確認只是提醒、不是權限檢查;覆核輪產生證據蒐集任務時,歸屬也是伺服器決定的,這兩件都不是洞。
「階段歷程」是每一輪稽核往前推、往回退的時間軸,經理退回時要填理由,會記在這裡。「回退標記」是退回時,把已經產生的東西(凍結的 SSP、稽核計畫、改善計畫)標成「作廢」,不真的刪掉。這兩張表沒有客戶欄位、資料庫隔離也沒開,所以程式層只要漏一處,資料就一路通到底。
要回答:
| # | 這是什麼問題 | 出事會怎樣 | 要先有什麼才打得到 | 該補檢查的位置 | 哪一側發現 | 跟總表的關係 |
|---|---|---|---|---|---|---|
| C2b-1 | 階段歷程完全不守門,網址上的專案編號被忽略 | 同公司非成員看得到經理親手寫的退回理由與操作人;若新客戶機器上輪次表沒開隔離,別家客戶也打得到 | 登入帳號+知道輪次編號 | 套件側 audit_round_app_service.py:870(list_stage_transitions 起點);主專案側 api/flow_engine/routes/stage_rollback_route.py:56(get 起點) |
套件側掃描(面板 3:0,降為輕) | =第 52 項,既有案、不另計;與 C2a-3 同一條 |
| C2b-2 | 輪次清單不問你是不是專案成員 | 同 C2a-1 | 同 C2a-1 | 同 C2a-1 | 套件側掃描(面板 3:0,中) | =C2a-1=第 57 項,重複、不另計 |
| C2b-3 | 單筆輪次只看輪次編號 | 同 C2a-2 | 同 C2a-2 | 同 C2a-2 | 套件側掃描(面板 3:0,輕) | =C2a-2,重複、不另計 |
主專案側(prep_job_generation_service.py)零發現:研究員讀完沒有提出任何可疑點,面板沒有東西可投票。
上表三條都經過三人面板投票;第 4、5 節是我自己開檔追的,沒有經過投票。
三條都和 C2a 報告第 4.1~4.3 節是同一件事,場景與修法請看 scan-C2a.md。這裡只補本棒角度多看到的一點:
C2b-1 階段歷程:面板特別指出,歷程表是在 FR-050 的 migration(2026-07-19-fr050)裡刻意不開隔離的,設計時預期「一定先經過輪次表查詢,由輪次表的隔離擋」。但輪次表在出貨版 02-schema.sql 沒開隔離(C2a 第 6 節),這個前提在新客戶機器上不成立。面板把這點列為「跨客戶」的前置條件。
修正分支已補:套件 fix/security-b1 的 list_stage_transitions 已補上「網址專案要和輪次歸屬一致,不一致回 404」與成員檢查(面板引用 commit 3e82b625),但和 C2a 一樣還沒合回,也同樣是「選填參數、沒傳就不檢查」的寫法。
我把整個系統(套件 monorepo 全部套件+主專案 app/api/di_containers/common)裡碰這兩張表的地方全部找出來:
階段歷程表(round_stage_transitions)
| 動作 | 誰呼叫 | 用哪個輪次 | 呼叫前有沒有驗 |
|---|---|---|---|
| 寫(推進) | 主專案 stage_advance_service.py:442 advance_stage |
wf_ctx["round"],由網址上的輪次編號撈出 |
⚠️ 見 5.4:角色是用網址上的專案查的,不是用輪次自己的專案 |
| 寫(回退 ×3) | 套件 rollback_to_planning/rollback_to_audit_planning/rollback_to_auditing → _record_transition |
同一筆輪次 e.id |
✅ 先用輪次自己的 project_id 驗經理 |
| 讀 | 套件 list_stage_transitions(:877) |
網址上的輪次 | ❌ 沒驗,=第 52 項 |
回退標記表(round_rollback_supersessions)
| 動作 | 誰呼叫 | 呼叫前有沒有驗 |
|---|---|---|
| 寫 | 只有套件 _mark_superseded(:892),被三支 rollback_to_* 呼叫,標的是同一輪的 ssp_id/assessment_plan_id/poam_id |
✅ 先用輪次自己的 project_id 驗經理 |
讀 list_by_round/list_by_entity_ids/is_superseded |
沒有任何呼叫者 | 不適用 |
結論:repo 只用輪次編號過濾本身不是問題(repo 層本來就不該守門),關鍵在呼叫端。寫入路徑的呼叫端都已驗過,讀取路徑只有第 52 項那一條漏了。
RoundRollbackSupersessionDomainService 的 is_superseded、list_by_entity_ids、list_by_round,全系統 grep 沒有任何呼叫者。主專案的 DI 只把這個 domain service 注入給 AuditRoundAppService,而那裡只呼叫 .add()。
白話說明:系統目前「只記作廢、從不讀作廢」,所以沒有任何畫面會根據這張表把東西藏起來或顯示「已作廢」。這是功能面的缺口(回退後被標作廢的 SSP、稽核計畫、改善計畫,列表上看不出來),不是資安問題。DEV 目前這張表有 13 筆、歷程表有 48 筆(13:53 唯讀實查)。
建議列進 FR-092 死碼清單,或者確認產品是否本來要做「作廢顯示」卻漏做了(第 7 節)。
planning_readiness_checker.py 唯一的呼叫者是主專案 oscal_stage_handlers.py 的 LaunchAuditOnCompleteHandler.execute(「規劃」階段推進時)。追下去的順序是:
StageAdvanceService.advance_stage 先做角色驗證(stage_advance_service.py:292,不符就 403),force=true 還要求必須是經理。force=true)就放行。launch_audit,那裡又再用輪次自己的專案驗一次經理。所以這支檢查前後都有真正的權限檢查夾著,它本身只決定「要不要跳提醒」。它失敗時放行,最壞情況是「該跳的提醒沒跳」,使用者本來就能按確認跳過,不構成安全問題。
另外確認兩個子查詢都沒有外洩:檢查 A(未完成的證據蒐集任務數)只回一個數字;檢查 B(缺哪些角色)只回角色名稱(manager/auditor/reviewer),都是給已經通過角色檢查的人看的。
advance_stage 的角色驗證(stage_advance_service.py:292)是 _get_user_role(project_uid, user_id),project_uid 來自網址;但它推進的輪次 round_uid 也來自網址,兩者沒有比對是否屬於同一個專案。理論上,A 專案的經理可以填「A 專案+B 專案的輪次」,通過第一道角色檢查。
但實際上打不穿:推進後真正改資料的 handler(launch_audit/start_auditing/finalize_audit/close_round)都會在套件裡再用輪次自己的專案驗一次角色,所以會在那裡被擋。歷程表寫入(5.1 表中第一列)是在 handler 成功之後才寫,所以也寫不進去。
漏網之魚只有兩個:
review_decision 這個 handler 不呼叫套件、不做第二道檢查。但 DEV 的 stage_objects 顯示內建流程沒有綁 review 階段(程式註解也說「本期 builtin 無 review stage」),目前打不到。get_current_stage_info 同樣只用網址專案驗角色,而且不會擋,只回「你能不能推進」,會把 B 輪次的階段、進度、前置條件結果回給 A 專案的人。已查證:這條已在 FR-088 H1 報告登記(scan-H1-stage-advance-rollback.md F4:get_current_stage_info 用網址專案查角色、用網址輪次撈資料、兩者不核對;該報告第 144 行也建議 advance_stage 一併補),既有案、本棒不另計。
prep_job_generation_service.generate 有兩個呼叫者:
| 呼叫者 | project_id/round_id/control_ids 從哪來 |
|---|---|
套件 _generate_reverify_prep_jobs(覆核輪) |
全部取自伺服器端的輪次物件:round_entity.project_id、round_entity.id、由母輪稽核結果推導的「不符合控制項」 |
主專案 project_start_app_service._generate_prep_jobs(專案成立) |
由剛建立的專案與資源庫推導 |
請求內容帶不進任何歸屬欄位。新建的流程執行與任務由資料庫觸發器 trg_wfexec_fill_tenant_org_insupd、trg_jobexec_fill_tenant_org_insupd 自動補上客戶與部門(DEV 13:53 實查)。流程範本的 clone 走一般 insert,要受隔離規則 with_check 約束,範本表的隔離在 DEV 是開的。
一個小地方要提一下:generate 會直接用 SQL 更新 oscal.catalog_control_parts.props(專案成立模式才做)。它更新的是「這個專案的 cloned catalog」裡的 AO part,id 來自伺服器端查詢,不是外部輸入,所以不是洞。
| 表 | DEV 隔離 | 規則數 | 目前筆數 |
|---|---|---|---|
compliance.round_stage_transitions(歷程) |
關 | 0 | 48 |
compliance.round_rollback_supersessions(回退標記) |
關 | 0 | 13 |
compliance.project_audit_rounds(輪次) |
開 | 4 | 不適用 |
出貨版 02-schema.sql 三張都沒開,詳見 C2a 報告第 6 節。
review_decision 這個 handler 沒有第二道檢查,那時 advance_stage 自己的歸屬比對就會變成唯一防線。建議修 H1 F4 時照該報告第 144 行的建議,把 advance_stage 也一併補上。「工具報的三條存在嗎」:可信度高。套件側 9 票全數投出,三條都是 3:0;主專案側零候選。三條都是 C2a 已報的重複項。
「第 5 節」:runner 自行開檔核對,未經投票。關鍵事實都實查過:
app/api/di_containers/common。stage_advance_service.py 與 oscal_stage_handlers.py。「只有這些嗎」:不保證。
| 項目 | 套件側 | 主專案側 |
|---|---|---|
| 掃描範圍 | 14 檔/1,386 行 | 1 檔/226 行 |
| 基準 commit | eafc7ae511d5(dirty) |
8ac028f8ccd7(dirty) |
| 檔位 | effort low,只看正式程式碼 | 同左 |
| 研究員 | 派 2 支,回 2 支 | 派 2 支,回 2 支(兩支都回空) |
| 候選 | 3 條,去重後 3 條 | 0 條 |
| 投票 | 9 票全數投出 | 0 票(無候選) |
| 票型 | F1 3:0 中(輪次清單);F2 3:0 輕,原中(階段歷程);F3 3:0 輕(單筆輪次) | 不適用 |
| 驗證章 | verified(CLAUDE-SECURITY-REVISION-eafc7ae511d5-dirty.json) |
verified(CLAUDE-SECURITY-REVISION-8ac028f8ccd7-dirty.json) |
| 工具 run ID | wf_2e46bc79-203 |
wf_f44a65bc-93b |
| 耗時 | 約 13 分鐘(11 個 agent,零失敗,1 個回傳空結果) | 約 1 分鐘(2 個 agent,零失敗) |
| 工具原始報告 | 套件 repo CLAUDE-SECURITY-20260924-054855/(未入版控) |
BE repo CLAUDE-SECURITY-20260924-054633/(未入版控) |
工具報告的 F 編號對照:套件側 F1=C2b-2(輪次清單)、F2=C2b-1(階段歷程)、F3=C2b-3(單筆輪次)。
主專案側比套件側早跑:決策者先送了主專案側的指令,兩次掃描互不相依,不影響結果。