檢查日期 2026-09-25|對應卡片 CM-2158(母卡 CM-2152)|檢查範圍 7 個檔案 1,908 行|工具 run ID
wf_14dcf3f9-ca2|掃描版本159a2fff(feature/review)
決策者點名的「批次完成把呼叫者當管理員」,寫在程式裡的那條預設走不到;真正的洞在隔壁:批次完成拿「網址上的專案」判斷你是不是管理員,卻不核對勾選的任務屬不屬於那個專案。 這跟本批其他五個入口是同一個病:只認網址專案,不認任務歸屬。
is_manager = True 這個預設值,只在「守門零件沒接上」或「網址沒帶專案編號」時才會生效。實際上零件有接,網址也一定帶專案編號,所以這條預設在正式路徑上走不到(第 59 項當年寫的「待核對」,核對結果是不成立)。但我手做實測(未經三人面板)發現另一個洞:你是專案 A 的管理員、又是專案 B 的旁觀者(viewer)時,網址填 A、勾選 B 的任務,系統就用「你是 A 的管理員」直接跳過「這張任務是不是指派給你」的逐筆檢查。DEV 上真的有這種身分組合的帳號。目前分支上這招換不到新能力,因為第 59 項的洞本來就讓 viewer 單筆完成任務;等修正分支(CM-2147)合回來,內層守門會擋住它。所以我建議不另計、併進第 59 項那張修正卡的驗收項。帶著前棒的形狀來看:U5/U6 的病是「背景工作用系統身分跑、沒核對專案歸屬」。U7 沒有背景工作,但同一個問題換了個入口出現:每個入口都只問「你是不是網址那個專案的人」,沒人問「你要動的這張任務是不是那個專案的」。
「任務」是稽核流程裡最小的工作單位(例如「上傳防火牆設定截圖」),掛在某個專案的某個控制項底下。這 7 支程式是任務的所有對外入口:
| 檔案 | 行數 | 角色 |
|---|---|---|
app/flow_control/service/job_import_service.py |
470 | 規劃頁「匯出/匯入任務 Excel」四步:匯出、上傳驗證、重新驗證、確認 |
api/flow_control/serializers/job.py |
417 | 任務 API 的輸入輸出格式 |
api/flow_control/routes/job_route.py |
259 | 任務的看、新增、修改、刪除、清單、「我的任務」 |
infra/readmodel/audit/flow_control_dashboard_repo_impl.py |
254 | 首頁儀表板統計 |
infra/readmodel/tasks/my_grc_jobs_query.py |
198 | 「我的任務」清單查詢 |
app/flow_control/service/job_batch_complete_service.py |
193 | 「批次完成任務」+完成後的彙總通知信 |
app/readmodel/service/my_jobs_app_service.py |
117 | 「我的任務」服務層 |
要回答的核心問題:任務編號是誰給的?系統有沒有核對這個編號屬於呼叫者有權限的專案?
| # | 這是什麼問題 | 出事會怎樣 | 要先有什麼才打得到 | 該補檢查的位置 | 嚴重度+為什麼 | 來源 |
|---|---|---|---|---|---|---|
| U7-1 | 匯入任務 Excel 的「上傳驗證」零守門,而且整份檔案展開進記憶體(壓縮炸彈) | 一份 10MB 以內的檔就能吃掉 1.5GB 記憶體、卡住處理程序 20 秒以上;連送幾份,整個產品下線 | 同一家公司任何有「專案」模組的登入帳號(修正分支上要是任一專案成員) | app/flow_control/service/job_import_service.py:203(開檔前量「解壓後大小」,或改用 read_only=True 逐列讀);入口守門見第 158 項 |
🟠 中:與第 130 項同級,門檻低、後果是全站停擺 | runner 實測(未經三人面板) |
| U7-2 | 匯出任務 Excel 時,任務名、部門名、設備名原樣寫進儲存格 | 有人把任務名改成 =HYPERLINK(...),別人匯出後打開,點一下就把同表資料送到外部網址 |
能改任務名(專案管理者)或部門、設備名稱的人;受害者用試算表軟體打開 | app/flow_control/service/job_import_service.py:158-167(=、+、-、@ 開頭的值前面加單引號,或強制存成文字) |
🟢 低:要受害者打開並點擊;CM-2189 修了範本那三支,這支沒修 | 工具 F7,面板 3:0 |
| U7-3 | 批次完成的彙總通知信把「操作者暱稱」原樣塞進 HTML 信件 | 把暱稱改成一段釣魚連結,批次完成一張任務,專案所有管理者和審閱者都會收到一封「系統寄的」、帶著那個連結的信 | 任一能批次完成至少一張任務的專案參與者(暱稱本人可以隨意改) | app/flow_control/service/job_batch_complete_service.py:168-174(塞進 HTML 前先跳脫);同一型還有 workflow_execution_service.py:1219、project_service.py:776 兩處,要一起修 |
🟢 低:只能插內容、不能執行程式;但信是系統名義寄出,釣魚可信度高 | 工具 F8,面板 3:0 |
| U7-4 | 批次完成用「網址專案」的角色決定要不要逐筆核對指派,但不核對任務屬於網址專案 | A 的管理員兼 B 的 viewer,可以批次完成 B 裡不是指派給他的任務 | 同時是 A 的管理員和 B 的參與者(任何角色) | app/flow_control/service/job_batch_complete_service.py:58-62(逐筆用任務自己的專案查角色,或先核對任務屬於網址專案) |
🟢 低:目前分支上等同第 59 項(viewer 本來就能單筆完成);修正分支的內層守門會擋 → 建議不另計、併第 59 項 | runner 實測(未經三人面板) |
| — | 修改、刪除、查看單筆任務只認網址專案(工具 F1/F2/F5) | 見第 45 項 | — | — | 中 | 既有第 45 項重現,不另計 |
| — | 批次匯入「確認」可改別專案任務(工具 F3) | 見第 156 項 | — | — | 中 | 既有第 156 項重現,不另計 |
| — | 匯出任務 Excel 的專案與計畫可以不一致(工具 F4) | 見第 157 項 | — | — | 中(總表記低) | 既有第 157 項重現,不另計;這次面板 3:0 評中,總表當時 2:1 評低 |
| — | 規劃頁任務清單不問是不是專案成員(工具 F6) | 見第 155 項 | — | — | 中 | 既有第 155 項重現,不另計 |
現況(2026-10-01):本棒各條後來的處理結果如下(過程紀錄保留,不改)。
- U7-1(匯入 Excel 壓縮炸彈)=總表第 198 項,✅ 已修(CM-2203,commit
eb4ff79cd/040c3c827,1.21.0 出貨)。- U7-2(匯出 Excel 公式)=總表第 199 項,✅ 已修(CM-2213,commit
a5804a990/bde8f9d16,1.21.0 出貨)。- U7-3(暱稱塞進通知信)=總表第 200 項,✅ 已修(CM-2213,同上 commit,1.21.0 出貨)。
- U7-4(批次完成用網址專案角色):併總表第 59 項(viewer 不可完成任務,該項 ✅ 已修);U7-4 那條「逐筆用任務自己專案查角色」是否一併落地,查不到單獨記錄,保留待核。
job_batch_complete_service.py:50-56「預設把呼叫者當管理員」——預設走不到;真正的缺口在第 58~62 行先回答卡片的問題:不成立。 程式一開始設 is_manager = True,只有三個條件同時滿足才會去查真正的角色:守門零件有接、專案服務有接、網址有帶專案編號。我逐一核對:
di_containers/flow_control/flow_control_containers.py:579-585 都有接。/project/<project_uid>/jobs/batch-complete(套件 jedi-task-platform 的 routing.py:68),專案編號是網址的一段,不可能是空的。job_batch_complete_route.py:31-38,永遠把 project_uid 傳進來。所以「預設是管理員」那條路在正式環境走不到。它的問題在寫法:「零件沒接就放行」。將來有人改接線或加第二個呼叫點,就會默默變成全部放行。建議改成沒接就擋(修正分支上 job_import_service.py 已經這樣做)。
真正的缺口(U7-4,runner 實測): 第 58 行用「網址上的專案」查你的角色。只要你在那個專案是管理員,第 62 行就把「逐筆核對指派」整段跳過。但勾選的任務編號是呼叫者自己填的,沒有任何一行核對它屬不屬於網址上的專案。
70bba021… 指派給 user 19,不是 17。NO_PERMISSION。complete_job(workflow_execution_service.py:660)會再查一次「你是不是任務所屬專案的參與者」,任何角色都過。這就是第 59 項:viewer 本來就能從單筆完成的網址完成別人的任務。所以在目前分支,這招能做到的事,單筆入口早就做得到。fix/security-b1 的 CM-2147 把 complete_job 內層改成「只有被指派人或管理員能完成」(app/flow_engine/service/job_operator_guard.py,拿任務自己的專案判斷)。合回來以後,這招會被內層擋下,錯誤會變成那一筆的失敗原因。批次那層的過濾仍然是假的,只是內層兜住了。job_import_service.py「匯入三步只有最後一步有守門」——成立(第 158 項),而且多一個炸彈入口任務編號誰給的:「上傳驗證」從 Excel 的 A 欄讀,「重新驗證」和「確認」從前端送來的 JSON 讀,全部是呼叫者給的。
| 步驟 | 目前分支的守門 | 前兩步能不能讀到別專案的資料 |
|---|---|---|
匯出 :104 |
查網址專案成員,但計畫編號不核對(第 157 項) | 能,整張任務表 |
上傳驗證 :198 |
零守門(第 158 項) | 能拿錯誤訊息試探:「任務 X 不屬於此稽核計畫」可以判斷 X 在不在計畫 Y 裡;「使用者不存在:abc」可以列舉帳號、部門、設備名稱 |
重新驗證 :315 |
零守門(第 158 項) | 同上,而且不用上傳檔案,改 JSON 就能大量試 |
確認 :398 |
查網址專案管理員,但白名單只查「任務存在」(第 156 項) | 不是讀,是直接改別專案的任務 |
前兩步不會直接吐出別專案的任務內容,但能確認某個編號存不存在、屬不屬於某計畫,這正是第 156 項攻擊需要的材料。
新增(U7-1):「上傳驗證」在零守門的狀態下,第 203 行 load_workbook(file, data_only=True) 把整份檔案展開成記憶體物件。唯一的大小檢查(:194,10MB)量的是壓縮後的大小。實測如下:
| 項目 | 數值 |
|---|---|
| 特製檔(表頭正確,過得了格式檢查,底下 33 萬列同值) | 壓縮後 9.8MB、解開 95MB |
| 照第 203 行同一寫法開檔 | 22.4 秒、記憶體高峰 1,455MB |
一個請求就佔住一個處理程序超過 20 秒、吃掉將近 1.5GB。預設四個處理程序同時被打,就是將近 6GB。落地版單一容器、沒設記憶體上限(第 130 項已查)。修正分支已把守門搬到開檔前(要是專案成員才能上傳),門檻變高,但開檔方式沒變,炸彈本身還在。
my_grc_jobs_query.py、flow_control_dashboard_repo_impl.py:WHERE 有沒有限定「是我的」——不成立my_grc_jobs_query.py:113-115 第一個條件永遠是 user_id == user_id,而這個 user_id 在 job_route.py:246 取自 get_user_context().id(登入憑證),請求內容改不到。project_uid、round_uid 等篩選只會在自己的任務裡再縮小。另一支 get_user_task_queue_by_sp 也一樣(flow_engine_route.py:184)。public.vw_user_job_queue 開了 security_invoker=true(DEV 唯讀查詢確認),代表查詢時會套用底下各張表的客戶隔離規則,不會用建立者的權限繞過。ilike 加參數綁定,沒有字串拼接 SQL;排序欄位走白名單(:174-182),不在名單裡的直接忽略;每頁筆數有上限(jedi_common 的 PagerSchema,MAX_PAGE_SIZE)。:56-67、:110-121),user_id 同樣來自登入憑證。套件的儀表板路由把 is_admin 寫死成 False(dashboard_route.py:36),連管理員都只看自己參與的專案。原生 SQL(:188-214、:247-253)全部用參數綁定,而且只在「可見專案」清單裡查。job_route.py:有沒有哪支方法沒走到 _require_manager——有兩支| 方法 | 守門 | 結論 |
|---|---|---|
清單 POST .../jobs/list(:52) |
無 | 漏(第 155 項,工具 F6) |
查看 GET .../job/<uid>(:82) |
無,project_uid 收了但沒用 |
漏(第 45 項,工具 F5) |
修改 PUT(:104) |
網址專案管理員 | 有守,但不核對任務屬於該專案(第 45 項,工具 F1) |
刪除 DELETE(:147) |
網址專案管理員+規劃期 | 同上(第 45 項,工具 F2) |
新增 POST .../jobs(:183) |
網址專案管理員 | 不成立:新增的 SQL 本身就用網址專案去找評估項目(flow_control_job_repo_impl.py:677-697,WHERE p.uid = :puid),找不到就不建,沒辦法把任務建進別的專案 |
我的任務(:231) |
只看自己 | 不成立(見上一條) |
修正分支(CM-2039、CM-2191)已補上查看、修改、刪除、清單四支,目前分支上都還開著。
| 型態 | 本棒有沒有 |
|---|---|
| 只驗你是誰、沒驗資料是不是你的 | 有,本批主病:六個入口都只認網址專案 |
| 列表有查、單筆沒查(或反過來) | 有:修改、刪除有查,查看、清單沒查 |
守門條件用 or 串、其中一個永遠成立 |
無 |
| 「查不到」和「沒權限」混成同一個回應 | 批次完成:不存在是一句話、沒權限是 NO_PERMISSION,可以分辨任務存不存在;但任務編號本來就能從清單或匯出拿到,不另立 |
| 背景或系統身分假設「呼叫者一定是自己人」 | 本棒範圍沒有背景工作;通知信用執行緒寄,但不查資料庫 |
| 防重放或計次只在單一程序有效 | 無 |
| 註解寫「刻意不檢查」、上一層也沒檢查 | 有:job_import_service.py:418 註解寫「防止未經驗證的 confirm」,實際白名單不限計畫(第 156 項);批次完成的註解說「context 不齊就跳過檢查」(U7-4) |
| 「零件沒接就放行」 | 有:批次完成 :52、匯出 :108、_require_manager(job_service.py:133)三處都是「沒接就不守」;修正分支只把匯入匯出改成擋 |
工具交回 9 條候選,三人面板 27 票全投,8 條 3:0 通過、1 條 0:3 被否決。
| 工具編號 | 說什麼 | 面板 | 對到總表 |
|---|---|---|---|
| F1 | 修改任務只核網址專案的管理員身分,不核任務歸屬 | 3:0 中 | 第 45 項 |
| F2 | 刪除任務同上,連「輪次凍結」檢查也查錯專案 | 3:0 中 | 第 45 項(追記①已記) |
| F3 | 批次匯入確認能改全公司任何任務 | 3:0 中 | 第 156 項 |
| F4 | 匯出任務 Excel 核一個專案、匯出另一個計畫 | 3:0 中 | 第 157 項(總表評低,這次面板評中) |
| F5 | 查看單筆任務零守門 | 3:0 中(一票評低) | 第 45 項 |
| F6 | 任務清單零守門 | 3:0 中 | 第 155 項 |
| F7 | 匯出任務 Excel 沒過濾公式 | 3:0 低 | 新=U7-2 |
| F8 | 批次完成通知信沒跳脫 HTML | 3:0 低 | 新=U7-3 |
| F9 | 批次完成把原始例外訊息回給前端 | 0:3 否決 | — |
F9 為什麼被否決、我怎麼看:面板三票一致認為,寫資料庫的錯誤不會走到這一行,所以吐不出 SQL 語句。我的實測也證實:except Exception 會把 str(e) 原樣放進回應(我在測試裡丟一個假的資料庫錯誤,回應就帶著 [SQL: ...])。機制存在,但面板認為正常路徑上觸發不到,我同意不立項。修第 59 項時可以順手把 :117 改成固定錯誤碼。
F7 補充:CM-2189(895db0ef8,修正分支)修了範本下載那三支,沒修這支。我在修正分支查過 job_import_service.py,沒有任何公式中和。
F8 補充:暱稱本人可以隨意改(見第 150 項)。同一型寫法在範圍外還有兩處:workflow_execution_service.py:1219(每日待辦通知,塞收件人自己的暱稱,傷害較小)和 project_service.py:776(專案通知)。修的時候三處一起做。
verified。卡片點名的四條是我逐支開檔回答的;U7-1 和 U7-4 是我手做實測,未經三人面板投票。job_service.py、job_batch_complete_query.py、job_import_lookup_query.py、workflow_execution_service.py 的完成任務段、common/authz/workflow.py、project.py,以及套件 jedi-task-platform 的四支路由。打折處:
load_workbook 寫法開檔,沒有經過 gunicorn,也沒量多個處理程序並發的情形。| 項目 | 數值 |
|---|---|
| run ID | wf_14dcf3f9-ca2 |
| 報告目錄 | CLAUDE-SECURITY-20260925-053454/(不入版控) |
| 驗證章 | verified(stamp CLAUDE-SECURITY-REVISION-159a2fff977c-dirty.json;dirty 是平行線未 commit 的改動,不在範圍內) |
| 掃描參數 | --effort low、focus attack-surface、範圍 7 檔 |
| 研究員 | 2 派 2 回(主掃 1+密鑰掃 1,密鑰零發現),零失敗、零重試 |
| 候選/投票 | 9 候選、27 票全投;8 條 3:0、1 條 0:3 |
| agent 總數 | 29 個,0 錯誤 |
| 耗時 | 約 60 分鐘 |
| 淨新增 | 中 1(U7-1 炸彈,runner)+低 2(U7-2、U7-3,面板);U7-4 建議併第 59 項不另計 |