U7 檢查結果:任務入口與批次(任務增刪改查、Excel 批次匯入匯出、批次完成、我的任務)

U7 檢查結果:任務入口與批次

檢查日期 2026-09-25|對應卡片 CM-2158(母卡 CM-2152)|檢查範圍 7 個檔案 1,908 行|工具 run ID wf_14dcf3f9-ca2|掃描版本 159a2fff(feature/review)

§1

🔴 一句話結論

決策者點名的「批次完成把呼叫者當管理員」,寫在程式裡的那條預設走不到;真正的洞在隔壁:批次完成拿「網址上的專案」判斷你是不是管理員,卻不核對勾選的任務屬不屬於那個專案。 這跟本批其他五個入口是同一個病:只認網址專案,不認任務歸屬。

  • 批次完成(卡片最重要的一條):is_manager = True 這個預設值,只在「守門零件沒接上」或「網址沒帶專案編號」時才會生效。實際上零件有接,網址也一定帶專案編號,所以這條預設在正式路徑上走不到(第 59 項當年寫的「待核對」,核對結果是不成立)。但我手做實測(未經三人面板)發現另一個洞:你是專案 A 的管理員、又是專案 B 的旁觀者(viewer)時,網址填 A、勾選 B 的任務,系統就用「你是 A 的管理員」直接跳過「這張任務是不是指派給你」的逐筆檢查。DEV 上真的有這種身分組合的帳號。目前分支上這招換不到新能力,因為第 59 項的洞本來就讓 viewer 單筆完成任務;等修正分支(CM-2147)合回來,內層守門會擋住它。所以我建議不另計、併進第 59 項那張修正卡的驗收項。
  • 批次匯入(卡片第二條,「任務編號誰給的」):任務編號是呼叫者自己填在 Excel 或 JSON 裡的。「確認」那一步只查「這個任務存不存在」,不查「是不是這個計畫的」(第 156 項,工具 F3 這次再度 3:0 命中)。前兩步「上傳驗證」與「重新驗證」完全沒有身分檢查(第 158 項)。
  • 🟠 新發現(中風險,runner 實測、未經面板):「上傳驗證」那一步零守門,而且整份 Excel 直接展開成記憶體裡的物件。實測 9.8MB 的特製 Excel 檔,開檔花了 22 秒、記憶體衝到 1.46GB。同一家公司任何有專案模組的登入帳號都能打,連送幾份就能把服務撐爆。這是第 130/176 項「壓縮炸彈」的第五個入口,之前沒被記錄過。
  • 工具報 8 條全部 3:0 通過:6 條中風險都是既有項目重現(第 45/155/156/157 項),淨新增 2 條低(匯出任務 Excel 沒過濾公式、批次完成通知信沒跳脫 HTML)。
  • 「我的任務」與儀表板:查詢條件永遠是「登入者本人」,是從登入憑證取的,不是從請求內容取的。資料庫檢視表也開了隔離保護。不成立。

帶著前棒的形狀來看:U5/U6 的病是「背景工作用系統身分跑、沒核對專案歸屬」。U7 沒有背景工作,但同一個問題換了個入口出現:每個入口都只問「你是不是網址那個專案的人」,沒人問「你要動的這張任務是不是那個專案的」。

§2

這一棒在檢查什麼

「任務」是稽核流程裡最小的工作單位(例如「上傳防火牆設定截圖」),掛在某個專案的某個控制項底下。這 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 「我的任務」服務層

要回答的核心問題:任務編號是誰給的?系統有沒有核對這個編號屬於呼叫者有權限的專案?

§3

掃到什麼(總覽)

# 這是什麼問題 出事會怎樣 要先有什麼才打得到 該補檢查的位置 嚴重度+為什麼 來源
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 那條「逐筆用任務自己專案查角色」是否一併落地,查不到單獨記錄,保留待核。
§4

卡片點名的疑點逐條回答

1. 🔴 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 行就把「逐筆核對指派」整段跳過。但勾選的任務編號是呼叫者自己填的,沒有任何一行核對它屬不屬於網址上的專案。

  • DEV 唯讀查詢(2026-09-25 14:00 前後):租戶 102 的 user 17 在專案 323 是管理員、在專案 642 是 viewer。642 有一張進行中的任務 70bba021… 指派給 user 19,不是 17。
  • 手做實測:照 DEV 這組真實身分寫單元測試,直接呼叫批次完成服務,網址填 323、勾選 642 的任務:逐筆指派核對完全沒被呼叫,直接進入完成任務,回傳「成功 1 筆」。對照組把網址老實填 642:回 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,拿任務自己的專案判斷)。合回來以後,這招會被內層擋下,錯誤會變成那一筆的失敗原因。批次那層的過濾仍然是假的,只是內層兜住了。
  • 建議:不另計,併進第 59 項那張修正卡,驗收項加一條「A 管理員+B viewer,網址填 A、勾 B 的任務,要被擋」。修法是逐筆用任務自己的專案查角色,不要用網址的。

2. 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 項已查)。修正分支已把守門搬到開檔前(要是專案成員才能上傳),門檻變高,但開檔方式沒變,炸彈本身還在。

3. 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)全部用參數綁定,而且只在「可見專案」清單裡查。

4. 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)已補上查看、修改、刪除、清單四支,目前分支上都還開著。

5. 「有人守了一半」共通疑點逐條對照

型態 本棒有沒有
只驗你是誰、沒驗資料是不是你的 有,本批主病:六個入口都只認網址專案
列表有查、單筆沒查(或反過來) 有:修改、刪除有查,查看、清單沒查
守門條件用 or 串、其中一個永遠成立 無
「查不到」和「沒權限」混成同一個回應 批次完成:不存在是一句話、沒權限是 NO_PERMISSION,可以分辨任務存不存在;但任務編號本來就能從清單或匯出拿到,不另立
背景或系統身分假設「呼叫者一定是自己人」 本棒範圍沒有背景工作;通知信用執行緒寄,但不查資料庫
防重放或計次只在單一程序有效 無
註解寫「刻意不檢查」、上一層也沒檢查 有:job_import_service.py:418 註解寫「防止未經驗證的 confirm」,實際白名單不限計畫(第 156 項);批次完成的註解說「context 不齊就跳過檢查」(U7-4)
「零件沒接就放行」 有:批次完成 :52、匯出 :108、_require_manager(job_service.py:133)三處都是「沒接就不守」;修正分支只把匯入匯出改成擋
§5

工具報的逐條

工具交回 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(專案通知)。修的時候三處一起做。

§6

可信度分兩層

  • 「這幾條存在嗎」:工具那 8 條都經過三人面板完整投票(27 票全投、零漏投),驗證章 verified。卡片點名的四條是我逐支開檔回答的;U7-1 和 U7-4 是我手做實測,未經三人面板投票。
  • 「只有這幾條嗎」:範圍 7 支我逐支通讀完。範圍外我讀了追下去需要的檔: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 的四支路由。打折處:
    • 實測都是單元測試層直接呼叫服務,沒有起後端打真的 HTTP 請求(本機後端沒在跑)。
    • 炸彈實測是在本機用同一行 load_workbook 寫法開檔,沒有經過 gunicorn,也沒量多個處理程序並發的情形。
    • DEV 查詢是唯讀的,時間點 2026-09-25 14:00 前後;平行修正線可能改變資料。
§7

執行概況

項目 數值
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 項不另計