FR-096 · 需求索引 · 本頁由 build 掃資料夾生成
✅ 三棒全部掃完並驗收(2026-09-15);發現已全數處理——對應總表 M22 四件全數已修,隨 1.21.0 出貨。母卡 CM-1803;子卡 CM-1804~1806 三張一次建完連號(子卡 Notion 狀態當時未回寫,早期修正卡 09-21 作廢,改照內化版問題總表派工)。套件 57 檔+宿主 25 檔,按「密碼在哪、權限在哪」切三棒共 82 檔:P1 設定表全鏈(24,jedi)✅ → H1 主專案接線(25,BE)✅ → P2 選單字典全鏈(33,jedi)✅ 零發現。這支跟前面幾支的風險形狀不同——前面掃的是「誰能看誰的資料」,這支是系統自己的鑰匙(寄信/LDAP/物件儲存的帳密)與全站生效的總開關。P1 查出:讀設定的三個入口只驗登入不驗權限,任何登入帳號一個請求就拿到物件儲存帳密(HIGH);遮罩名單漏掉物件儲存那一組(LOW);H1 查出客戶管理員可改全公司登入規則與 LDAP/寄信設定(HIGH,已修)。
前兩棒(P1 系統設定表、H1 主專案接線)都掃完並驗收通過,一共找到 3 個問題(2 個高、1 個低)。第 3 棒 P2 也已掃完,零發現,三棒收口。
第 2 棒最嚴重的那個,一句話:一個客戶的管理員,可以改掉全公司所有人的登入規則——包含平台管理員自己。他可以關掉全公司的雙因子驗證、把登入失敗鎖定次數改成極大值(等於永遠不會鎖定、可以無限次猜密碼)、把登入憑證有效期從五分鐘延長到一個月。原因是「修改登入安全政策」這個權限被標成客戶層級,每開一個新客戶就自動發給那個客戶的管理員,但這支功能寫入的不是該客戶自己的設定,而是全系統共用的那一列。更嚴重的另一半:完全相同的形狀也套在 LDAP(員工帳號目錄)與寄信伺服器設定上——一個客戶的管理員可以把全公司的登入驗證來源,指到他自己的機器上。登入政策被改壞還看得出來,驗證來源被掉包是直接接管所有人的登入,這一半比上面那半更該先修。
完整結果見 H1 報告,驗收過程見下方「H1 驗收」段。
最嚴重的那個,一句話:讀取系統設定的三個入口只檢查「你有沒有登入」、完全不檢查權限。任何一個最低權限的員工帳號——不必是管理員、不必有任何權限、不必知道任何編號——打一個請求就拿得到物件儲存的帳號密碼。物件儲存是放所有客戶上傳檔案的地方,拿到那組帳密等於所有客戶的檔案可讀可寫。同一支檔案裡,新增/修改/刪除三個入口每一個都有權限檢查,證明讀取這裡是漏掉的,不是刻意設計。
為什麼「每個客戶的資料庫隔離」擋不住:隔離擋的是「看到別的客戶那一列」,可是每個客戶自己那一列裡裝的是同一把鑰匙——安裝時產生一組,之後每建一個新客戶就把它原封不動複製一份過去。首腦 2026-09-15 對開發環境資料庫實際查證(只比對雜湊值、沒有印出明文),確認租戶 1 與租戶 102 手上的密鑰完全相同——「複製給新客戶」從推測變成實證。
第二個問題(低)是:就算權限補好了,回傳設定時的密碼遮罩名單漏掉了物件儲存那一組,所以有正當權限的管理員打開頁面時,瀏覽器裡仍會收到明文密鑰。
完整結果與可信度說明見 P1 報告,驗收過程見下方「P1 驗收」段。
這一支跟前面幾支不一樣。前面掃的是「誰能看到誰的資料」——客戶 A 撈得到客戶 B 的名冊、任務、檔案。這一支存的不是客戶資料,是系統自己的鑰匙:寄信伺服器、LDAP 目錄服務、物件儲存的連線設定,裡面有帳號和密碼。一把寄信密碼外流,攻擊者能用公司名義寄釣魚信;一把物件儲存密碼外流,所有客戶上傳的檔案都在他手上。而且改設定是全站生效的總開關——改掉 LDAP 伺服器位址,等於把全公司的登入驗證導去攻擊者的機器。
首腦開卡時已經開檔核出三件事(未經三人檢查員面板驗證,寫進卡當錨點):
auth_containers.py:180、notification_containers.py:20、user_change_password_containers.py:46,加上 login_containers.py 直接接底層)。這四條路徑是登入、寄信、改密碼——全都會碰到寄信伺服器的密碼。system_config_service.py:20 一行把整包塞進 log,而設定列的內容是 JSON,寄信那列裡面就是「密碼欄位:真密碼」。這是總表已登記的「密碼寫進日誌」那幾條(FR-085 C2 HIGH)的源頭表。plugin/contract.py:61 的名單有四組、沒有儲存設定;:65 的機密欄位名只有兩個、沒有物件儲存帳密用的那兩個欄位名。FR-086 B2-A 已從上傳那一側報過這件事(HIGH),這次要從名單這一側確認是不是同一個根因。資料庫實查(首腦 2026-09-15 對 DEV 唯讀):設定表有隔離(4 條規則),23 筆分佈在 6 個客戶,其中 6 筆的密碼欄位是有值的——不是空表,真的有東西可偷。
| 棒 | 卡 | 範圍 | 檔數 | 狀態 | 首腦驗收 |
|---|---|---|---|---|---|
| P1 | CM-1804 | 設定表全鏈+插件契約與四道守門殼(jedi 套件) | 24 檔 1,674 行 | ✅ **掃完,2 條發現(1 高 1 低) 報告|驗證章 verified |
✅ 已驗(2026-09-15) 逐條開檔核對,無改判** |
| H1 | CM-1805 | 主專案接線:守門版服務+繞隔離讀寫器+四支 DI 組裝(BE repo) | 25 檔 2,672 行 | ✅ 掃完,工具報 2 條(皆高)→ 淨新增 1 條 報告|驗證章 verified |
✅ 已驗(2026-09-15) F1 判為第 72 項重複;F2 信心 中→高(DEV 實查佐證) |
| P2 | CM-1806 | 選單字典全鏈(jedi 套件,含 13 支空檔) | 33 檔 842 行 | ✅ 掃完,0 條發現(1 條候選三票一致否決) 報告|驗證章 verified |
✅ 已驗(2026-09-15),零發現屬實 |
一次只派一棒,每棒獨立驗收完才派下一棒。合理停損點在第 2 棒之後(P1+H1 共 49 檔已涵蓋全部的密碼與權限風險面),但沒跑完三棒不能宣稱「這支套件掃過了」。
結論:兩條發現全部屬實,無改判。可以派第 2 棒。
掃描工具跑完會蓋一個「驗證章」,記錄這一輪自己有沒有跑完整。首腦親自打開那份章檔核對(不是聽掃描的 session 轉述),數字如下:
| 項目 | 結果 | 白話 |
|---|---|---|
| 驗證章狀態 | verified |
完整通過,沒有中途被拒收 |
| 拒收原因 | 無 | — |
| 候選問題 → 去重後 | 4 → 4 | 研究員一共提了 4 條,沒有重複的 |
| 投票數 | 12(4 條 × 3 個檢查員) | 全部投完,沒有漏投 |
| 通過 / 否決 | 2 通過 / 2 否決(都是一致票) | 否決率一半 |
| 未審候選 / 面板中斷 / 被降級 | 0 / 0 / 0 | 沒有漏審、沒有斷線、沒有人偷偷調低嚴重度 |
| 派出 / 回報的研究員 | 1 / 1 | — |
| 耗時 | 9,670 秒(2 小時 39 分) | — |
| 掃描的程式版本 | 327c283,工作區乾淨(dirty: false) |
掃的就是那一版,不是改到一半的狀態 |
| 掃描 run ID | wf_881b24e3-df3 |
追查原始紀錄用的編號 |
否決率一半這件事值得講:檢查員不是橡皮圖章。被否決的兩條,否決理由具體到打開檔案指出「這個資料結構只有六個欄位、多送會直接報錯」「同一個數值在另外兩個地方也是內建預設值」。願意否決,通過的那兩條才有份量。
| 發現 | 嚴重度 | 首腦核對了什麼 | 判定 |
|---|---|---|---|
| F1 讀設定不驗權限 | 🔴 高(維持) | 打開套件 system_config_route.py,確認讀取的兩個方法確實只掛「要登入」、寫入的三個方法確實都多一道權限檢查;檔案自己的說明文字也承認「讀取只要登入即可」。再打開主專案 api/system_config/routes/system_config_route.py:131,確認那支 GET 也只有登入檢查——三個入口都要補,補一個沒有用 |
✅ 屬實,無改判 |
| F2 遮罩名單漏掉物件儲存 | ⚪ 低 | 打開套件 plugin/contract.py,數第 61 行的群組名單確實只有四組、沒有物件儲存;第 65 行的欄位名單確實只有兩個、沒有物件儲存實際用的那個欄位名。上方註解確實寫著「凍結」 |
✅ 屬實 |
F1 為什麼維持「高」(照決策者的「可利用門檻」判準):只要一個能登入的帳號,不需要任何權限、不需要知道任何編號,一個請求就拿到可直接讀寫所有客戶上傳檔案的鑰匙。這與 FR-086 的「登入即可跨客戶取檔」是同一級別。
報告寫「每建一個新客戶就原封不動複製一份,所以全部客戶共用同一把」——這句話推論方向對,但當時是從程式碼推的,沒有實際資料佐證。首腦 2026-09-15 對開發環境資料庫做唯讀查詢(只比對雜湊值、沒有印出任何明文密鑰),結果:
那為什麼還有四個客戶是空的? 因為負責複製的那支程式「失敗不擋建立」——開發環境沒有真的接物件儲存,它就靜靜跳過了。正式環境每一套安裝都會跑寫入程序,客戶那邊的欄位會是滿的。
這個修正不會降低嚴重度,反而讓它更明確:從「推測會共用」變成「已實測到兩個客戶手上拿著同一把」。
| 卡上第幾點 | 結果 |
|---|---|
| ② 遮罩名單漏物件儲存 | ✅ 成立,就是 F2 |
| ⑤ 四道守門是否真的掛上、能否繞過 | ✅ 成立,而且查出更基本的事:四道守得全是寫入,讀取根本沒有第五道(F1) |
| ④ 新增端點能否用任意欄位灌進資料 | ❌ 三個檢查員一致否決,理由站得住(資料結構只有六個固定欄位,多送直接報錯) |
| ⑦ 預設值裡的登入效期單位寫錯 | ❌ 三個檢查員一致否決,理由站得住(同一數值在另外兩處也是內建預設,刪掉這行效期一秒都不會變)。沒有查出硬編的密碼 |
| ① 讀設定時把整列(含密碼)寫進日誌檔 | 首腦自核屬實(app/service/system_config_service.py:20),但與跨 arc 總表已登記的 FR-085 C2「密碼與 JWT 原文進日誌」是同一根因,不另計一條。本棒的價值是確認源頭就在這張設定表,下游不必再一條一條撿 |
| ③ 遮罩機制對第三種資料形狀會不會靜默失效 | ⬜ 工具根本沒碰——屬「沒被提出」,不是「查過沒問題」 |
| ⑦ 的另一半 隔離規則有沒有漏洞 | ⬜ 同上,沒碰 |
| ⑧ 底層查詢沒給條件就回全表 | ⬜ 同上,沒碰 |
分兩層,答案不一樣:
| 項目 | 狀況 |
|---|---|
| 報告 | ✅ runner 補交,scan-P1-config-chain.md |
| commit | ✅ 首腦補(runner 未自行 commit) |
| Notion 子卡回寫 + 狀態改「修正待驗證」 | ✅ 首腦補(runner 未自行回寫) |
| 修正卡 | ✅ 已處理——F1/F2 已登記跨 arc 總表 §3.1,後由 FR-114 修正線修完(M22,1.21.0 出貨) |
結論:兩條發現都成立,但淨新增只有 1 條。可以派第 3 棒。
首腦親自打開那份章檔核對(不是聽掃描的 session 轉述):
| 項目 | 結果 | 白話 |
|---|---|---|
| 驗證章狀態 | verified |
完整通過,沒有中途被拒收 |
| 候選問題 → 去重後 | 2 → 2 | 沒有重複的 |
| 投票數 | 6(2 條 × 3 個檢查員) | 全部投完,兩條都 3:0 通過 |
| 未審候選 / 面板中斷 / 被降級 | 0 / 0 / 0 | 沒有漏審、沒有斷線、沒有人偷偷調低嚴重度 |
| 派出 / 回報的研究員 | 1 / 1 | — |
| 耗時 | 790 秒(13 分 10 秒) | 見下方「為什麼只跑 13 分鐘」 |
| 掃描的程式版本 | 2b11bd54,工作區乾淨(dirty: false) |
掃的就是那一版 |
為什麼只跑 13 分鐘:P1 跑了 2 小時 39 分,差一個數量級。原因是投票成本=候選數×3 位檢查員,P1 有 4 條候選、這棒只有 2 條。流程一步沒跳,驗證章完整。且掃描後這 25 檔至今一字未改(首腦比對掃描版本到 HEAD 的差異為空),結果沒有過期。
| 發現 | 嚴重度 | 首腦核對了什麼 | 判定 |
|---|---|---|---|
| F1 讀設定不驗權限 | 🔴 高 | 打開 api/system_config/routes/system_config_route.py,確認 :131 的讀取只掛「要登入」,而 :146 修改、:158 刪除都有權限檢查,連隔壁那支唯讀的「還原出廠設定」(:103)都有 |
♻️ 屬實,但是舊案——就是總表第 72 項,不另計 |
| F2 客戶管理員可改全域登入政策 | 🔴 高 | 見下方「首腦把可信度往上調」 | 🆕 新增,屬實 |
| F2-B LDAP/寄信同形狀 | 🔴 高 | 同一次資料庫查詢確認 ldap-config.update/smtp-config.update 同樣非平台層、同樣發給 8 個非總部客戶 |
🆕 新增,與 F2 同一條記 |
F1 的一處行號更正:工具報告寫 :134(函式內呼叫服務那一行),真正缺守門的是 :131 的函式本身。報告與總表第 72 項一律以 :131 為準,開工單時兩份文件才不會各指一處。
三位檢查員一致認為 F2 成立,但把可信度停在 中等,卡在一個光讀程式碼答不出的問題:「新開的客戶,預設管理員角色到底有沒有拿到這個權限?」 沒拿到的話這條就打不出來。這只有查資料庫能定案,首腦對開發環境做唯讀查詢:
security-policy.update 標記為非平台層,9 個客戶裡有 8 個非總部客戶的管理員角色實際持有它(Billows Admin、歐洲航空系統管理員、以及五個 System Manager)。jedi_iam/app/service/tenant_provisioning_service.py:120-131)——不需要有人手動指派,開一個新客戶就自動多一個能改全公司登入規則的人。→ 檢查員唯一不確定的前提,在開發環境成立。可信度中等 → 高,嚴重度維持高風險。
| 卡上第幾點 | 結果 |
|---|---|
| ⑤ 三支端點守門不一致 | ✅ 實質回答,就是 F1 |
| ② 權限分流表有沒有映得太寬鬆 | 🔶 只碰一半——F2 正是權限層級標錯的實例,但那張對照表本身沒被逐項檢視 |
| ③ 六處關掉客戶隔離的查詢有沒有收窄 | 🔶 只碰一半——F2 用到其中一處當機制,六處沒有逐支稽核 |
| ⑥ 遮罩名單在主專案是否又建第二份 | 🔶 只碰一半——F1 碰到名單漏了儲存設定,但「有沒有第二份」沒查 |
| ① 四個地方繞過守門版服務 | ⬜ 完全沒碰——而這是卡片自己標成「本棒最重要的一條」 |
| ④ 那支「有就更新、沒有就新增」的寫入,併發會怎樣 | ⬜ 完全沒碰 |
| ⑦ 出廠快照的隱藏是不是滴水不漏 | ⬜ 完全沒碰 |
| ⑧ 新客戶繼承設定會不會繼承到別家的 | ⬜ 完全沒碰 |
🔴 ⬜ 是「沒被提出」,不是「查過沒問題」。 尤其第 ① 點完全沒被回答,下一任不要以為那四條路徑乾淨。
| 項目 | 狀況 |
|---|---|
| 報告 | ✅ 首腦補寫(runner 未交付,scan-H1-host-wiring.md) |
| commit | ✅ 首腦補(runner 未自行 commit) |
| Notion 子卡回寫 + 狀態改「修正待驗證」 | ✅ 首腦補(runner 未自行回寫) |
| 修正卡 | ✅ 已處理——F2/F2-B 登記於總表 §3.1 第 74 項(客戶管理員可改全公司登入規則),由 FR-114 修正線修完:登入規則只有客戶組織樹第一層能改(內化版 M22-1,1.21.0 出貨) |
⚠️ 這是全計畫第七次 runner 未交付四件(前六次:C1/S7/B2/H3/H1/FR-096 P1)。掃描本身完整有效,缺的只是「寫成給人看的文件」這一步。
結論:零發現屬實,否決站得住。這支套件三棒全數收尾。
| 項目 | 結果 | 白話 |
|---|---|---|
| 驗證章狀態 | verified |
完整通過,沒有中途被拒收 |
| 候選問題 → 去重後 | 1 → 1 | — |
| 投票數 | 3(1 條 × 3 個檢查員) | 全投,0 通過/1 否決(一致票) |
| 未審候選 / 面板中斷 / 被降級 | 0 / 0 / 0 | 沒有漏審、沒有斷線 |
| 派出 / 回報的研究員 | 1 / 1 | — |
| 耗時 | 638 秒(10 分 38 秒) | 候選只有 1 條,投票成本=候選數×3 |
| 掃描的程式版本 | 8bc5d27,工作區乾淨 |
不是開卡當時那版——3f382d8 在開卡後改過範圍內兩支檔,首腦派工前已標明 |
| scope 檔數 | 33(與卡片一致) | 首腦派工前實查對上 |
候選內容:前台取下拉選項的端點(任何登入者可打)只過濾「啟用」、沒過濾「公開」(system_menu_route.py:39)。
首腦四個來源逐項自核,同意否決:
| 查了什麼 | 結果 |
|---|---|
| 出貨預設資料 | 74 筆全部公開(migrations/004-system-menus-seed.sql) |
| 欄位預設值 | 資料表/程式模型/資料物件三處都是「公開」 |
| 誰寫得動這欄位 | 只有新增與修改兩支,兩支都要平台管理員(system_menu_route.py:93/:113,首腦開檔確認 @platform_admin_required 在最內層) |
| 開發環境實查(首腦重查,2026-09-15) | 74 筆、標成私有的 0 筆、3 筆停用;該表無隔離、0 條規則(與派工前查證一致) |
→ 今天打過去選不到任何東西,否決正確。
同一支程式裡就有反例——管理端的分頁查詢有強制過濾公開欄位(app/service/system_menu_service.py:38 寫死 _filter.public = 1),前台那支反而沒有。首腦開檔確認兩處屬實。
意思是「私有」這個設計在管理端被當真、在前台沒有。哪天平台管理員標了一列私有,那一列會立刻出現在所有登入者的下拉選單裡。不是今天打得到的漏洞(要先有平台管理員去建那種資料),故不列為發現;修法只有一行(第 39 行同時檢查兩個欄位)。歸「未來的陷阱」不歸資安發現。
守門順序(四條管理端點都疊平台管理員、順序跳不過)、前台端點回傳範圍、讀選單寫日誌(會,但這張表無密碼無個資,影響低)、排序欄位白名單(有,對不上直接忽略)、撞鍵錯誤訊息(固定句、不帶實際值)、兩支 SQL(內容乾淨、都有防重複機制)——六項全部通過。
__init__.py,剩下 20 支能不能保證每支都被讀過,這份結果給不出保證。| 項目 | 狀況 |
|---|---|
| 報告 | ✅ runner 自行交付(scan-P2-menu-dictionary.md) |
| commit | ✅ runner 自行 commit(a5a175f2) |
| Notion 子卡回寫 + 狀態改「修正待驗證」 | ✅ runner 自行完成 |
| 修正卡 | 無需開卡(零發現) |
✅ 這一棒交付四件齊全——本 arc 三棒裡唯一做滿的一棒(P1 缺三件、H1 四件全缺)。
三棒的分界是「密碼在哪、權限在哪、其餘」:
為什麼不按分層水平切:權限漏洞活在層與層的接縫上,只給端點或只給服務兩邊都看不出來。三棒都是從端點一路到資料庫的完整垂直切面。
為什麼設定與選單不合成一棒:合起來 57 檔超過一棒的上限(面板成本=候選數×3 個檢查員,每個從零讀檔),而且兩張表的租戶語意刻意相反(設定要隔離、選單要共享),混在一棒會讓研究員把兩套標準搞混。
選單表沒有客戶隔離,是正確的,不是漏洞。 首腦 2026-09-15 對 DEV 實查:這張表沒開隔離、0 條規則、共 74 筆,內容全是下拉選單的碼表(使用者狀態、啟用狀態、問卷題型這類),沒有客戶資料、沒有個資。選單字典是全站 UI 的資料來源,隔離起來會壞掉所有頁面。套件的說明也寫明兩張表的語意刻意相反:「設定是每個客戶自己的參數,選單是全站共用的字典」。
但有一件相關的事要記一句:查選單的請求格式六個欄位全部非必填,送一個空的查詢條件就回全表。以這張表的內容而言影響有限,但它屬於首腦正在盤的「共用底層送空條件就回全表」那條線(跨 arc 總表 §6 A 組第 15 項,已經第三次撞到)。報告寫一句歸那條線即可,不要當獨立發現升級。
| 項目 | 值 |
|---|---|
| 主 session 模型 | Opus 5 (1M context),不要用 Sonnet(研究員繼承主 session 的 context 上限,Sonnet 跑不完會被工具的「180 秒沒動作就判當掉」砍掉重派) |
| effort | low(單一研究員掃全範圍+三人檢查員面板,不跑盤點、不做威脅建模) |
| focus | attack-surface(跳過測試、打包產物、文件) |
| 派工節奏 | 一次只派一棒,驗收完才派下一棒 |
.claude/skills/security-scan-lead/SKILL.mddocs/features/security-scan-consolidated/README.md~/Projects/Jedicogy/module/jedi-python-package/jedi-system-core/以下全部由 build 掃資料夾產生,新增檔案重 build 即自動出現。標題連結指向渲染後的 HTML,md 連向源檔。
| 文件 | 類型 | 標題 | 最後更新 |
|---|---|---|---|
| scan-H1-host-wiring / md | 盤點證據 | H1 檢查結果:主專案接上「系統設定核心」的那一段 | 2026-09-15 |
| scan-P1-config-chain / md | 盤點證據 | P1 檢查結果:系統設定表的完整路徑 | 2026-09-15 |
| scan-P2-menu-dictionary / md | 盤點證據 | P2 檢查結果:選單字典全鏈(jedi-system-core) | 2026-09-15 |