jedi-task-platform/jedi_task_platform/ 底下 35 支檔——任務指派的網址入口/業務邏輯/資料存取、六張成員相關資料表的查詢層與資料表模型、兩支建表腳本ca60cfd8efc7bf22e92a0c0fb4632e9be856e76f(branch feature/FR-075,工作區乾淨)claude-security plugin v0.11.0,effort low、focus attack-surfacewf_ae470f56-685verified — 三個檢查員對 2 條發現各投一票、6 票全數投出、2 條全 3:0 通過,無退件掃完了,範圍內找到 2 個真問題,最嚴重的是「任何能登入的帳號送一個空請求,就能撈出全庫一萬兩千多筆任務指派紀錄」——裡面有每個任務指派給誰、那個人的登入帳號與暱稱、誰是審核者。這和第 1 棒(P1)找到的成員名冊外洩是同一個病在不同表上重演:查詢條件全部非必填,程式把「沒有條件」當成「不用過濾」,直接回整張表。
第二個問題是寫入端:新增任務指派時,系統拿「你自己填的專案編號」去檢查你是不是那個專案的管理者,卻不檢查你填的任務到底屬不屬於那個專案。結果是:只要你在公司內任何一個專案當管理者(自己開一個專案就自動是管理者),就能把別人專案的任務登記成你自己專案底下的指派——然後那個任務會出現在你的待辦清單裡,而且主系統判斷「你能不能完成/退回這個任務」時正是靠這張表反查專案,可能因此被騙過。
另外我人工核對出 4 件工具沒報的事(第 5 節),其中最要緊的是:六張表在 DEV 資料庫的隔離全是關的(唯讀實查 2026-09-14),而出貨給新客戶的基線檔裡也沒有這六張表的隔離設定——套件雖然已經寫好隔離腳本(003-participant-rls.sql),但那支不在本棒範圍、也還沒套進任何環境。也就是說上面兩個洞目前沒有任何第二道防線。
產品裡每個稽核任務(例如「檢查某條控制項的證據」)會指派給某些人去做,有人是執行者、有人是審核者。「哪個任務指派給誰」這件事,就存在一張叫 task_assignees 的資料表裡,DEV 上有一萬兩千多筆。
這一棒檢查三層是不是都空的:
把三層放同一棒,就是為了一眼看完是不是三層全空。答案是:接近全空。
工具報 4 條候選,去重後是 2 個問題,兩條都 3:0 通過面板。
| # | 這是什麼問題 | 出事會怎樣 | 要先有什麼才打得到 | 在哪裡 | 怎麼修 |
|---|---|---|---|---|---|
| P2-1 🟡 中 (工具 F1) |
「查任務指派清單」這支 API 完全沒有檢查你是不是這個專案的人,查詢條件八個欄位全部非必填——送一個空請求 {},程式把「沒條件」當成「不用過濾」,變成無條件全表查詢 |
一次撈光全庫一萬兩千多筆任務指派(DEV 實測 12,494 筆),每筆含:專案編號、控制項編號、任務編號與代碼、被指派人的登入帳號與暱稱、是不是審核者、建立者與修改者的登入帳號。等於全站員工帳號清冊+每個專案的稽核分工全貌。也可以只帶一個 user_uid 反查「某個特定的人被指派了哪些任務」,用來鎖定目標 |
只要一個能登入的帳號(任何角色,甚至不必是任何專案的成員)。不需要知道任何編號 | 入口 participant/api/routes/task_assignee_route.py:44-52缺口本體 participant/app/service/task_assignee_service.py:70條件全選填 participant/api/serializers/task_assignee.py:4-12 |
照同一支檔裡 delete_task_assignee(:239-250)已經寫對的做法補三件事:① project_id 改必填(serializer 加 required=True),沒給就回 400;② 查詢前呼叫「你是不是這個專案的成員」檢查(主系統已有現成的 common.authz.project.assert_project_participant,套件側比照 common/guard.py 開一支 port 由主系統注入);③ 拒絕完全不帶條件的查詢,不要讓它退化成全表撈 |
| P2-2 🟡 中 (工具 F2,工具原評高,面板三票一致降為中) |
新增任務指派時,拿「你自己在請求裡填的專案編號」去判斷你是不是管理者,卻從不檢查你填的那個任務屬不屬於這個專案——兩個值各自獨立、中間沒有任何比對就寫進資料庫 | 你自己開一個專案(開專案的人自動就是管理者),就能把別人專案的任務登記成你專案底下的指派:①被指派的人(可以填自己)立刻在待辦清單看到受害專案的任務內容;②更嚴重的是,主系統要判斷「你能不能完成/退回某個任務」時,是靠這張表反查該任務屬於哪個專案(app/flow_engine/service/workflow_execution_service.py:150-153),塞進一筆假的就可能讓反查指向你自己的專案,於是守門放行,你就能動別人專案的任務狀態 |
需要在公司內任一專案是管理者(自己新開一個專案即可,app/project/service/project_start_app_service.py:164-179 會自動把建立者補成 manager);受害任務要跟你同一個客戶(跨客戶會在前一步被 job_executions 的隔離擋掉);要升級到「能改別人任務狀態」還需要那個任務原本沒有指派人(反查沒有排序,沒有既有紀錄時必中) |
守門 participant/app/service/task_assignee_service.py:144任務解析 :148寫入 :164 |
寫入前先把「這個任務真正屬於哪個專案」查出來(走既有的 ITaskExistenceQuery port,或經 job_executions → main_workflow_execution 對應),對不上就回 403/404;control_id 同樣要驗證屬於該專案;守門要用「從資源反查出來的專案編號」而不是請求裡填的那個——同檔 delete_task_assignee 已經是這個寫法,照抄即可 |
| # | 內容 | 嚴重度 |
|---|---|---|
| P2-3 | 六張表在 DEV 的資料庫隔離全是關的,而且出貨給新客戶的基線檔裡也沒有這六張表的隔離設定——套件已寫好隔離腳本 003-participant-rls.sql(2026-09-14 新增,不在本棒範圍),但尚未套進任何環境、也尚未併進出貨基線。上面兩個洞目前沒有任何第二道防線 |
🟡 中(放大 P2-1/P2-2 的影響範圍) |
| P2-4 | 六支資料存取層沒有任何一支自己加專案範圍條件——全部沿用共用底層的「欄位有值才加條件」規則,也就是**「上層不帶條件=回全表」是這六張表的共同行為**,不是 task_assignees 獨有 |
🟡 中(是 P2-1 的根因,也代表 P1 那三支同病同源) |
| P2-5 | task_assignees.project_id 存的不是任務真正所屬的流程編號——DEV 實查 12,485/12,494 筆兩者對不上(那是設計如此,一個存專案、一個存流程),所以資料庫層根本沒辦法用外鍵擋 P2-2,只能靠程式檢查。基線檔裡雖然有一支會擋這件事的觸發程序(trg_task_assignees_validate_job),但DEV 上這張表掛了 0 個觸發程序,那支函式存在卻沒有被掛上去 |
🟡 中(說明 P2-2 為何無兜底) |
| P2-6 | 卡片說「批次新增指派」是死端點、前端無呼叫者——實際上前端有在呼叫(src/views/project/TaskSetupView.vue:292,該頁在路由 project-task-setup 上是活的)。後端那支恆回空陣列,所以前端這個「快速設定」按鈕按下去什麼都不會發生、也不會報錯 |
🟢 低(非資安,是功能靜默失效) |
本棒沒有範圍外發現。 工具的密鑰專項(掃「有沒有把密碼金鑰寫進程式碼」)這次沒有回報任何憑證類問題,4 條候選全在範圍內。
project_participants 一句,task_assignees 與其餘四張從未登記——本棒補齊(見 P2-3)。現況:已修(M10-5,1.21.0 出貨)
① 這是什麼問題 有一支 API 可以查「任務指派清單」。它只檢查「你有沒有登入」,沒有檢查「你是不是這個專案的人」。而且它接受的八個查詢條件(專案、控制項、任務、使用者……)全部都是選填的,所以你可以一個都不填。程式碰到「沒有任何條件」時,不是拒絕,而是當成「不用過濾」,於是把整張表回給你。
② 出事會怎樣 DEV 實測這張表有 12,494 筆。一次請求全部回來,每一筆包含:
user_uid)與暱稱(user_name)is_approver)、角色created_user/updated_user)合起來就是全公司員工的帳號清冊,加上每個專案的稽核分工全貌。登入帳號外洩可以拿去做針對性的密碼嘗試;「誰負責審核哪個專案」可以拿來挑社交工程的對象。也可以只帶一個 user_uid 條件,反查某個特定的人手上有哪些任務——鎖定特定高權限人員時很好用。
③ 要先有什麼才打得到 只需要一個能登入的帳號,任何角色都行,甚至不必是任何專案的成員。不需要事先知道任何編號。 跨客戶的部分還需要「這個部署尚未對這六張表開資料庫隔離」——DEV 唯讀實查(2026-09-14)確認六張表隔離全關,出貨基線裡也沒有(見 P2-3),所以目前條件成立。就算之後開了隔離,同一個客戶內「不是這個專案的人」照樣讀得到。
④ 在哪裡
participant/api/routes/task_assignee_route.py:44-52(POST /api/1.0/task-assignees,只掛了主系統注入的「有沒有登入」檢查)participant/api/serializers/task_assignee.py:4-12participant/app/service/task_assignee_service.py:70 — 整個方法只有兩行,直接把請求內容展開丟進查詢jedi-common 的 base_repository_impl.py:512,if value is not None and hasattr(...) 才加條件,全 None 就變成沒有 WHERE 的 .all()⑤ 怎麼修 同一支檔案裡的 delete_task_assignee(:239-250)已經寫對了:先撈出紀錄、反查它真正屬於哪個專案、再對那個專案做守門。照那個形狀補三件事:
project_id 加 required=True,沒給就回 400common.authz.project.assert_project_participant,套件側比照 participant/common/guard.py 既有的做法開一支 port 由主系統注入(不要在套件裡另寫一套)首腦核對回應:卡片「重點看什麼」第 2 條(POST /task-assignees 只驗登入)成立,與首腦預判的形狀完全一致。
現況:已修(M10-6,FR-114 CM-2071,套件 commit ef9a313b/BE commit fccfe4db7)
① 這是什麼問題 新增任務指派時,請求裡要填四個東西:專案編號、控制項編號、任務代碼、要指派給誰。程式的做法是:
中間沒有任何一行去確認「你填的這個任務,真的是那個專案底下的任務嗎」。兩個值各自獨立,你可以隨意搭配。
② 出事會怎樣 自己新開一個專案(開專案的人會自動被補成管理者),就通過了第 144 行的檢查。接著填上別人專案的任務代碼,就能把那個任務登記成你專案底下的一筆指派。後果有兩層:
app/flow_engine/service/workflow_execution_service.py:150-153)。你塞進去的那一筆假紀錄會讓反查回答「這個任務屬於攻擊者的專案」,於是守門放行——你就能對別人專案的任務按完成或退回。③ 要先有什麼才打得到
job_executions 的隔離擋掉)。④ 在哪裡
participant/app/service/task_assignee_service.py:144:148:164participant/api/serializers/task_assignee.py:15-19task_id 這條外鍵刻意不建(migrations/002-participant-fks.sql:12-15,因為跨模組疆界);隔離腳本的寫入規則也只檢查「這個專案看得到嗎」,不檢查任務歸屬⑤ 怎麼修 寫入前先把「這個任務真正屬於哪個專案」查出來,跟請求裡填的專案編號比對,對不上就回 403 或 404。control_id 同樣要驗證屬於該專案。最關鍵的一點:守門要用「從資源反查出來的專案編號」,而不是請求裡填的那個——同一支檔的 delete_task_assignee 已經是這個寫法(:239-250),照抄形狀即可。
面板為什麼把嚴重度從高降到中:三個檢查員一致認為第二層那條升級鏈有前置條件(要反查剛好抓到插入的那筆),而第一層只是看到任務內容,屬有限影響。首腦同意這個判定。
首腦核對回應:卡片「重點看什麼」第 4 條(migration 自陳「一律先經 project_id 收斂」是否每條路都成立)——不成立,這就是反例:project_id 是呼叫端自填、未經驗證,「收斂」這件事在寫入路徑上根本沒發生。
這是什麼問題:資料庫有一個叫 RLS 的機制(就是「每個客戶只能看到自己的資料」的資料庫層隔離)。這六張表一張都沒開。
查證方式:DEV 資料庫唯讀查詢(2026-09-14):
control_group_participants | f ← f 代表關閉
process_participants | f
project_control_participants | f
project_group_participants | f
project_participants | f
task_assignees | f
再查主專案的出貨基線(新客戶安裝時用的 scripts/init/02-schema.sql):全檔有 61 條開啟隔離的指令,這六張表一條都沒有。
套件已經寫好腳本了,但還沒套:套件裡有 migrations/003-participant-rls.sql(2026-09-14 新增,FR-094 第 9 棒 CM-1800 的產物)把六張表的隔離都補上了——但那支檔不在本棒範圍(本棒 scope 只含 001 與 002),而且從 DEV 實況看還沒有套進任何環境,主專案的出貨基線也還沒重產。
影響:P2-1 與 P2-2 目前沒有第二道防線。P2-1 跨客戶撈全表這件事,現在是真的做得到。
建議:這件事應該併進 FR-094 那條線追(套件 003 何時套進 DEV/STG/POC、出貨基線何時重產),不是本棒能修的。注意:就算套了隔離,P2-1 的「同客戶內非本專案的人也讀得到」仍然存在——隔離只擋跨客戶,擋不了同客戶內跨專案。
現況:已裁定逐支補、底層不動(M10-9);有洞的支已補,1.21.0 出貨
卡片要我逐支看的六支加 user_enrichment.py,我全部開檔看過了。結論:
| 檔案 | 有沒有自己寫的查詢方法 | 有沒有加專案範圍條件 |
|---|---|---|
task_assignee_repo_impl.py |
有 5 支(get_user_task_queue / add / update / delete / add_batch / get_existing_keys_multi / delete_by_project_id) |
寫入與刪除類都有帶完整主鍵條件;get_user_task_queue 帶 user_id+task_id 清單;get_existing_keys_multi 帶 task_id+user_id 清單。都不是「回全表」型 |
其餘五支(project_participant / control_group_participant / process_participant / project_control_participant / project_group_participant) |
只有 add / update / delete / delete_by_project_id,全部帶完整主鍵條件 |
同上 |
user_enrichment.py |
蓋掉底層 7 支讀取方法 | 不加任何條件,只做「讀出來之後把 user_id 換成姓名」 |
真正的問題在這裡:六支 repo 沒有任何一支自己實作 get_all / get_one——那兩支走的是共用底層 BaseRepositoryImpl,規則是「這個欄位有值才加條件,沒值就不加」(base_repository_impl.py:512)。所以「上層不帶條件=回全表」是這六張表的共同行為,不是 task_assignees 一張表的問題。
這解釋了為什麼 P1 那三支(成員名冊)和 P2-1(任務指派)會是同一個病:它們共用同一個查詢底層,而那個底層的設計就是「不帶條件不過濾」。修的時候要有心理準備:補在上層(service)是一支一支補;如果想一次擋掉,就要在底層加「拒絕空條件查詢」的規則,但那會影響所有用到這個底層的模組,屬跨模組決策。
跨租戶 join 的疑慮不成立:user_enrichment.py 把使用者資料補進來時,走的是主系統注入的名冊 port(get_users_by_ids),最終查的是 public.users 表——那張表的隔離是開的(DEV 實查為 t),所以查不到別家客戶的帳號,只會留白。這一點卡片有問,答案是健康的。
project_id 存的不是流程編號,資料庫層沒辦法用外鍵擋 P2-2(工具未報,人工查證)想確認 P2-2 有沒有資料庫層的兜底,查了三件事:
task_assignees 只有一條外鍵 fk_task_assignees_project(指向 projects),沒有指向任務的外鍵——migrations/002-participant-fks.sql:12-15 明文寫「刻意不建,因為跨模組疆界」。project_id 與任務的關係:DEV 實查 12,494 筆裡,有 12,485 筆的 project_id 跟該任務的 main_workflow_execution_id 對不上。這不是資料壞掉——project_participant.py:20-23 的註解明說「project_id 存的是專案編號,不是流程編號,兩者值域不重疊」。意思是:資料庫沒有任何一欄可以用來檢查「這個任務屬不屬於這個專案」,只能靠程式查。trg_task_assignees_validate_job,內容正是「檢查 task_id/project_id/process_id 三個編號對不對得到同一筆任務,對不上就報錯」——如果掛上去,P2-2 就會被資料庫擋住。但 DEV 實查:這張表掛了 0 個觸發程序,函式存在、沒有被掛上。結論:P2-2 目前三層都沒擋(程式沒查、外鍵沒有、觸發程序沒掛)。修 P2-2 只能從程式層下手;如果想要資料庫兜底,把那支既有的觸發程序掛上去是現成的選項,但要先確認它的 process_id 欄位語意與現況一致(那支函式讀 NEW.process_id,而 task_assignees 表上沒有這個欄位,所以直接掛會壞——這也可能就是它從沒被掛上的原因)。
現況:🗑️ 整頁已拆除(M10-14,1.21.0 出貨)
卡片說:batch_add_task_assignees 是 stub 恆回空陣列,前端常數還在但 grep 無呼叫者,「報告非資安段記一筆可刪即可」。
核對結果:前端有呼叫者,卡片這句要更正。
src/views/project/TaskSetupView.vue:292 有實際呼叫 API.TASK_ASSIGNEES_BATCH,在「快速設定」功能裡。project-task-setup 與 project-task-setup-ap 兩條路由都指向它)。ProjectPlanningView.vue:2172 有註解說明這個端點「已 dark、改走 per-job 寫入路徑」,但 TaskSetupView.vue 沒有跟著改。實際後果:使用者在「專案任務設置」頁按下快速設定,前端送出請求,後端 task_assignee_service.py:186-190 直接回空陣列,前端收到成功回應、跳出「設定成功」的提示——但什麼都沒有寫入。這是功能靜默失效,不是資安問題,但比「死端點」嚴重:使用者會以為設定好了。
建議:不要直接刪端點。先確認 TaskSetupView 的快速設定是不是還要留;要留就比照 ProjectPlanningView 改走 per-job 寫入路徑,不留才刪端點與前端常數。這件事應另開卡,不屬本棒範圍。
本棒 scope 內唯一的 app service 是 TaskAssigneeService,它有 8 支公開方法。逐支核對「主系統哪裡呼叫、呼叫前有沒有守門」:
| 方法 | 行號 | 誰呼叫 | 呼叫前有沒有守門 | 守門是否條件式 |
|---|---|---|---|---|
get_task_assignees |
:69 | 套件 route task_assignee_route.py:50(POST /task-assignees);另外 AI 儀表板也讀得到(di_containers/dashboard_apis/participant.py:42-53) |
❌ 零守門,只有「有沒有登入」 | — |
get_task_assignee_menu |
:74 | 無人呼叫(套件內外 grep 皆零;只有 docs/api/participant/generate_docx.py 的文件字串提到) |
— | 宿主零取用,未來陷阱 |
get_task_assignees_with_inherits |
:94 | 無人呼叫(同上) | — | 宿主零取用,未來陷阱 |
get_user_task_queue |
:135 | 無人呼叫(「我的任務」已改由主專案 MyJobsAppService 供應,FR-069 4.1/CM-1477) |
— | 宿主零取用,未來陷阱 |
add_task_assignee |
:140 | 套件 route :66(POST /task-assignee) |
✅ 有 assert_project_manager |
⚠️ 是:if enforce_role and self._participant_role_service:。本服務的角色服務有注入(di_containers/flow_engine/task_assignee_containers.py:66),所以目前會檢查。但守門對象是自填的專案編號——見 P2-2 |
batch_add_task_assignees |
:175 | 套件 route :27(POST /task-assignees/batch);前端 TaskSetupView.vue:292 實際在呼叫 |
❌ 零守門 | 恆回空陣列,寫不進東西所以目前無風險,但見 P2-6 的功能失效問題 |
update_task_assignee |
:193 | 套件 route :81(PUT /task-assignee) |
⚠️ 有,但撈不到既有紀錄就整段跳過(:213-214) | ⚠️ 雙重條件式:if enforce_role and role_service 外,再加 if existing is not None and existing.project_id is not None |
delete_task_assignee |
:234 | 套件 route :102(DELETE /task-assignee) |
✅ 本檔唯一寫對的一支:先撈紀錄、不存在就 404、反查真實專案、與請求的專案交叉比對、再守門(:239-250) | ⚠️ enforce_role 仍是條件式,但資源反查是無條件的 |
delete_by_project_id |
:260 | 無人呼叫(主系統的專案刪除沒走這支;DEV 實查有 9 筆指向已不存在任務的孤兒紀錄) | ❌ 零守門 | 宿主零取用,未來陷阱 |
這張表的兩個重點:
get_task_assignee_menu/get_task_assignees_with_inherits/get_user_task_queue/delete_by_project_id)。它們現在沒有 route,所以打不到,但留著就是未來陷阱——哪天有人接上 route 或從別處呼叫,會發現它們一道守門都沒有。登記備查。update_task_assignee 的守門寫在條件後面(卡片「重點看什麼」第 1 條)——見下一節。卡片列了六項,逐條答:
成立,我追下去確認了跳過之後會發生什麼。 task_assignee_service.py:213-214 的條件是 if existing is not None and existing.project_id is not None:,撈不到就整段跳過守門,繼續往下走。
但往下走的第一件事是 validate_assignee_is_not_exist(:216)——那支在查無紀錄時會拋 404(task_assignee_domain_service.py:40-47)。所以目前的實際結果是 404,不是無授權寫入。
這是「靠一個檢查擋另一個缺口」的結構,跟 P1-4 找到的流程參與者是同一個形狀(那邊更糟,它 docstring 宣稱的 validate 根本不存在;這邊至少 validate 是真的)。風險在於:那支 validate 的存在與否跟授權無關,哪天有人覺得「查無就當作新增」把它改成 upsert,這道守門就靜默消失了。建議修法:把守門條件改成「撈不到就拋 404」,不要依賴後面那支。
POST /task-assignees 只驗登入(首腦核對✅屬實)成立 → 就是 P2-1,工具也報了,3:0 通過。
已逐支開檔核對,對照表在第 5 節 P2-4。 結論:六支 repo 沒有任何一支自己實作 get_all/get_one,走的是共用底層的「有值才加條件」規則,所以「不帶條件=回全表」是六張表的共同行為。user_enrichment.py 的跨租戶疑慮不成立(走名冊 port,底層 users 表隔離是開的)。
不成立。 001-participant-tables.sql:16-21 寫「租戶隔離由上游的專案層擋(participants 一律先經 project_id 收斂)」。
get_task_assignees 根本不帶 project_id 就能查(P2-1)——「一律先經 project_id 收斂」不成立。project_id 是呼叫端自填、不驗證(P2-2)——「收斂」發生了,但收斂到的是攻擊者指定的值。補充一點:這段註解本身其實已經被更新過了。我讀到的版本(第 16-21 行)結尾寫著「不要指望應用層的 project_id 收斂(實測列表查詢不帶 project_id 就是全表撈)」——也就是說寫這支 003 隔離腳本的人(FR-094 第 9 棒)已經發現這件事並改寫了註解。卡片引用的是舊版說法。
002 的外鍵行為:六條 project_id 外鍵都是 ON DELETE CASCADE,刪專案時指派紀錄會跟著刪,不會留孤兒——這一點是健康的。task_id 那條外鍵刻意不建(跨模組疆界),所以指向已刪任務的孤兒擋不住:DEV 實查有 9 筆(12,494 筆中)指向已不存在的任務。數量很小,屬殘留不屬設計缺陷。
batch_add_task_assignees(首腦核對說 FE 無呼叫者)這條要更正:前端有呼叫者,而且頁面是活的。 詳見第 5 節 P2-6。實際後果是功能靜默失效(按了沒反應也不報錯),比死端點嚴重,建議另開卡。
participant/domain/ports.py 的介面契約有沒有 Optional 讓宿主少給也能過查過了,結論分兩半:
IProjectRoleGuard(:81-99)沒有預設實作,缺了就在 common/guard.py:44-49 直接 raise,不放行。IUserDirectory 的兩支核心方法(get_user/get_users_by_ids)也沒有預設值,缺了拒絕掛載。這一半是健康的。IUserDirectory.get_org_units_by_ids(:58-69)與 get_users_by_login_names(:71-78)預設回空字典;IAuditLogger/IJobNotifier/IJobEnrichment 缺了就降級(不記事件、不通知、附件數回 0)。這三者失效的後果是畫面留白或稽核紀錄漏記,不會讓任何人多拿到權限。但要指出一件事:IAuditLogger 缺了會讓稽核事件靜默不記錄(只留一行 WARNING)。這在資安上不是「洞」,但是事後追查的盲點——出事時查不到誰指派了什麼。主系統目前有接(core/plugins/participant.py:build_config 有給 audit_event_codes),所以現況正常。登記備查。
兩條發現都經過三個獨立檢查員各自從零讀檔驗證,6 票全數投出、2 條全 3:0 通過、驗證章 verified、無退件。我另外逐條開檔核對過行號與程式碼內容,與報告一致。P2-1 的「空請求=全表」有共用底層的程式碼為證(base_repository_impl.py:512),P2-2 的三行(守門/解析/寫入)我逐行讀過,中間確實沒有比對。
資料庫相關的四項人工核對(隔離狀態、外鍵、觸發程序、孤兒筆數)都是對 DEV 資料庫的唯讀實查,不是推測。
沒有執行過任何攻擊:本棒全部發現都來自讀程式碼與查資料庫,沒有真的送過請求驗證。上面寫的「打得到」是從程式碼推出來的,不是實測過的。
low:一個研究員讀完整個範圍,沒有做威脅建模、沒有做廣度掃。low 的定位是「快速分流但仍經驗證」,不是「窮舉」。research_coverage 是 null),所以沒有逐檔記錄哪些檔被讀到結論、哪些沒讀到。35 支檔裡工具只在 3 支檔上報出問題,其餘 32 支是「讀過沒事」還是「沒讀到」,工具沒說,我無法從它的輸出判斷。user_enrichment.py、三支 migration、四支 model/mapper/entity、ports.py、route、serializer——這些我實際開檔看過。沒有逐檔精讀的是:六支 mapper 裡的四支、三支 __init__.py、四支 model 中的兩支(都是純欄位宣告,風險低)。所以:報出來的兩條可以信;「這 35 支檔沒有別的問題」這句話不能信。
| 項目 | 值 |
|---|---|
| run ID | wf_ae470f56-685 |
驗證章 verification.status |
✅ verified |
候選數 candidates |
4 |
去重後 candidates_deduped |
2 |
面板票數 panel_votes |
6(=2 條 × 3 個檢查員,全數投出) |
達標發現 panel_quorum_findings |
2(兩條都 3:0) |
未審候選 unreviewed_candidate_sites |
0 |
研究員 researchers_dispatched/returned |
2 / 2 |
| 嚴重度被下修 | F2:HIGH → MEDIUM(三個確認票一致給 MEDIUM) |
| 掃描耗時 | 8,107 秒(約 2 小時 15 分) |
| 掃描版本 | ca60cfd8efc7bf22e92a0c0fb4632e9be856e76f(feature/FR-075,乾淨) |
| effort/focus | low/attack-surface |
| 範圍檔數 | 35(與卡片核對一致) |
| 工具原始產物 | jedi-task-platform/jedi_task_platform/CLAUDE-SECURITY-20260914-130805/(套件 repo,該目錄自帶 .gitignore 不入版控) |
003-participant-rls.sql 已經寫好了,缺的是「套進環境」與「重產出貨基線」兩個動作。get_task_assignee_menu/get_task_assignees_with_inherits/get_user_task_queue/delete_by_project_id,四支都零守門,現在打不到但接上就開。