---
title: 寄信與通知
eyebrow: Guidant AI 資安檢視 · 模組報告
h1: 寄信與通知（jedi-notification）
lede: 系統對外講話的嘴巴——寄稽核提醒信、推 Discord 與 Telegram 訊息。**最受關注的一條是「寄測試信」那個按鈕**：寄信設定全系統只有一份、存在最上層，而「改寄信設定」的權限下放到各個下層客戶，所以下層的管理員可以按一下，把上層那組郵件帳密導到自己的機器上。
---

## 這塊在產品裡做什麼

系統要通知人的時候——寄稽核提醒信、把流程事件推到聊天群組——走的都是這塊。它管三條通知管道：

| 管道 | 需要保管什麼機密 |
|---|---|
| **寄信** | 郵件伺服器位址 ＋ 帳號 ＋ **密碼** |
| **Discord 群組** | 一組專屬網址（誰拿到誰就能以我們的名義在那個群組發文） |
| **Telegram** | 機器人通行證（拿到即可冒充那隻機器人） |

::: grid2
::: {.card .plain}
#### 它平常在做什麼
1. 客戶在設定頁填好通知管道的帳密
2. 系統把帳密**存起來**
3. 要通知人時，**主動對外連線**把訊息送出去
:::
::: {.card .warn}
#### 為什麼它是敏感目標
三條管道有同一個共同點：**都要保管客戶填的機密，都要主動對外連線**。

保管機密 → 有「機密怎麼被讀出來」的風險。
主動連外 → 有「被騙去連到別的地方」的風險。
:::
:::

---

## 檢視軌跡

**檢視期間**：2026-09-08（單日完成兩輪）｜**範圍**：48 個檔案（模組本體 30、主系統接線 18）｜**共 2 輪**

