範圍:4 檔,共 1,747 行(
api/flow_control/routes/job_force_start_route.py、app/flow_engine/service/job_force_start_service.py、app/flow_engine/service/join_wait_inspector.py、app/flow_engine/service/workflow_execution_service.py)。 掃描工具:Claude Code 官方claude-securityplugin,effort low,只看生產程式碼。 掃描基準 commit:9abe5e64e895(工作區有其他 session 還沒提交的改動,所以 stamp 標了-dirty)。 驗證章:verified。這一棒只掃描,不修改程式。
卡片問的頭號問題,答案是「只檢查網址上的專案,沒有回頭核對任務屬於哪個專案」。 甲專案的管理人,只要在網址上填自己的專案,後面接乙專案的任務編號,就能把乙專案卡住的任務強制推成進行中。乙專案的管理人沒有同意,也不會知道;稽核紀錄上寫的還是甲專案。
同一個缺口也出現在「查詢卡住狀態」那支:它連「你是不是這個專案的人」都沒有檢查。任何登入者都能讀到別的專案正在等哪幾條分支,包括任務名稱、狀態和任務編號。這些任務編號剛好就是打上一條需要的材料。
兩條都是同一家客戶內、跨專案的問題。資料庫的客戶隔離已經開啟(下面第 5 節查過),所以不會跨到別家客戶。
流程引擎在 FR-088 之後改動的部分(合流閘道等兄弟分支、刪掉的 230 行死碼),沒有找到新問題,也沒有順手刪掉守門(第 5 節)。
FR-112(09-20)為了解決「流程卡住、只能直接改資料庫」,在規劃頁加了兩個功能:
兩支網址長這樣:/grc/project/<專案編號>/job/<任務編號>/blocked-status 和 .../force-start。網址同時帶了專案和任務,這種形狀最常出的錯,就是「檢查的是甲、動手改的是乙」(總表第 118、119 項是同形狀的先例)。所以這一棒的重點是:專案和任務有沒有被核對成同一組。
| # | 這是什麼問題 | 出事會怎樣 | 要先有什麼才打得到 | 該補檢查的位置 | 嚴重度 | 來源 |
|---|---|---|---|---|---|---|
| W8-1 | 強制開始只檢查「你是不是網址上那個專案的管理人」,沒有核對任務是不是這個專案的 | 甲專案管理人可以把乙專案卡住的任務推成進行中,乙專案的指派人會收到通知,稽核事件記在甲專案名下 | 在同一家客戶裡至少是一個專案的管理人,並且知道對方的任務編號 | app/flow_engine/service/job_force_start_service.py:119(_load,查到任務之後,要核對它屬於網址上的專案);權限檢查 :107 要改成檢查任務實際所屬的專案 |
中 | 工具 3:0 票+本棒開檔核對 |
| W8-2 | 查詢卡住狀態完全沒有檢查專案成員,也沒有核對任務屬於哪個專案 | 同一家客戶的任何登入者,都能讀到別的專案在等哪些分支任務:名稱、流程節點編號、狀態、任務編號 | 同一家客戶的任何登入帳號(要有專案模組授權),並且知道一個任務編號 | app/flow_engine/service/job_force_start_service.py:60(inspect 開頭,要補「任務屬於網址上的專案」和「你是這個專案的參與者」兩道檢查) |
低 | 工具 3:0 票+本棒開檔核對 |
兩條都是新的,總表沒有登記過。
場景
某家客戶裡有兩個專案:甲是小王負責的內部稽核,乙是另一組人負責的外部稽核。小王是甲的管理人,跟乙沒有任何關係。乙專案有一個「主管覆核」任務,正在等兩條平行分支(兩個組員各自收證據)都做完才能開始。
小王拿到那個覆核任務的編號(取得方式見 W8-2)。他在規劃頁的網址上填自己甲專案的編號,後面接乙專案的任務編號,然後按「強制開始」。系統只確認「小王是甲的管理人」,檢查通過,接著就把乙專案的覆核任務從「待辦」改成「進行中」:
project_uid=甲,事後查紀錄,只會看到甲專案的管理人在甲專案做了一次強制開始,看不出他改的是乙專案。為什麼會這樣
_require_manager(job_force_start_service.py:107-117)用的是網址上的 project_uid:查出專案編號,確認呼叫者是該專案的管理人。_load(:119-132)只用 job_uid 查,查到任務後沒有問「這個任務是哪個專案的」。project_uid 傳進 _load,但函式裡根本沒用到。為什麼當初沒有從任務反查專案:commit 525989b7a 的說明寫得很清楚,是刻意的。DEV 上只有約 11% 的流程能從流程反查回專案,如果走反查這條路,九成的管理人會被誤擋。所以改成直接用網址上的專案檢查權限。這個取捨本身合理,但少了一步:用網址上的專案檢查權限可以,前提是要先確認任務真的屬於這個專案。
嚴重度為什麼是中不是高
:69、:75 兩道前置條件),不是什麼任務都能改。修法方向(給修正卡參考,不是定案)
在 _load 查到任務後,確認任務屬於 project_uid,不屬於就回 404。專案內的任務要怎麼認歸屬,系統裡已經有現成的判斷方式:compliance.task_assignees.project_id。規劃頁存指派人時(infra/flow_control/repository/flow_control_job_repo_impl.py:600-603)、輪次凍結的判定(同檔 :327-351)都用它。要注意的陷阱:如果某些卡住的任務在 task_assignees 裡沒有資料,這樣核對會把它們擋掉,等於又碰到 525989b7a 想避開的那個「誤擋」問題。所以修之前,要先在 DEV 查一次「待辦且卡在合流的任務,有多少筆沒有 task_assignees」。
場景
同一家客戶裡的一般員工小李,沒有參與乙專案。只要他手上有一個乙專案的任務編號,就能在網址上隨便填一個專案編號(甚至是不存在的),後面接那個任務編號,呼叫「查詢卡住狀態」。系統會回傳乙專案那個任務正在等的每一條分支:任務名稱、流程節點編號、目前狀態,以及每條分支任務的編號。
拿到這些任務編號後,小李如果同時是某個專案的管理人,就可以拿來打 W8-1;也可以拿去試其他「只看任務編號」的網址。
為什麼會這樣
job_force_start_route.py:38-39)。inspect(job_force_start_service.py:57-61)直接呼叫 _load,沒有任何成員檢查。說明文字寫「任一專案參與者可看」,但程式沒有做這件事;project_uid 同樣沒有被用到。嚴重度為什麼是低:只能讀,讀到的是任務名稱、狀態這類流程資訊,不是證據內容;而且要先知道一個任務編號。它的主要風險在於幫 W8-1 提供材料。
修法方向:在 inspect 開頭補兩道檢查,跟 W8-1 用同一套:「任務屬於網址上的專案」,以及 assert_project_participant(專案參與者)。
job_force_start_service.py:69-70)。已完成、已取消、進行中、已退回的任務都會被擋下,回 412(GRC_412055)。:72-76)。上游不是合流閘道(None)或者已經等到了(空清單),一律擋下,回 412(GRC_412056)。join_wait_inspector.find_pending_branch_ends,引擎端在 workflow_execution_service.py:494-502)。兩者不會各說各話,所以沒有辦法讓畫面判定和引擎判定不一致,從中找縫隙繞過。這一項不成立。
「退回」有一道「輪次開始執行後就凍結、不可退回」的檢查(workflow_execution_service.py:167-188)。強制開始沒有這道檢查,但這是符合語意的:任務卡住本來就發生在執行期(輪次已啟動、已凍結),強制開始正是給執行期用的出口。如果加上凍結檢查,這個功能就完全不能用了。
別輪的任務同樣受 5.1 兩道條件限制:已經走完的輪次,任務不是「待辦」,推不動。
這一項不成立。
reason 欄位:有長度上限,會原樣寫進稽核事件api/flow_control/serializers/job.py:410(validate.Length(max=200)),超過就回 400。W5-2 那種「欄位超長」的問題不會發生。job_force_start_service.py:89,經 jedi_common.utils.audit_log.audit 組成 [AUDIT:JOB_FORCE_STARTED] ... reason=<原文>),沒有過濾換行字元。實務影響很小:只有管理人能填,最多 200 字,而且寫進 system_logs 時是一整筆資料,不會被拆成兩筆。只在純文字日誌檔裡,可能讓人看到一行假的 [AUDIT:...]。列為觀察,不另計。 如果要一起處理,應該在 audit() 那層統一把換行跳脫掉,不要只修這一個欄位。FR-088 H4 掃描時的基準是 8fc8c1c1。之後這支檔案有三個 commit 改過,合計 +45/−230 行:
| commit | 做了什麼 | 核對結果 |
|---|---|---|
0b1998f83(FR-092 第 9 棒) |
刪掉 6 個死方法,約 231 行 | 逐一 grep,被刪的 add_job_comment、get_ext_workflow_execution_by_uid、兩支 _and_pager 等等,在 api/ app/ common/ di_containers/ domain/ infra/ 都已經沒有任何呼叫者。被刪的程式裡沒有 assert_*、ForbiddenError 這類守門。流程討論區(H4-1)現在的寫法走 element_variable_service(api/flow_engine/routes/flow_engine_route.py:105),不受這次刪除影響,H4-1 的狀態沒有改變 |
efe31bf67(CM-1964) |
合流閘道後面的任務,要等兄弟分支都結束才啟動 | 在 complete_job、complete_main_workflow_job 兩個推進迴圈裡加了一道 _is_parallel_join_ready(:617、:754)。這道檢查只會讓推進變嚴格,不會放寬。兩處的原有守門(H4-2 報的「只檢查是不是成員」)沒有改變 |
525989b7a(CM-1980) |
把合流判定抽成 join_wait_inspector.py 共用 |
判定邏輯原封不動搬出去,引擎和強制開始共用同一支 |
FR-088 H2、H4 已經登記的問題(H4-1 討論區沒有守門、H4-2 完成/退回任務只檢查是不是成員),現在都還是原樣,不重報。
H4-3 報告時,workflow_executions、projects 的隔離是關著的。本棒在 DEV 重查(2026-09-23 22:46,唯讀 pg_class.relrowsecurity):compliance.job_executions、compliance.workflow_executions、compliance.projects、compliance.task_assignees 四張表都是 true。job_executions 的政策是「超級管理員,或租戶在允許範圍內」(scripts/sql/2026-09-14-fr094-cm1770-rls-poams-evidence-jobs-parse-jobs.sql:130-160)。
所以 W8-1、W8-2 的範圍確實停在「同一家客戶內」,不會跨到別家。STG、POC 沒有查。
「工具報的兩條存在嗎」:可信度高。 三位獨立檢查員從三個角度(打不打得到、影響多大、中間有沒有別的防線)各投一票,6 票全數投出,兩條都是 3:0 成立,嚴重度也沒有被調降。檢查員確認過中間沒有其他防線把任務和專案綁在一起,包括路由、服務和任務查詢的 repository。
「第 5 節」:runner 自行開檔核對,未經三人面板投票。 關鍵事實都有實查:四張表的隔離狀態有查 DEV;被刪方法有沒有呼叫者有 grep;reason 的長度上限和寫入方式有開檔確認。
「只有這些嗎」:不保證。
workflow_execution_service.py 整支 1,374 行都在範圍內,但研究員的重點放在 FR-088 之後改動的部分和新端點;其他部分 H4 已經掃過。task_assignees 認歸屬會不會誤擋」還沒查資料,修正卡要先查。| 項目 | 數值 |
|---|---|
| 掃描範圍 | 4 檔,共 1,747 行 |
| 基準 commit | 9abe5e64e895(工作區 dirty,有平行 session 的改動) |
| 檔位 | effort low,只看生產程式碼 |
| 研究員 | 派出 2 支,回來 2 支 |
| 原始候選 | 2 條,去重後 2 條 |
| 投票 | 三位檢查員 × 2 條 = 6 票,全數投出 |
| 票型 | W8-1 3:0(三票都評中);W8-2 3:0(三票都評低) |
| 驗證章 | verified(CLAUDE-SECURITY-REVISION-9abe5e64e895-dirty.json) |
| 工具 run ID | wf_c314acba-b10 |
| 耗時 | 約 13 分鐘(8 個 agent,零失敗;其中 1 個回傳空結果,不影響候選數) |
| 工具產出的原始報告 | CLAUDE-SECURITY-20260923-143410/(沒有進版控) |
task_assignees 裡沒有資料」。如果數量不少,要改用別的方式認歸屬,不然會回到 525989b7a 想避開的誤擋問題。audit() 要不要統一跳脫換行(5.3):影響所有稽核事件,不只這一支。建議列成低優先的共用改動,不用併進這張卡。沿革見 FR-115 LOG 與 git log。