檢查日期 2026-09-15|耗時 13 分 10 秒|對應卡片 CM-1805|檢查範圍 25 個檔案
只有一件新問題,但它很嚴重:一個客戶公司的系統管理員,可以改掉「全部客戶+我們自己」共用的登入規則——甚至可以把全體員工的登入驗證來源指到他自己的機器上。 另外掃到的那一條不是新的,是上一棒就已經登記過的同一件事(總表第 72 項),只是這次從另一側又撞到一次。
系統裡有一塊「系統設定」的功能:管理員在畫面上調整寄信伺服器、登入規則(要不要雙因子、密碼打錯幾次鎖定)、檔案要存去哪裡、要不要接公司的帳號目錄(LDAP)等等。這塊功能的內臟放在一個共用套件裡(jedi-system-core),而主專案負責把它接到產品上——開出 API、掛上權限檢查、決定哪些欄位要遮起來不給看。
這一棒檢查的就是「接線」這一段的 25 個檔案:誰能讀這些設定、誰能改這些設定、改下去會影響到誰。
上一棒(P1)檢查的是套件內臟那一半(24 個檔),已經驗收完,找到 2 條。這一棒是同一個功能的另一半。
| # | 嚴重度 | 這是什麼問題 | 出事會怎樣 | 要先有什麼才打得到 | 在哪裡 | 狀態 |
|---|---|---|---|---|---|---|
| F2 | 🔴 高 | **一個客戶公司的管理員,改到的不是自己公司的登入規則,而是「所有人共用的那一份」 | 他可以幫全部署的每一個人**(包含我們自己的平台管理員)關掉雙因子驗證、把「密碼打錯幾次就鎖定」改成永不鎖定、把登入憑證有效期從 5 分鐘延長到 1 個月 | ① 任一客戶公司的系統管理員帳號 ② 該帳號不必特別申請——新客戶開通時系統就自動發這個權限 |
端點 api/system_config/routes/security_policy_route.py:31(PUT 在 :59)寫入 app/system_config/service/security_policy_app_service.py:162繞隔離 infra/system_config/system_config_root_reader.py:136 |
🆕 新增 |
| F2-B | 🔴 高 (比 F2 更該先修) |
同樣的手法,可以把「全公司登入時去哪裡驗證帳號密碼」(LDAP 伺服器位址)改成攻擊者自己的機器 | 登入規則被改壞,還看得出來;驗證來源被掉包,等於直接接管所有人的登入——員工照常輸入帳密,帳密卻是送到攻擊者的機器上 | 同 F2(ldap-config.update/smtp-config.update 這兩個權限一樣自動發給客戶管理員) |
app/system_config/service/guarded_system_config_service.py:104 _write_shared_root_config() |
🆕 新增(與 F2 同一條記,修法相同) |
| F1 | 🔴 高 | 讀取系統設定的那支 API 忘了檢查權限,任何登入者都讀得到檔案儲存的帳號密碼 | 拿得到檔案倉庫的帳密就能繞過系統,直接讀走/改掉/刪掉所有上傳的證據檔與文件 | 任何一個能登入的帳號(不需要任何權限) | api/system_config/routes/system_config_route.py:131 |
♻️ 不是新的——就是跨 arc 總表已登記的第 72 項(上一棒 P1 查到的)。這次帶回兩個新細節,補進第 72 項 |
淨結果:工具報 2 條,扣掉 1 條重複,真正新增 1 條(高風險)。問題沒有變多,但新增的這一條很重。
現況:已修(M22-1,CM-2054);後續「誰算總部」判斷的缺口另由 M22-4 修正(CM-2202,1.21.0 出貨)
系統裡有一份「登入安全政策」——要不要雙因子驗證、密碼打錯幾次鎖帳號、鎖多久、登入憑證多久過期。直覺上每個客戶公司應該各有一份、各自調整。
但實際上整套系統只有一份,而且每個客戶公司的系統管理員都改得動它。
他打「更新登入安全政策」這支 API,程式確實有檢查「你有沒有改登入政策的權限」——他有。但接下來寫入的時候,程式刻意關掉了「每個客戶只能碰自己資料」的資料庫隔離機制,把值寫進總部那一份。於是:
| 角色 | 檔案:行號 | 這一行在做什麼 |
|---|---|---|
| 端點守門 | api/system_config/routes/security_policy_route.py:31 |
_update = require_capability("security-policy.update")——只檢查「有沒有這個權限」,沒有檢查「是不是平台管理員」 |
| 更新入口 | api/system_config/routes/security_policy_route.py:59 |
PUT 方法(外層 :57 只要求已登入) |
| 實際寫入 | app/system_config/service/security_policy_app_service.py:162 |
update_policy() |
| 繞過客戶隔離 | infra/system_config/system_config_root_reader.py:136 |
write_root_config_value();其中 :179 明文寫 SET LOCAL app.is_super_admin = 't'(關掉隔離),並把寫入對象釘死成總部那一列 |
三位檢查員一致認為這條成立,但把「可信度」停在 medium(中等),卡在一個他們光讀程式碼答不出來的問題:「新開的客戶,預設的管理員角色到底有沒有拿到這個權限?」 如果沒拿到,這條就打不出來。
首腦到開發環境的資料庫唯讀查了,答案是「有,而且是自動拿到的」:
① 誰實際持有這個權限——security-policy.update 這個權限被標成「非平台層」,9 個客戶公司裡有 8 個非總部公司的管理員角色實際持有它:
| 客戶公司 | 持有的角色 |
|---|---|
| 1 Guidant.AI | Administrator |
| 102 Billows Tech | Billows Admin |
| 131 歐洲航空 | 歐洲航空系統管理員 |
| 152 租戶A/153 租戶B/158 JEDI/162 租戶C/163 租戶D/164 租戶E | System Manager |
② 全域那一份真的只有一份——開發環境的登入政策設定共 7 筆,全部屬於總部(編號 1):是否要雙因子、登入鎖定次數、鎖定時間、憑證有效期、憑證續期有效期、密碼多久要換、多久沒登入算閒置。「一份供全體使用」屬實。
③ 那一份真的被全站吃下去——core/app_factory.py:148 服務啟動時就把它讀進全域設定;core/plugins/identity.py:141 判斷「這次登入要不要雙因子」時直接讀它。
④ 為什麼每個新客戶都會自動拿到(根因)——jedi_iam/app/service/tenant_provisioning_service.py:120-131:新客戶開通時,系統把「所有權限」扣掉標記為「平台層」的那些之後,其餘全部發給該客戶的預設管理員角色。所以任何被標成「非平台層」的權限,新客戶一開通就自動有,不需要有人去手動指派。
→ 檢查員唯一不確定的前提,在開發環境成立。首腦把可信度從 medium 調到 high(高),嚴重度維持「高風險」。
現況:已修(M22-1,CM-2054,與 F2 同一套修法)
這一半工具只順帶提了一句,但首腦查證後認為必須獨立講,而且應該排在 F2 前面修。
很多公司的員工帳號不是存在我們系統裡,而是存在公司自己的帳號目錄伺服器(LDAP)。員工登入時,我們的系統會拿他輸入的帳號密碼去問那台伺服器「這個人對不對」。
「那台伺服器在哪裡」這個設定,跟 F2 一樣是全體共用一份,而且改它的權限(ldap-config.update)同樣被標成「非平台層」,同樣自動發給那 8 個客戶公司的管理員。寄信伺服器設定(smtp-config.update)也是一樣的形狀。
意思是:一個客戶公司的管理員,可以把「全公司登入時去哪裡驗證帳密」指到他自己架的機器上。 之後所有人照常輸入帳號密碼,帳密卻是送到攻擊者手上;他也可以讓自己的機器對任何帳密都回答「對」,直接以任何人的身分登入。
登入規則被改壞,事後看得出來(大家會發現雙因子不見了);驗證來源被掉包,是無聲的接管——畫面完全正常,員工不會察覺,而攻擊者拿到的是全公司的明文帳號密碼。
app/system_config/service/guarded_system_config_service.py:104 _write_shared_root_config()ldap-config.update、smtp-config.update,兩者同樣是「非平台層」,同樣發給那 8 個客戶公司(首腦同一次查詢確認)現況:已依決策者裁定修正(M22-1,CM-2054);以下為掃描當時列的三個方向
好消息是不用造新東西——「只有平台管理員才能做」這道守門已經存在、現成可用:common/authz 對外提供的 require_platform_admin_route(實作在 jedi_iam/authz/decorators.py:29 與 jedi_iam/authz/platform.py:25)。
| 方向 | 做法 | 差別 |
|---|---|---|
| 1. 加掛平台管理員守門 | 在登入政策與共用設定(LDAP/寄信)的寫入路徑上,除了現有的權限檢查,再加一道「必須是平台管理員」 | 最直接,改動最小 |
| 2. 限縮寫入對象 | 不再一律寫進總部那一列,改成「只寫呼叫者自己公司的那一列」,只有總部公司才能編輯總部那一列 | 保留客戶自己調整的能力,但要處理「客戶那一列目前不存在」的情況 |
| **3. 把這三個權限改標成「平台層」 | 把 security-policy.update、ldap-config.update、smtp-config.update 標記為平台層 |
治本**——改標之後新客戶不會再拿到;但已經發給 8 個客戶的授權仍然存在,要另外清掉 |
⚠️ 三個方向都要決策者拍板,因為它們會改變一件產品行為:「客戶自己到底能不能調整自家的登入強度」。這不是純技術選擇。
現況:已修(M22-2,CM-2063)
這條就是跨 arc 總表第 72 項,上一棒(P1)就已經登記過的同一件事。 第 72 項原文已經明白列出三個入口,其中就包含主專案的 api/system_config/routes/system_config_route.py:131。H1 只是從主專案這一側再撞到同一件事。問題沒有變多,不要重複計數、不要重複開修正卡。
「讀取某一組系統設定」的那支 API,只檢查「你有沒有登入」,沒有檢查「你有沒有權限看這個」。任何一個能登入的帳號——包括權限最低、完全不該碰設定的專案成員——都能讀出檔案儲存的位址、桶名、帳號與密鑰。
而這組帳密是安裝程式寫進去的、這台機器真正的檔案倉庫憑證,而且新客戶開通時會原封不動複製一份。拿到之後可以繞過整個系統,直接讀走、覆寫或刪掉倉庫裡所有上傳的證據檔、SSP 文件與檢測報告。
api/system_config/routes/system_config_route.py:
第 131 行 讀取 get(self, group, key) 只有「要登入」 ← 沒有檢查權限
第 146 行 更新 put(...) 有檢查「更新設定」的權限
第 158 行 刪除 delete(...) 有檢查「刪除設定」的權限
同一支檔案裡的「還原出廠設定」功能:
第 103 行 讀取 GET 有檢查「讀取儲存設定」的權限 ← 連唯讀的都有
第 111 行 執行 POST 有檢查「更新」的權限
同一支檔案裡,唯獨這一支讀取沒有權限檢查,寫入的三支都有,連隔壁那支唯讀的「還原出廠設定」都有。這證明是漏掉,不是刻意設計。
⚠️ 行號更正:工具原始報告寫
:134,那是函式內部呼叫服務的那一行;真正缺守門的是:131的函式本身。本報告與總表第 72 項一律寫:131,開工單時兩份文件才不會各指一處。
新細節 1:同一個沒守門的入口,吐出來的不只是檔案倉庫帳密。 還包括登入安全政策(PASSWORD_POLICY/CONFIG)與閒置逾時設定(WEB_IDEL_CONFIG)——等於把「這套系統的登入防護設定成什麼樣」整份攤給任何登入者看,是攻擊前的偵查材料。
新細節 2:遮罩只遮掉「密碼」一個欄位,其餘照樣外洩。 系統有一份「要遮起來不給看」的名單,但它只遮掉欄位名叫 secret 或 private_token 的值。所以同一列裡沒被遮的欄位一併外洩:
名單本身也有缺口——jedi_system_core/plugin/contract.py 的遮罩群組名單是 SMTP、THIRD_PARTY_LOGIN、NOTIFY_CONFIG、ISSUE_INTEGRATE_CONFIG,沒有檔案儲存設定;遮罩欄位名單是 secret、private_token,沒有物件儲存用的 access_key/secret_key。(主專案端 api/system_config/routes/system_config_route.py:81 的 _MASK_CONFIG 與 :84-86 的 _mask() 只是委派套件那份機制。)
修法隨第 72 項辦理,重點兩件事:
api/system_config/routes/system_config_route.py:131 的 get() 第一行補上 assert_config_capability(group, "read"),與同檔的更新、刪除一致。access_key/secret_key/minio_access_key/minio_secret_key 加進遮罩欄位名單——這樣就算未來又漏一次守門,也不會直接變成帳密外洩。理由一:這是快篩,不是全面檢查。 用的是最低強度模式——一位研究員把 25 個檔案讀完就提報,不做元件盤點、不做威脅建模、不跑密鑰專項掃描。所有結論都出自「讀程式碼」,沒有實際打過任何 API、沒有跑過任何攻擊驗證。
理由二(最重要):卡片列了八個要追查的點,工具只碰了一部分。
| 卡片上的追查點 | 結果 |
|---|---|
| ⑤ 三支端點的權限守門不一致 | ✅ 實質回答了——就是 F1 |
| ② 權限分流表有沒有映得太寬鬆 | 🔶 只碰到一半——F2 正是「權限層級標錯」的一個實例,但那張對照表本身沒有被逐項檢視 |
| ③ 六處「關掉客戶隔離」的查詢有沒有收窄 | 🔶 只碰到一半——F2 用到其中一處當作機制,但六處沒有逐支稽核 |
| ⑥ 遮罩名單在主專案是不是又建了第二份 | 🔶 只碰到一半——F1 碰到名單漏了檔案儲存設定,但「主專案有沒有第二份名單」沒查 |
| ① 四個地方繞過守門版服務 | ⬜ 完全沒碰(auth_containers.py:180/notification_containers.py:20/user_change_password_containers.py:46/login_containers.py:14-15) |
| ④ 那支「有就更新、沒有就新增」的寫入,兩個人同時打會怎樣 | ⬜ 完全沒碰 |
| ⑦ 出廠快照的隱藏是不是滴水不漏 | ⬜ 完全沒碰 |
| ⑧ 新客戶繼承設定會不會繼承到別家的 | ⬜ 完全沒碰 |
🔴 ⬜ 的意思是「沒有被提出」,不是「查過沒問題」。 尤其第 ① 點是卡片自己標成「本棒最重要的一條」的,而它完全沒有被回答——下一任接手的人不要以為那四條路徑是乾淨的。
理由三:範圍只涵蓋一半。 這一棒只掃「主專案接線」這 25 個檔,選單字典那一棒(P2,33 個檔)還沒跑。現在還不能說「系統設定核心已經掃過了」。
| 項目 | 數字 |
|---|---|
| 檢查範圍 | 25 個檔案(與卡片逐檔對上) |
| 檢查強度 | 最低(low),聚焦「攻擊面」 |
| 派出/回報的研究員 | 1 / 1 |
| 候選問題 → 去除重複 | 2 → 2 |
| 投票數 | 6(2 條發現 × 3 位檢查員),兩條都 3 票全過 |
| 沒投到票的 / 投票中斷的 / 被降低嚴重度的 | 0 / 0 / 0 |
| 驗證章狀態 | verified(無拒收理由) |
| 掃描當下的程式碼版本 | commit 2b11bd54,branch main,工作區乾淨 |
| 耗時 | 13 分 10 秒(790 秒) |
上一棒(P1)跑了 2 小時 39 分,這一棒 13 分鐘,差了一個數量級,看起來很可疑。原因是:
投票的成本 = 候選問題數 × 3 位檢查員。 P1 有 4 條候選要投,這一棒只有 2 條,時間自然短。流程一步都沒有跳過——驗證章是完整的 verified,6 票全投、沒有中斷、沒有未審的候選。
短的原因是「找到的東西少」,不是「檢查得少」。至於「找到的東西少」是不是等於「問題少」,看上面「是不是只有這兩條」那一段——答案是不能這樣推論。
📄 本報告由首腦依工具原始產物補寫(該棒 runner 未交付報告);掃描本身完整有效,缺的只是「寫成給人看的文件」這一步。