> **原始技術報告**放在需求中心的 [FR-078 站](https://guidantai-feature-doc.jedicotech.com/FR-078-2609-notification-security-scan/)，檔名見下表最後一欄。那裡有每一輪的完整技術細節、檔案清單與逐條推理，供需要深究或稽核抽查時查閱。

| 輪次 | 查什麼 | 檔數 | 覆核投票 | 結果 | 原始報告檔名 |
|---|---|---:|---|---|---|
| **N1** | 模組本體——三條管道怎麼存機密、怎麼對外連線 | 30 | 6 票全投完，兩條都 3 票一致 | 通過 2 條，都在同一支檔案裡 | `scan-N1-package-core` |
| **N2** | 主系統這端怎麼把它接上來——尤其是「測試連線」那些按鈕 | 18 | 23 票全投完 | 通過 6 條（**範圍內 2 條**，另 4 條是順手撈到的密碼外洩） | `scan-N2-host-wiring` |

::: {.callout .ok}
**這是整批檢視裡第一次覆核完整跑完**

在此之前的那一輪（遠端代理程式第一輪）覆核程序全部因為用量超標被中斷，一票都沒投出來。**這一輪是第一次拿到完整紀錄**，而且不是靠運氣——是把範圍縮小到 30 個檔以內換來的。

覆核者也沒有敷衍了事：他們把系統底層負責寄信與加密的那幾段現成程式**逐行讀出來**，證明「不帶設定的加密連線確實不驗對方身分」，不是照單全收提報者的說法。
:::

::: {.callout .warn}
**第二輪中途撞到用量上限，但處理方式是對的**

第二輪驗到一半撞上用量上限，有一條的三位覆核者只回來兩位。**工具沒有拿兩票充數**——它把那一條整條作廢、交給下一輪用完整三人重新投一遍。

所以票數是 23 不是 21：多出來的兩票，正是第一輪那條作廢掉的。**「續投」不是補一張票進去，是整條重來**——因為第三票如果在已知另外兩票結果的情況下投，性質就完全不同了。
:::

::: {.callout .warn}
**三件沒查到、不能當作乾淨**

1. **背景寄信時會不會讀到別家客戶的郵件設定**——完全空白。若真的會讀錯，後果是「用 A 客戶的郵件伺服器寄 B 客戶的信」，屬於跨客戶外洩。
2. **Telegram 的機器人通行證拼在網址裡**——網址會進系統紀錄檔與中間的網路設備紀錄，外洩面比放在其他位置大。這一輪沒有追這條。
3. **那四條順手撈到的密碼外洩，不代表文件目錄與腳本目錄被檢查過**——那兩處從來就不是檢視目標。
:::

---

## 問題一覽

**排序按風險，同風險的同一類排在一起。**「出事會怎樣」那欄講的是**業務影響**——誰受影響、損失什麼，不是技術現象。

| # | 問題 | 風險 | 分類 | 出事會怎樣（誰受影響） | 怎麼修 | 狀態 |
|---|---|---|---|---|---|---|
| **1** | 「寄測試信」會把系統存著的那組郵件密碼，送到按鈕操作者自己填的那台主機 | 🟡 中 | 密碼外流 | **寄信設定全系統只有一份、存在最上層；而「改寄信設定」這個權限下放到各個下層客戶**，所以下層的管理員可以把上層那組郵件帳密導到自己的機器上。拿到之後能用那個身分對外寄信，而且**會通過寄件人驗證——收件人完全看不出是偽造的**。在我們自己營運的多租戶環境裡，上層就是我們公司；落地安裝版裝好時這一頁是空的、帳密由客戶自己填，外流的是客戶自己那組 | **由客戶自行管控**——寄信設定屬最上游帳號的權責，要不要把這個權限下放給子單位是客戶自己的決定。程式面可順手加強一項：測試連線時目標主機換了，就不沿用存著的密碼、要求重填 | ✅ **程式面加強已做**——伺服器位址或連接埠跟存著的不一樣時，不沿用存著的密碼、要求重新輸入（CM-2111）。**權限面由客戶自行決定**。<br>**評為中風險的理由**：外流的是同一家公司內部上層的帳密，**範圍縮在單一客戶內部**，與其他「同一家公司內部越權」的條目同一個量級 |
| **2** | 寄信失敗時，把整組郵件設定原樣寫進系統紀錄檔——那組設定裡有密碼 | 🟡 中 | 敏感內容寫進日誌 | **看得到系統紀錄的人就等於拿到密碼**——維運人員、能查資料庫的人、外部紀錄收集服務的管理員，這些人**遠多於被授權改郵件設定的人**。而且不需要攻擊者做任何事，**寄信失敗一次就會發生**（密碼填錯、網路抖動都算）。⚠️ **共用地基那道密碼遮蔽蓋不到這裡**，實跑驗過，密碼一個字都沒被遮掉 | 錯誤訊息只印錯誤本身，不要把整組設定丟進去 | ✅ **已修**——失敗紀錄只記伺服器位址、連接埠與錯誤類別（CM-2111）。主系統掃描又從系統診斷包那一端撞到同一件事（總表第 207 項），不另列；它暴露的那種密碼寫法已併進診斷包遮罩那張卡一起修好（8-B，CM-2212，1.21.0 出貨） |
| **3** | 連郵件伺服器時「有加密但沒認人」——對方拿一張隨便自簽的證明來，系統照單全收 | 🟡 中 | 密碼外流 | 除了同樣洩漏郵件帳密之外，**每一封信的內容都會落到攔截者手上**——包含登入用的一次性驗證碼與新帳號的初始密碼信。**等於帳號被接管的材料整批送出去**。這條要成立，攻擊者得先在我們與郵件伺服器之間的網路路徑上（郵件服務在外部雲端時比較容易做到） | 設定頁加「驗證伺服器憑證」開關與信任憑證欄位；新建設定預設要驗 | ✅ **已修**——新建／重存的設定預設驗證；**升級前的既有設定維持不驗**，設定頁頂部提示建議開啟（CM-2111） |
| **4** | 「測試 Discord 群組」填什麼網址就打什麼，系統不限制目標 | ⚪ 低 | 外部送什麼就收什麼 | **拿我們的伺服器當跳板去探測客戶內網**——哪台機器活著、哪個服務開著。按得動的人比「寄測試信」更少：**要有通知設定的權限，還要是帳號層的最高管理員**。因為回應內容不會送回給操作者，所以偷不到資料、只能探測，風險因此評為低 | 兩層一起做：① 擋掉指向內網的位址；② 只允許已知的通知服務網域（Discord 那支就只能打 Discord）。設定畫面完全不動 | ⬜ **未修**（併入工單 CM-1605） |

### 檢視時順手做密碼掃描撈到的四條

這四條跟寄信通知**沒有關係**——是檢視時順手做的密碼掃描撈到的。列在這裡是為了完整。

| # | 問題 | 風險 | 分類 | 出事會怎樣（誰受影響） | 怎麼修 | 狀態 |
|---|---|---|---|---|---|---|
| **5** | 一個測試用設定檔被推上版控，裡面是四把真的對外 AI 服務金鑰 | 🟠 高 | 密碼外流 | **直接的財務損失**——任何拿得到程式庫的人都能用我們公司的帳號呼叫那些服務；其中一把還能讀走服務端存的執行紀錄，裡面例行含有客戶資料 | 撤銷重發、把檔案撤出版控 | ✅ **已修正**——金鑰已撤銷換發，**版控殘留字串待清**（工單 CM-1607） |
| **6** | 同一個檔案裡還有三樣東西：**簽發登入憑證的金鑰**、**資料庫密碼**、**檔案儲存服務的金鑰**。三樣的嚴重度不同，分項如下 | ⚪ 低<br>（三項同級，理由見下） | 密碼外流 | **① 簽發登入憑證的金鑰**：拿到它就能自己偽造一張管理員身分的登入憑證，不需要密碼也不需要雙因子。<br>**② 資料庫密碼**、**③ 檔案儲存服務的金鑰**：拿到就能直接讀寫資料。<br>🔴 **三樣都只影響我們自己的開發機**——安裝程式每裝一套都會各自隨機產生新的一份，**所以外流的這三樣不會跟著出貨到任何客戶**。影響範圍從「每一套裝出去的都中」縮成「我們自己的開發環境」 | 同上 | ✅ **已修正**——三樣都已撤銷換發，**版控殘留字串待清**（工單 CM-1607） |
| **7** | 一支產生文件的腳本，把**客戶展示環境**的完整資料庫連線資訊（含密碼）寫死在程式碼裡 | 🟡 中 | 密碼外流 | **可以直接連進客戶會看到的展示環境資料庫**——那個環境依規定等同正式環境 | 改成從環境設定讀取 | ✅ **已修正**——該組密碼已換發，**版控殘留字串待清**（工單 CM-1608） |
| **8** | 系統管理員帳號的密碼寫死在三支腳本裡，其中兩支寫成「環境變數沒設就用這個」——而那個「這個」就是真密碼 | 🟡 中 | 密碼外流 | 拿到程式庫就等於拿到一組可用的管理員帳密。**變數忘了設時會靜默用真密碼，操作的人不會被告知** | 同上 | ✅ **已修正**——該組密碼已換發，**版控殘留字串待清**（工單 CM-1608） |

::: {.callout .warn}
**這四條都與其他模組頁重疊，問題總表請只算一次**

- **第 5 條（四把 AI 服務金鑰）＝ [公告那塊](M17-bulletin.html)第 9 條**，同一批九個檔。
- **第 6 條（簽發登入憑證的金鑰）＝ [公告那塊](M17-bulletin.html)第 6 條**，同一把，**兩頁的評級一致（低）**——安裝程式每裝一套都會各自隨機產生，外流的只是我們開發機那一份。
- **第 7 條（腳本寫死展示機連線資訊）＝ [意見回饋那塊](M23-issue.html)第 1 條同一組密碼**——而且它是那 249 個檔裡**唯一一支會被執行的腳本**，其餘全是文件與對話紀錄。
- **第 8 條（三支腳本寫死管理員密碼）＝ [公告那塊](M17-bulletin.html)第 11 條**，但那一頁只列到一支，**這裡的三支才是完整範圍**（兩支寫成「環境變數沒設就用這個」、一支直接寫死沒有逃生門），三支用的是同一組密碼。

**所有外流的通行證、金鑰、密碼都已經撤銷或換發完畢**，剩下的只有把字串本身從版控裡清乾淨這件事——性質是打掃，不是堵漏。
:::

---

::: {.callout .plain}
**下面只展開需要你自己判斷、或容易被誤解的幾條。** 其餘的修法明確，看上面表格的「怎麼修」那一欄就夠了——**沒有展開不代表沒查，也不代表不重要。**
:::

## 第 1 條：「寄測試信」把存著的郵件密碼送到操作者指定的主機 {#cm1605}

### 問題是什麼

郵件設定頁有一個「寄測試信」按鈕。使用者**不改密碼欄位**的時候，系統會從資料庫撈出**存著的那組真密碼**，配上使用者這次送過來的設定去試寄。

問題在於——**伺服器位址那一欄也是使用者填的**。

所以整件事變成：

```
使用者填一台自己控制的主機位址  →  密碼欄留空不改
                ↓
系統：「好，我用存著的真密碼去連你指定的那台機器」
                ↓
使用者在自己的機器上收到那組存著的郵件帳號密碼
```

### 外流的是誰的帳密

**寄信設定全系統只有一份、存在最上層；而「改寄信設定」這個權限下放到各個下層客戶。所以下層的管理員，可以把上層那組郵件帳密導到自己的機器上。**

外流的到底是誰的帳密，要看產品裝在哪裡：

| 產品裝在哪裡 | 最上層是誰 | 外流的是誰的帳密 |
|---|---|---|
| 我們自己營運的多租戶環境 | **我們公司** | 我們公司對外寄信的那一組 |
| 客戶自己機房的落地安裝版 | **客戶自己的總管理帳號** | 客戶自己填進去的那一組 |

**落地安裝版裝好的時候，這一頁是空的。** 出貨的初始資料只建立「改寄信設定」那四個權限與選單，一筆寄信設定的資料列都沒有——帳密全部由客戶自己填。

### 這是刻意的設計

持有「改寄信設定」這個權限的角色有 **9 個**，橫跨最上層與 7 個業務客戶；程式裡也寫明**這個權限就是要下放給業務客戶的管理員**。

**裁定**：寄信設定由客戶最上游的帳號自行管控，**要不要把這個權限下放給子單位，是客戶自己的決定，不干我們的事。**

::: {.callout .decided}
**這是同一條原則的第三次援引**

系統設定那塊、弱點檢測那塊都做過同樣的判斷：**落地版裝在客戶自己的機房，主機權限也都在他們手上，這類設定本來就該由客戶自己管。** 硬要由我們鎖住，只會讓客戶完全不能自主。

這一條是第三次援引同一條原則。
:::

### 拿到之後能做什麼

| 能做的事 | 為什麼嚴重 |
|---|---|
| **用最上層那個身分對外寄信** | 而且會**通過寄件人驗證**——收件人的信箱不會標示可疑，因為那確實是該單位真正的郵件伺服器 |
| 對客戶或合作方發釣魚信 | 信來自真實網域，可信度極高 |
| 如果設定裡關掉了加密 | 密碼是**完全明文**送過去的 |

### 為什麼評為中風險

| 判斷依據 | 這件事 |
|---|---|
| 要先有帳號嗎 | **要**，而且要有改寄信設定的權限 |
| 那個權限誰有 | **下層客戶的管理員就有**（刻意下放的，共 9 個角色） |
| 要準備什麼 | 一台自己控制得了的主機，會收郵件連線即可——**技術門檻很低** |
| 受害的是誰 | **最上層那個單位的對外信譽**——被冒用的是它的名義，而不只是資料被讀走 |
| **影響範圍多大** | **縮在單一客戶內部**——拿到的是同一家公司上層的帳密，不會跨到別家客戶去。這是評為中而不是高的原因 |

**順帶一提**：同一個程式庫裡已經有正確的做法——員工帳號目錄那支「測試連線」是**目標位址變了就不准沿用存的密碼、要求重填**。寄信這支漏了這一行。這不限制客戶任何權限，只是「你換了目標就自己重打密碼」。

---

## 第 2～4 條：其餘三條

### 第 2 條：寄信失敗時把密碼寫進系統紀錄（中）

**問題**：寄信失敗的處理程式，把**整組郵件設定**原封不動寫進系統紀錄檔——伺服器位址、寄件人、帳號、**密碼**、加密開關，整組都在裡面。

**最值得注意的是觸發條件**：這不需要攻擊者做任何事。**密碼填錯一次、伺服器連不上一次、網路抖一下**，就會寫進去。

同一支程式**成功的時候是一欄一欄印的、不含密碼，只有失敗的時候把整組設定整包丟進去**。而產品的「讀取郵件設定」功能本來就刻意把密碼遮掉了——**遮了正門，後門卻開著**。

::: {.callout .warn}
**共用地基修好的那道密碼遮蔽，蓋不到這裡**

那道遮蔽只認得一種特定的寫法（欄位名與值都用雙引號包起來的那種格式）；而這裡印出來的是另一種格式。

**實際跑過：密碼一個字都沒有被遮掉，原封不動留在紀錄裡。**

所以**不能靠「上游已經修了」就放過這條**，這一條必須單獨修。
:::

**修法**：錯誤訊息只印錯誤本身（連不上、被拒絕、逾時），不要把整組設定丟進去。

### 第 3 條：加密連線不驗對方身分（中）

**問題**：連郵件伺服器時「有加密但沒認人」。對方拿一張隨便自簽的證明過來，系統照單全收、**不會拋出任何錯誤**，下一行就把帳號密碼送過去。

**影響比第 2 條更廣**：除了洩漏郵件帳密，**每一封信的內容都會落到攔截者手上**——包含登入用的一次性驗證碼與新帳號的初始密碼信。等於帳號被接管的材料整批送出去。

::: {.callout .decided}
**成因與另外兩處不同，這個區別在稽核場合很重要**

「加密但不認人」在這個產品裡有三處，但成因分成兩種：

| 位置 | 成因 | 現況 |
|---|---|---|
| 員工帳號目錄 | 程式裡**明確寫了「關掉驗證」那一行** | ✅ 已修（設定頁有憑證欄位） |
| 快取服務 | 同上，明確寫了關閉 | ⚠️ 套件那半修了，主系統自己那支還沒 |
| **寄信（本條）** | **沒有關掉任何東西**——那行呼叫是空的、什麼都沒帶，是**程式語言內建的預設本來就不驗對方身分** | ❌ 完全沒碰 |

**不是我們關掉的，是我們沒有主動打開。** 後果一樣（帳密與每一封信的內容都會被攔走），但成因不同。

**為什麼這個區別要講清楚**：稽核方會問「為什麼你們把驗證關掉」，而正確答案是「**我們沒有關，是程式語言的預設如此、我們沒有額外加強**」。這兩句在稽核場合的份量差很多。
:::

**修法：選了加密就驗證對方身分；既有設定升級後行為不變。**

真正的問題是**設定上寫了加密、程式卻沒有兌現驗證**。客戶在設定頁選了加密，他合理相信連線是安全的——實際上半路有人假冒伺服器，系統照樣把帳密和信件交出去。**設定騙了他。**

現在的做法：

- 設定頁在加密開關下方多一個「**驗證伺服器憑證**」開關，**新建的設定預設開著**。
- 接公司內部郵件伺服器（內部憑證或自簽）的客戶，把簽發它的 CA 憑證貼進「**信任憑證**」欄位即可驗過；填了就只信這一份。
- 真的拿不到憑證的客戶可以關掉驗證，開關旁會寫明風險。
- **升級前存的設定沒有這個開關的值，一律當作「不驗」**——不這樣做，升級當天所有接自簽伺服器的客戶都會突然寄不出信。設定頁打開時頂部會提示「目前未驗證，建議開啟」，**但不會替客戶自動改掉**，要客戶自己存檔才生效。

**用內部郵件轉送的客戶不受影響**——他們選「無加密」本來就可以，內網直送很常見。

**與第 2 條合成同一張工單**（同一支檔案、同一段程式，分開改會互相踩到）。

### 第 4 條：「測試 Discord 群組」可當內網探測器（低）

**問題**：那個按鈕填什麼網址就打什麼，**系統不限制目標**。可以拿它去探測客戶內網有哪些機器、哪些服務開著。

**誰按得動**：它比「寄測試信」更嚴——**要有通知設定的權限，還要是帳號層的最高管理員**，兩個條件都滿足才按得下去。

**還有一個權限更低的變體**：把內網位址**存起來**，之後每次系統發通知都會去打它一次。

**為什麼只評為低**：回應的內容不會回傳給操作者，所以**偷不到資料，只能探測**。覆核者因此把提報的中風險降為低。

::: {.callout .warn}
**與第 1 條不是同一個病，不能共用一套修法**

- **第 1 條送出去的是系統存著的密碼**（憑證外流）。
- **第 4 條送出去的是呼叫者自己填的網址**（只是被當跳板）。

Discord 那個網址**本身就是祕密**——拿到就能冒名在那個群組發文，所以它留空時用存的那一組、呼叫端改不了目標。

兩條**可以併成一張卡做**（程式位置相鄰），**但修法不同、不可共用一套。**
:::

**修法（兩層一起做）**：

| | 做什麼 | 擋的是什麼 |
|---|---|---|
| **①** | **擋掉指向內網的位址** | 攻擊者原本辦不到的事——他在外面碰不到客戶內網，借我們的伺服器是唯一的路 |
| **②** | **只允許已知的通知服務網域**（Discord 那支就只能打 Discord） | 拿我們當跳板打外網——避免我們的位址被列入黑名單，或在別人的紀錄上留下我們的名字 |

**②比①更單純**：不用維護「哪些算內網」的清單，直接比對「是不是 Discord 的網址」就好。

**設定畫面完全不動**，客戶照樣只填一個網址，擋在系統內部、使用者看不到。

---

## 順手記下的一件小事（不是資安問題）

寄信功能的資料格式裡宣告了「副本收件人」「密件副本」「附件」三個欄位，但**實際寄信的程式完全沒有讀它們**——填了就是丟掉，**沒有任何警告或錯誤**。

**為什麼值得記**：開發者看到有「副本」欄位，合理地以為填了就會寄副本，填完測試也不會報錯，但**收件人永遠收不到**，而且沒有任何線索指向原因。在稽核的情境下，這可能造成「以為通知了某個關係人、實際上沒有」。

不是資安問題，但確實是程式錯誤。

---

## 這塊的結論

::: {.callout .warn}
**一句話**：這塊的四條發現，**三條指向同一件東西——系統存著的那組寄信帳號密碼**。一條是主動送給別人（第 1 條），兩條是被動洩漏（寫進紀錄、連線被攔）。

**第 1 條屬於客戶自己的權限治理，不是產品缺陷**：寄信設定全系統只有一份、存在最上層，改它的權限下放到各個下層客戶——這是刻意設計。要不要把這個權限下放給子單位，由客戶最上游的帳號自己決定。落地安裝版裝好時那一頁是空的，帳密由客戶自己填。

**真正要動程式的是第 2、3 條**（合成一張工單，同一支檔案）**與第 4 條**（可與第 1 條併卡做，但修法不同）。其中第 2 條特別要注意：**共用地基那道密碼遮蔽蓋不到它**，不能當作已經被上游修掉了。

**一個橫跨產品的觀察**：「加密但不認人」在這個產品裡是第三處，但**寄信這處的成因與前兩處不同**——前兩處是程式裡明確寫了關閉，這處是**沒有主動打開**。所以往後的程式審查要把「**有沒有主動要求驗證對方身分**」列為固定檢查項目；只查「有沒有人寫了關閉」會整類漏掉。
:::

| 統計 | |
|---|---|
| 檢視輪數 | 2 輪、48 個檔案 |
| 通過的發現 | **8 條**——寄信通知本身 4 條，另外 4 條是檢視時順手做密碼掃描撈到的、與這塊無關 |
| 最嚴重（🔴） | **0 條** |
| 高風險（🟠） | **1 條**——版控裡的四把對外 AI 服務金鑰（**已撤銷換發**） |
| 已修正 | **7 條**——寄信通知本身第 1（程式面）～3 條；順手撈到的那四條，外流的金鑰與密碼全部撤銷換發完畢，**剩版控殘留字串待清** |
| 待處理 | 寄信通知本身第 4 條（Discord 測試可探測內網，工單 CM-1605），加上殘留字串清理（工單 CM-1607、CM-1608） |

---

| 你想知道 | 看哪裡 |
|---|---|
| 我們用什麼方法查、結果有多可信 | [檢視方法與工具](GUIDE-01-method-and-tools.html) |
| 其他模組的檢視結果 | [回總報告首頁](index.html) |
