本卡屬 FR-114 資安修正(母卡 CM-2019),決策者 2026-09-24 裁 A 案納入本批:SUMMARY #96(M16-2)、#97(M16-3)+ CM-1605 程式面加強(M16-1)。中風險。套件 jedi-notification 隨本批發版(發版卡 CM-2110 第三段等本卡)。
① 寄信失敗一次(密碼打錯、網路抖一下),系統把整組郵件設定連密碼原樣寫進紀錄檔——看得到紀錄的維運、能查資料庫的人、外部紀錄收集服務,都等於拿到密碼。共用地基那道密碼遮蔽蓋不到這個格式(報告實跑過,一個字都沒遮)。② 連郵件伺服器時有加密但不確認對方是誰:攔在網路路徑上的人拿一張自簽證明,系統照收,接著把帳密和每封信(含登入驗證碼、新帳號初始密碼)送過去。成因是 Python starttls() 不帶參數時預設不驗,不是我們關掉的。③ 管理員按「寄測試信」時把伺服器位址改成自己的機器、密碼欄不動,系統會拿存著的真密碼去連那台。
首腦核對過的證據:
套件 jedi-notification/jedi_notification/infra/smtp_mail/smtp_mail_adapter.py
:39 server = smtplib.SMTP(self.config.smtp_server, self.config.port, timeout=60)
:43 server.starttls() ← 不帶 context,不驗憑證(#97)
:52 logger.error(f"Timeout sending email smtp info: {self.config}") ← 整組含 password(#96)
:57 logger.error(f"Error sending email smtp info: {self.config}") ← 同上(#96)
套件 jedi-notification/jedi_notification/app/dto/smtp_email_config_dto.py pydantic,欄位 from_name/from_address/smtp_server/port/username/password/tls/is_html
BE app/notification/service/test_mail_service.py (121 行)
:80-84 使用者沒改密碼 → 用 DB 存著的 secret 覆蓋,不管 host 有沒有被改(CM-1605)
:94-103 組 SmtpEmailConfigDTO
BE app/notification/service/notification_service.py:58-71 正式寄信讀 ROOT SMTP/'*' 組 DTO
設定存放:public.system_configs,group='SMTP',value 是 jsonb。DEV 實查兩列(tenant 1 key='*'、tenant 102 key='gmail.com'),value 鍵=tls,from,host,port,user,enable,secret,from_name
FE src/views/smtp-config/SmtpConfigForm.vue (341 行) :32 預設 tls:true、:281-282 tls 開關
套件 /Users/chouraymond/Projects/Jedicogy/module/jedi-python-package/.claude/worktrees/jedi-wt-fix-security branch fix/security-b1
BE /Users/chouraymond/Projects/Billows/Audit-Manager/compliance-manager-be/.claude/worktrees/wt-fix-security branch fix/security-b1
FE /Users/chouraymond/Projects/Billows/Audit-Manager/compliance-manager-fe/.claude/worktrees/wt-fix-security branch fix/security-b1
🔴 主 checkout 給掃描線讀,絕不動。⚠️ 發版卡 CM-2110 可能同時在套件工作區改 17 支 pyproject.toml 的版號——你不要動任何 pyproject.toml 的 version 行(jedi-notification 的版號由發版卡處理),commit 前 git status 只 add 你自己的檔。
smtp_mail_adapter.py:52、:57 改成只記伺服器位址、port、錯誤類別與訊息(例 logger.error("SMTP send failed: host=%s port=%s error=%s", ...)),不再出現 self.config 或任何含 password 的物件。exc_info=True 那行保留(堆疊不含設定物件;若 exception 訊息本身可能帶帳密,改成只印 type(e).__name__)。{self.config}、self.config)、config.password 被格式化進 log 的其他地方,一併收掉。tls_verify(bool)與 tls_ca_cert(PEM 字串,可空)。鍵不存在=舊設定=當作不驗(相容升級)。命名比照 jedi-log 日誌轉送已有的 tls_ca_cert(jedi-log/jedi_api_log/forwarding/api/serializers/log_forwarding.py:27)。SmtpEmailConfigDTO 加 tls_verify: Optional[bool] = None、tls_ca_cert: Optional[str] = None。adapter:tls 為真時,tls_verify 為真 → ctx = ssl.create_default_context(),有 tls_ca_cert 就 ctx.load_verify_locations(cadata=...),server.starttls(context=ctx);tls_verify 為假或 None → 維持現行 server.starttls()。驗證失敗(ssl.SSLCertVerificationError)回 False 並 log 一行「郵件伺服器憑證驗證失敗,請貼上信任憑證或檢查伺服器設定」(同樣不帶設定物件)。先查 jedi-log forwarder.py:177 附近怎麼建 context,照同一寫法,不另立 helper。test_mail_service.py:94-103 與 notification_service.py:61-70 組 DTO 時多帶 tls_verify=smtp_config.get("tls_verify")、tls_ca_cert=smtp_config.get("tls_ca_cert")。notification_service.py 若超 800 行照新開檔規則。SmtpConfigForm.vue:① 新建時 tls_verify 預設 true(:32 附近預設值);② 在 tls 開關下方加「驗證伺服器憑證」開關(tls 關閉時隱藏/停用)與「信任憑證(PEM,選填)」多行輸入;③ 關閉驗證時開關旁顯示一行風險說明;④ 載入舊設定時 value 裡沒有 tls_verify 鍵→ 畫面顯示為「未驗證」且頂部顯示提示「目前未驗證郵件伺服器憑證,建議開啟」——但不要自動改成 true 存回去(使用者存檔才算數)。版面參考日誌轉送設定頁 LogForwardingForm.vue 的 TLS 區塊與 i18n log-forwarding.json 的寫法。文案三語(zh-tw/en/zh-cn)。