本卡屬 FR-096(母卡 CM-1803),第 1 棒:系統設定表的完整路徑+插件契約與守門殼(jedi-system-core 套件,24 檔 1,674 行)。只掃不修。
掃「系統設定」這張表從網路端點一路到資料庫的完整路徑。這張表存的是寄信伺服器、LDAP 目錄服務、物件儲存的連線設定——裡面有帳號和密碼。
要回答三個問題:① 讀設定的時候,密碼會不會被送出去或寫進日誌檔? ② 誰改得動這些設定? ③ 一個客戶改得到別的客戶、或改得到平台層的設定嗎?
密碼全在這一半。這支套件是「起手式五支」之一,每個新客戶裝上就有,所以這裡的洞是全產品範圍。而且總表已經登記好幾條「密碼寫進日誌」「儲存帳密沒遮罩就回前端」,源頭都指向這張表——這一棒要把源頭查清楚,不要再在下游一條一條撿。
選單那半(另一張表)內容已查證是無個資的碼表,切給第 3 棒;主專案的接線切給第 2 棒(跨 repo 必須分棒,掃描根目錄不同)。
這些是首腦 2026-09-15 親自開檔看過的,行號與內容屬實。不是要你重新確認這些,是要你從這裡往外追:
jedi_system_core/app/service/system_config_service.py:20 一行 logger.info(f"Get system configs: {configs}"),把 entity 整包塞進 log。設定列的 value 是 JSON,SMTP 那列裡面就是 {"secret": "真密碼"}。要追:這支方法有哪些呼叫者、log 檔誰讀得到、有沒有其他同款的整包輸出(__repr__、例外訊息、dump)。這是總表「密碼寫進日誌」(FR-085 C2 HIGH)的源頭表,要交叉對照。plugin/contract.py:61 的 DEFAULT_SECRET_MASKED_GROUPS 只有 SMTP/THIRD_PARTY_LOGIN/NOTIFY_CONFIG/ISSUE_INTEGRATE_CONFIG,沒有 STORAGE_CONFIG;:65 的 DEFAULT_SECRET_VALUE_KEYS 只有 secret/private_token,沒有 access_key/secret_key(那正是物件儲存帳密的欄位名)。FR-086 B2-A 已從上傳那一側報過,這一棒要從名單這一側確認是不是同一個根因,並看「這兩個欄位是凍結的對外契約」這個設定要怎麼加才不破壞相容。api/guards.py 的 mask_secret_value() 對 dict 走 .get()、對物件走 getattr()——因為宿主包過的服務在某條路徑回 dict、其餘回物件。要追:有沒有第三種形狀(list、巢狀、None)會讓遮罩靜默失效(getattr 對 dict 拿不到 group 就直接不遮、密碼明文出去,這個坑檔案自己寫了,要確認補得完整)。api/routes/system_config_route.py 的 PUT/DELETE 先呼叫 service.resolve_group_for_uid(uid) 決定守哪顆權限、再做事;app/service/system_config_service.py 裡 resolve_group_for_uid 查無回 None 不拋錯,呼叫端對 None 退回最嚴的權限。要追:這條路真的擋得住嗎——有沒有辦法讓它回一個較寬鬆的分組(例如送一個屬於別人的 uid),以及新增端點的分組是從送進來的內容取的(payload.get("group")),送一個假分組會怎樣。plugin/assembly.py:17 的 _assert_api_wiring() 檢查四個守門欄位有沒有填,缺一個就拒絕掛載——設計意圖是「寧可起不來,不要變成八條沒人守的公開端點」。要追:這道檢查有沒有辦法繞過(mount_api=False 的 library 模式、register() 被呼叫兩次、只填了三個欄位第四個填 lambda f: f 這種假守門)。changePwd=false 表示「密碼沒改,沿用舊的」;程式在 system_config_service.py:93-106 與 domain 層的 _preserve_existing_secret() 各處理一次。要追:有沒有辦法讓它把舊密碼吐回來(例如先讀再寫的路徑上密碼有沒有經過前端),以及 SMTP/THIRD_PARTY_LOGIN 兩個分支之外的分組是不是完全沒有這層保護。migrations/001 建表、002 開隔離規則與授權、003 塞預設值。要追:002 的四條隔離規則有沒有漏洞(例如寫入類沒有規則、或規則的條件寫得比讀取寬),以及 003 的預設值有沒有塞進任何非空的密碼(首腦看過是空字串,但要確認 access_key/secret_key/private_token 全部為空、且不是硬編了某個內建帳號)。infra/repository/system_config_repo_impl.py:16 的 get_configs(**kwargs) 用迴圈把任意欄位拼成條件——沒給條件就回全表。這張表有隔離擋著(DEV 實查 rls=true、4 條規則),但要追「哪些呼叫者用了它、有沒有在繞過隔離的連線上呼叫」。此外 :40 的 get_by_group_and_key 自己補了租戶條件與排序(註解說是為了避免超級管理員繞過隔離時非確定性挑列),要看同檔其他方法有沒有補。DEV 實查(首腦 2026-09-15 唯讀):system_configs 有隔離(rls=true、4 條規則),共 23 筆分佈 6 個客戶;分組 ISSUE_INTEGRATE_CONFIG 2/NOTIFY_CONFIG 2/RUNTIME_CONFIG 7/SMTP 2/STORAGE_CONFIG 7/THIRD_PARTY_LOGIN 2/WEB_IDEL_CONFIG 1。其中 6 筆的密碼欄位有值(不是空字串)——不是空表,真的有東西可偷。
同款舊發現當對照:FR-086 B2-A「物件儲存帳密明文回給任何登入者」(HIGH,遮罩名單漏 STORAGE_CONFIG)、FR-085 C2「密碼與 JWT 原文進 log」(HIGH)、FR-081 I1「帶著存取權杖走不驗憑證的連線」。這一棒若撞到同一件事,標「與 CM-xxxx 同根因」不另計新發現。