檢查日期 2026-09-15|耗時 10 分 38 秒|對應卡片 CM-1806|掃描版本
8bc5d27
沒有找到問題,這是三棒裡唯一乾淨的一棒。 工具提了一條候選、三個檢查員一致否決;首腦另外照卡片六個重點逐條追了一遍,確認否決站得住,但也查出一件要記下來的事:那條被否決的缺口,是靠「目前沒有那種資料」擋著的,不是靠程式擋著。
「選單字典」是一張存下拉選單選項文字的表——使用者狀態有哪幾種、問卷題型有哪幾種、設備類型有哪幾種。全站所有頁面的下拉選單都從這裡讀。
這一棒檢查這張表從網頁請求到資料庫的完整路徑(33 個檔案),要回答兩個問題:
第二個問題的答案是**「是」,而且這是刻意的、正確的**——這張表本來就是全站共用的一本字典,隔離起來會壞掉所有頁面。所以這一棒看的不是「資料會不會外洩」(表裡沒有客戶資料、沒有個資),而是**「改得動的人對不對」**。
| # | 嚴重度 | 這是什麼問題 | 出事會怎樣 | 要先有什麼才打得到 | 在哪裡 |
|---|---|---|---|---|---|
| — | — | 無 | — | — | — |
工具提了 1 條候選,三個檢查員一致否決(0 票通過 / 3 票否決)。首腦復核後同意否決——理由見下一段。
前台取下拉選項的那支端點(任何登入者都能打),回傳前只過濾了「啟用」、沒有過濾「公開」:
# jedi_system_core/api/routes/system_menu_route.py:39
res = [r for r in res if r.enable == 1]
# ↑ 只看 enable,沒看 public這張表有一個 public 欄位(1 = 公開、0 = 私有)。研究員的主張是:標成「私有」的那些列,會被這支端點回給任何登入者。
因為「私有」的列一列都不存在,而且沒有任何一條路會生出來。 首腦逐項核對過,四個來源全部對上:
| 查了什麼 | 結果 |
|---|---|
| 隨套件出貨的預設資料(74 筆) | 全部是公開(migrations/004-system-menus-seed.sql:15-88,沒有一列結尾不是 , 1)) |
| 欄位預設值 | 預設就是公開——資料表、程式模型、資料物件三處都寫 1 |
| 誰寫得動這個欄位 | 只有新增與修改兩支端點,兩支都要平台管理員(system_menu_route.py:93、:113) |
| 開發環境資料庫實查(唯讀) | 74 筆,標成私有的 0 筆。唯一三筆「停用」的是出貨時就刻意關掉的任務類型碼(exec_tool/file_upload/task),內容就是它們自己的選項文字 |
所以今天打過去,那個沒過濾的欄位選不到任何東西。 否決正確。
同一支程式裡就有反例:管理端的分頁查詢有強制過濾公開欄位(app/service/system_menu_service.py:38 寫死 _filter.public = 1),前台這支反而沒有。
意思是:「私有」這個設計在管理端被當真,在前台端沒有被當真。今天沒事,是因為還沒有人用那支(有好好守門的)修改端點把某一列標成私有。哪天平台管理員標了一列,那一列就會立刻出現在所有登入者的下拉選單裡。
這不是今天打得到的漏洞(要先有平台管理員去建立那種資料),所以不列為發現。修法很小:把第 39 行改成同時檢查兩個欄位即可。
首腦逐條開檔追過,這段是工具沒回答、由首腦自己補的:
| 卡上第幾點 | 結果 |
|---|---|
| ① 四條管理端點守門有沒有漏、順序會不會被跳過 | ✅ 沒有漏。分頁查詢、分類清單、新增、修改/刪除四條,每一條都疊了平台管理員檢查。順序一律是「先要登入 → 再檢查能力 → 最後平台管理員」,平台管理員那道在最內層,跳不過去 |
| ② 前台端點會不會回出不該給的列 | ✅ 今天不會,理由見上一段。但缺口形狀是真的,已記一筆 |
| ③ 讀選單會不會把整包寫進日誌 | ⚠️ 會,但這一半無機密。app/service/system_menu_service.py:22 把整包選單寫進日誌檔,與第 1 棒設定那半是同一個壞習慣的第二處。差別在這張表裡沒有密碼也沒有個資,寫進去的就是下拉選單的文字,影響低。另外查到這支方法還被 AI 儀表板申報成可查詢的 API(di_containers/dashboard_apis/system_menu.py),但它回的內容一樣無機密 |
| ④ 排序欄位有沒有白名單 | ✅ 有。底層會先把模型的真實欄位列出來,只有對得上的才拿去排序,對不上直接忽略(jedi-common 的 base_repository_impl.py:271)。送任意欄位名進不去 |
| ⑤ 撞鍵的錯誤訊息會不會洩漏既有的鍵值 | ✅ 不會。訊息是固定的一句「系統選單 group + key 已存在」,不帶任何實際值。另外確認:修改時可以把 A 分組的列改成 B 分組,但若撞到既有的鍵會被資料庫唯一約束擋下轉成 409,覆蓋不掉別人的列 |
| ⑥ 兩支 SQL 有沒有塞不該公開的東西、重複執行會不會出事 | ✅ 都乾淨。選單那支 74 筆全是下拉選項文字;路由那支塞的是選單管理頁自己的路由,綁的能力點全是平台層。兩支都有防重複機制(一支用「不存在才插入」、一支用「撞到就跳過」),重複執行不會塞兩份,也不會把客戶改過的值蓋回預設 |
這兩件在派工前就查證過,本次掃描結果與它們一致:
分兩層,答案不一樣:
verified,1 條候選、3 票全投、沒有中斷、沒有人調低嚴重度。__init__.py(無內容可讀),但剩下 20 支的逐檔覆蓋率,這份結果給不出保證。六個重點由首腦補追,涵蓋的是卡片點名的路徑,不等於全檔覆蓋。⚠️ 特別提醒:零發現不等於「這一半是乾淨的」。工具的已知盲點是會被程式碼註解說服——這一半的檔案 docstring 寫了大量「這是刻意設計」的說明(例如 route 檔頭就寫明「消費端點刻意不疊守門」),研究員讀到就會接受。唯一那條候選正是撞上這種註解的地方,而它被否決的理由經查屬實——但這代表註解確實在影響判讀。
| 項目 | 數字 |
|---|---|
| 檢查範圍 | 33 個檔案(其中 13 支為空的 __init__.py,實質約 842 行) |
| 掃描的程式版本 | 8bc5d27,工作區乾淨(dirty: false) |
| 掃描 run ID | wf_97ccbfe7-532 |
| 驗證章狀態 | verified(完整通過,沒有中途被拒收) |
| 候選問題 → 去重後 | 1 → 1 |
| 投票數 | 3(1 條 × 3 個檢查員),全部投完 |
| 通過 / 否決 | 0 通過 / 1 否決(一致票) |
| 未審候選 / 面板中斷 / 被降級 | 0 / 0 / 0 |
| 派出/回報的研究員 | 1 / 1 |
| 研究員自陳未讀清單 | 無(工具本輪未回報,故無法逐檔佐證覆蓋率) |
| effort / focus | low / 未設(範圍小,整包讀) |
| 耗時 | 638 秒(10 分 38 秒) |
| 派出的 agent | 4(全數完成,0 失敗) |
為什麼只跑 10 分鐘:投票成本=候選數 × 3 位檢查員。P1 有 4 條候選跑了 2 小時 39 分、H1 有 2 條跑了 13 分、這棒只有 1 條。流程一步沒跳,驗證章完整。
產物位置(套件 repo,已被該目錄自己的 .gitignore 擋住不入版控): jedi-system-core/CLAUDE-SECURITY-20260915-095516/