本卡屬 FR-078(母卡 CM-1602),修 N2(CM-1604)的 F1。面板 2/3 通過(反對票的理由首腦判定不成立,見下)。嚴重度 HIGH這是 N1+N2 兩棒真正屬於範圍內的主要成果。

問題是什麼(白話)

設定頁上的「發送測試郵件」按鈕,會把系統存著的 SMTP 密碼,送去呼叫者自己指定的郵件伺服器。攻擊者只要在自己機器上開一個假的 SMTP 服務,然後按下測試,平台的郵件帳號密碼就會被送過去。

更麻煩的是 SMTP 設定是全平台共用一組、存在 ROOT 租戶,但管理權下放給了業務租戶。所以這是「子租戶管理員拿走平台營運方的憑證」,不是自己拿自己的。拿到之後可以用平台的名義寄信(釣魚信會通過 SPF/DKIM 驗證)。

產品其實已經知道這個密碼該保護——設定讀取 API 專門把 SMTP 這組遮蔽掉(HIDDEN_SECRET)。這個端點等於繞過了那道已經做好的防護。

首腦核對過的證據

以下行號首腦已逐一開檔確認:

app/notification/service/test_mail_service.py:84   ← 把 ROOT 的既存密碼寫進使用者送來的設定
app/notification/service/test_mail_service.py:116  ← 拿著它去連使用者指定的 host
jedi-notification/jedi_notification/api/routes/mail_route.py:28  ← @use_kwargs(..., apply=False):schema 只是文件,沒有實際驗證
jedi-notification/jedi_notification/api/routes/mail_route.py:30-31 ← 守門只有 @auth_required + @capability_required

change_pwd 預設是 False,也就是預設就會回填密碼。request body 沒有經過任何 schema(apply=False),smtp_config 的 host/port/user/tls 全部由呼叫端自由指定。從 body 到 smtplib.SMTP() 之間,沒有任何一處檢查「送來的 host 是不是這組密碼原本所屬的 host」。

關於那張反對票(首腦判定不成立)

面板 2/3 通過,反對的那票不否認漏洞本身,理由是「@capability_required 守門縮小了可觸及範圍」。首腦判定這個理由不成立smtp-config.update 這個能力點正是刻意下放給業務租戶管理員的(見 system_config_root_reader.py 內 CM-1331 的說明:業務租戶管理員持有此能力點卻被擋,是當初要修的 bug)。所以持有者不是少數平台管理員,而是每個業務租戶的管理員——守門存在恰恰是攻擊成立的前提,不是防護。修正時不要因為「有守門」而降低優先序。

怎麼修

先查再寫:LDAP 有一模一樣的問題,已在 CM-1564 修過(連線測試改位址時強制重輸密碼)。先去讀那張卡與它的 commit(BE d4d1b0e4、FE 0f1d0ed),照同一個 pattern 做,不要另創一套。

要寫測試

本卡屬 CLAUDE.md 測試政策的「核心共用邏輯」例外(認證/授權/憑證處理),要寫測試