---
title: N2 掃描結果：通知鏈宿主接線（主專案）
---

# N2 掃描結果：通知鏈宿主接線（主專案）

> **掃描日期**：2026-09-08
> **掃描版本**：compliance-manager-be `feature/FR-075` @ `bc1c3381`（工作目錄有未 commit 變更）
> **工具**：Claude Code `claude-security` plugin（`claude-security:scan` workflow）
> **範圍**：七條路徑、**18 個受版控檔案**——`app/notification/service/`、`app/notify_config/service/`、`api/notify_config/`、`infra/notification/`、`infra/system_config/system_config_root_reader.py`、`di_containers/notification/`、`di_containers/notify_config/`
> **effort**：`low`，**focus**：`attack-surface`
> **模型**：主 session Opus 5 (1M context)，研究員繼承
> **狀態**：✅ **驗證面板完整跑完（分兩輪），stamp 為 `verification.status: verified`**——中途撞額度上限，但工具自己把不完整的那條交棒重驗，沒有拿部分票充數
> **對應卡片**：CM-1604（母卡 CM-1602）

## 一句話結論

**範圍內找到 2 條問題，核心那一條正是這一棒開來找的東西**：測試寄信端點會把平台存的 SMTP 密碼，送到**呼叫端自己指定的郵件伺服器**——業務租戶的管理員填一個自己的主機位址，就能把平台營運方的郵件憑證收下來。另外 4 條是掃描順手做的密鑰檢查撈到的**範圍外**發現（版控裡的硬編憑證），其中一半已在掃描當天處理完，另一半的密碼**至今仍有效**。

## 🔴 掃到什麼：六條一覽

**先看這張表就好**，每條的完整說明、程式碼行號與建議修法在後面各自的章節（點 ID 跳過去）。

| # | 嚴重度 | 這是什麼問題（白話） | 出事會怎樣 | 要先有什麼才打得到 | 位置 | 修正卡 |
|---|---|---|---|---|---|---|
| [F1](#f1high-測試信端點把平台-smtp-密碼送到呼叫端指定的主機) | 🟠 **HIGH** | 郵件設定頁的「寄測試信」按鈕：你不改密碼欄，系統就拿**它存的那組真密碼**去試寄；但**伺服器位址是你填的** | **子租戶的管理員拿走平台營運方的郵件帳密**。之後他能用貴公司名義寄信、且會通過 SPF 驗證，收件人看不出是偽造的。設定裡填 `tls: false` 的話密碼還是明文送過去 | 一個持有 `smtp-config.update` 能力點的登入帳號（**這個能力點是刻意下放給業務租戶管理員的**），加上一台他控制得了的 SMTP 監聽 | `test_mail_service.py:84`（回填密碼）<br>`test_mail_service.py:116`（拿去連他指定的主機） | CM-1605 |
| [F5](#f5low-discord-頻道測試的盲打-ssrf) | ⚪ LOW | 「測試 Discord 頻道」按鈕：填什麼網址就打什麼，沒有任何位址限制。**還有一條更低權限的變體**——把內網網址存起來，之後每次系統發通知都會去打它 | 拿我們的伺服器**當跳板探測客戶內網**：哪台機器活著、哪個埠開著、雲端 metadata 端點（`169.254.169.254`）打不打得到。回應內容不會回給呼叫端，所以偷不到資料，只能探測與觸發副作用——**面板因此把研究員報的 MEDIUM 降成 LOW** | 測試路徑要 `notify_config.update` ＋ 帳號層 super admin；**存起來的變體只要 `notify_config.update`** | `notify_config_test_service.py:86`（測試）<br>`notification_service.py:90`（存起來後每次發通知都打） | CM-1605（併）※ |
| [F2](#f2high範圍外-envtest-內的四把對外-api-金鑰已處理) | 🟠 HIGH<br>**⚠️ 範圍外** | 一個叫 `.env.test` 的檔案被刻意排除在忽略清單外、推上了 origin，裡面是四把真的對外 API 金鑰（不是範本值） | **直接的財務損失**——任何拿到 repo 的人都能用貴公司帳號呼叫這些外部 AI 服務；還能讀走服務端存的執行軌跡，裡面例行含有應用提示詞與客戶資料 | 只要讀得到 repo（包含 clone、鏡像、CI 快取），**不需任何網路位置** | `.env.test` 四行 | ✅ **已處理** |
| [F3](#f3medium範圍外-同檔的-jwt-密鑰與服務密碼已處理) | 🟡 MEDIUM<br>**⚠️ 範圍外** | 同一個檔案裡還有 JWT 簽章密鑰與 DB／Redis／MinIO 的密碼 | 拿到 JWT 密鑰就能**自己偽造一張管理員身分的登入憑證**（不需要任何網路位置）；DB 密碼則能直接連進資料庫繞過所有應用層授權 | 讀得到 repo；DB／Redis／MinIO 部分另需內網或 VPN 連得到 | `.env.test` 四行 | ✅ **已處理** |
| [F4](#f4medium範圍外-poc-資料庫密碼硬編在文件腳本裡未處理) | 🟡 MEDIUM<br>**⚠️ 範圍外** | 一支產生資料庫結構文件的腳本，把 **POC 環境**的完整連線資訊（含密碼）寫死在程式碼裡 | **直接連進客戶會看到的 POC 資料庫**——專案自己的 CLAUDE.md 就寫明 POC 等同 production。讀得到、視 RLS 而定可能改得到客戶的展示資料 | 讀得到 repo ＋ 連得到那台機器（內網或 VPN） | `docs/system-design/scripts/generate_db_schema_docx.py:34` | CM-1608 |
| [F8](#f8medium範圍外-blsadmin-管理員密碼硬編在三支腳本未處理) | 🟡 MEDIUM<br>**⚠️ 範圍外** | `blsadmin` 這個**系統管理員帳號的密碼**寫死在三支腳本裡。其中兩支寫成「環境變數沒設就用這個」——**而那個「這個」就是真密碼**，變數沒設時靜默使用 | 拿到 repo 就等於拿到一組**可用的管理員帳密**，登入任何還在用這組密碼的環境即取得管理員 session——而管理員正好有權改 SMTP 設定，**可以直接接上 F1 那條鏈** | 讀得到 repo ＋ 連得到某個該帳號仍用這組密碼的環境 | `scripts/e2e_test_module_frame_with_docx.py:38`（無逃生門）<br>`scripts/migrate_2026-08-09_fr062_existing_tenant_licenses.py:80`<br>`scripts/seed_2026-08-01_fr059_detection_profiles.py:65` | CM-1608 |

※ F5 併入 CM-1605：同屬通知設定的「使用者可控目標位址」問題族，改動相鄰。

**另有一項首腦查證的延伸發現**（工具沒報）：F2 那批已撤銷金鑰的字串**還散在版控裡 15 個檔**，見[下方專節](#f2-的延伸首腦查證已撤銷金鑰仍散在版控-15-個檔)。對應卡 **CM-1607**。

### ⚠️ 這份結果的可信度

**「這六條存在嗎」與「只有這六條嗎」要分開回答。**

**存在：可信。** 六條全部經過完整的三人對抗式面板（reachability／impact／defenses 各一票），stamp 記 `verification.status: verified`，`unreviewed_candidate_sites: 0`——**沒有任何一條是靠部分票或無票通過的**。其中五條 3/3 全票；F1 是 2/3，但**那張反對票首腦判定不成立**（理由見 F1 章節），且首腦已親自開檔核對 F1 的行號與守門形狀屬實。

**「只有這六條」：不可信，而且有兩層不可信。**

第一層是通例：`low` effort 單次快篩，沒有 inventory、沒有威脅模型、沒有廣度掃蕩，工具自己把 `completenessCheckOutcome` 標成 `not-applicable`。

第二層是這一棒特有的：**四條範圍外發現不代表那些目錄被掃過。** F2／F3／F4／F8 是研究員為了追脈絡讀到範圍外時，順帶由密鑰專項檢查撈出來的。**`.env.test`、`docs/`、`scripts/` 三棵樹從來沒有被當成掃描目標。** 這四條的存在不能讀成「那三處只有這些問題」——那三處等於沒掃過。

同樣地，掃描過程**沒有執行任何被掃的程式碼**——沒跑測試、沒發攻擊、沒拿任何憑證去連任何服務。

## 執行概況

| 項目 | 數字 |
|---|---|
| 研究員派出 / 回報 | 2 / 2 |
| 候選發現 | 9 條原始 → 去重後 **7 條** |
| 面板票數 | **23** |
| 面板審過的發現 | 6（`panel_reviewed_findings: 6`，`panel_quorum_findings: 6`）|
| 未投票候選 | **0**（`unreviewed_candidate_sites: 0`）|
| **驗證輪次** | **2**（第一輪撞額度中斷，第二輪續完）|
| 總耗時 | 約 3 小時 58 分（14,298 秒）|

---

## 🔴 中斷與續跑：工具自己把不完整的面板接住了

**這是本棒最值得記下來的一件事**，也是與 FR-077 R1 最關鍵的差別。

### 發生了什麼

第一輪面板驗到候選 `C6` 時撞上 session 額度上限。那一條的三個角度驗證者**回來了兩個，第三個初次派發加四次重試全部失敗**。

**工具沒有拿兩票充數。** 它把這件事記進 `adversarialCasualties`：

```
F6: panel incomplete (2/3 voters returned), handed to the next verification run
```

然後把該候選**交棒給下一輪**，而不是把它算成「已驗證」或「未驗證」。額度重置後跑第二輪，用**完整的三人面板**重新驗那一條——就是報告裡的 **F8**（blsadmin 密碼）。

`lostCandidates` 是空的，最終狀態零待決候選。**這份報告裡沒有任何一條是靠不完整的面板通過的。**

### 票數為什麼是 23 不是 21（自洽性檢查）

7 個候選 × 3 票 = 21，實際 23 票。多出的 2 票，**正是第一輪那個候選作廢掉的那兩張部分票**。

換句話說：**續跑不是「接續舊票再補一張」，是整條重投。** 這是正確的做法——三人面板的價值在於三個角度互相制衡，補一張票進去等於讓那一票在已知另外兩票結果的情況下投，性質完全不同。

### 為什麼六條發現卻編號到 F8（跳號說明）

7 個去重候選中有 **1 個被面板全票否決**（0 真 / 3 假），不會出現在報告裡。F6／F7 的跳號，就是那個否決加上續驗候選重新編號的結果。

### 🔴 與 FR-077 R1 的差別：兩者不可混為一談

兩棒都「撞到額度」，但發生的事完全不同：

| | FR-077 R1 | **本棒 N2** |
|---|---|---|
| 面板 | **全滅**——105 個 verifier 全失敗，21 票 0 張投出 | **完整跑完**——23 票全部投出 |
| 工具正式 findings | **空陣列** | 6 條 |
| stamp | `verification.status: unverified` | **`verified`** |
| 候選怎麼處理 | 全部標 `continued: true`，工具無法收束 | 1 條交棒續驗完成，其餘正常結束 |
| 結論怎麼來 | **人工從 workflow 回傳撈回來，runner 逐條開檔核對補上驗證** | **工具自己完成**，首腦只做抽查複核 |

**R1 是工具失敗、人接手；N2 是工具中斷、工具自己接住。** 描述本棒時不可套用 R1 的語言（「面板沒跑成」「結論未經背書」），那會嚴重低估這份結果的可信度。

---

## 範圍內的兩條

## F1（HIGH）— 測試信端點把平台 SMTP 密碼送到呼叫端指定的主機

**嚴重度**：HIGH · **面板**：2/3（一票反對，**首腦判定該反對理由不成立**）· **confidence**：medium · **CWE-522**
**位置**：`app/notification/service/test_mail_service.py:84`（回填既存密碼）→ `:116`（拿著它連呼叫端指定的 host），位於 `TestMailService.send_test_mail`
**首腦已開檔核對**：✅ 屬實

### 問題在說什麼（白話）

郵件設定頁上有個「寄測試信」按鈕。使用者的操作直覺是：「我不想重打密碼，就讓它用存好的那組試一下。」系統於是照辦——**從 DB 撈出既存的真密碼，貼進這次送來的設定裡**。

問題在於：**這次送來的設定裡，郵件伺服器位址也是使用者填的。**

於是完整的攻擊路徑只有一步：填一個自己控制的主機位址、密碼欄不改、按下測試。系統就會拿著平台**真正的**郵件帳號密碼，去登入攻擊者的伺服器——攻擊者的監聽程式只要聲稱支援 AUTH PLAIN，就把帳密收下了。設定裡填 `tls: false` 的話，連加密都省了，密碼是明文過去的。

### 這條為什麼是 HIGH（而 N1 的同族問題只是 MEDIUM）

N1 F1 是「密碼掉進 log，攻擊者要有辦法讀到 log」。**這一條是密碼被主動送到攻擊者指定的機器上**——不需要等它掉進哪裡，直接收貨。

而洩漏的東西的歸屬更關鍵：**SMTP 設定存在 ROOT 租戶、整個平台共用一份，但「改 SMTP 設定」這個能力點是刻意下放給業務租戶管理員的。** 所以場景是：**子租戶的管理員拿走平台營運方的郵件憑證。** 拿到之後他能用該組織的名義寄信，而且因為用的是真正的郵件伺服器，**SPF 驗證會通過**——收件人從技術指標上分辨不出是偽造的。

### 技術細節

```
POST /api/1.0/mail/test
  body 內的 smtp_config（host / port / user / tls）  ← 完全由呼叫端提供，未經驗證
  changePwd = false（預設）
    → test_mail_service.py:84   把 ROOT 租戶存的真密碼寫進這份設定
    → test_mail_service.py:116  用這份設定連線並 server.login()
```

**route 的守門只有 `@auth_required` ＋ `@capability_required`**，兩者都只回答「你是誰」，不回答「你送來的主機能不能連」。而 schema `SendMailTestRequest` 掛在 `mail_route.py:28` 時帶的是 **`apply=False`**——**等於只是文件，完全沒有做驗證**，而且它宣告的欄位只有 `to`，`smtp_config` 整包從來沒被任何 schema 檢查過。

### 🔴 那張反對票：首腦判定不成立

面板是 2/3 通過，唯一的反對票來自 reachability 角度。**必須把這件事寫清楚，因為那張票的理由恰好把因果關係搞反了。**

反對者**不否認漏洞存在**，也不否認缺少驗證。他的理由是：`mail_route.py:31` 有 `@capability_required` 守門，這縮小了可觸及的範圍。

**首腦判定不成立**，理由是：`smtp-config.update` 這個能力點**正是刻意下放給業務租戶管理員的**——`infra/system_config/system_config_root_reader.py` 內 CM-1331 的說明白紙黑字寫著這個設計意圖。

所以持有這個能力點的人，**本來就包含攻擊者情境裡的那個人**。守門的存在不是防護，**守門存在恰恰是攻擊成立的前提**——正因為子租戶管理員被授權改 SMTP 設定，「子租戶管理員拿走平台營運方憑證」這個場景才有意義。把它當成緩解因素，等於把威脅模型讀反了。

> **這也是為什麼 confidence 標 medium 而嚴重度仍是 HIGH**：medium 反映的是「面板未達一致」這個程序事實，不反映首腦對實質的判斷。首腦已開檔核對 `test_mail_service.py:60-117` 與 `jedi-notification/.../mail_route.py:20-41`，守門形狀與 `apply=False` 皆與報告逐字相符。

### 同款問題 LDAP 已經修過

**這是本 codebase 第二次踩到「測試連線時回填既存密碼、但目標位址可控」。** LDAP 那條已在 **CM-1564** 修掉，手法是**位址改變時強制要求重新輸入密碼**。SMTP 這邊沒有跟著修。

### 建議修法

1. **對齊 CM-1564（最小改動）**：`change_pwd` 為 false 時，只有在「送來的連線目標與存的那組完全一致」（host／port／username／tls 四項全等）才回填存的密碼；否則要求呼叫端自己填密碼。
2. **更根本**：**根本不要把「存的密碼」和「呼叫端指定的目的地」配在一起**。整份設定從 DB 讀出來、伺服器端組好，request 只能決定「寄給誰」這一個欄位。
3. **一併修**：把 `SendMailTestRequest` 的 `apply=False` 改成實際套用，並補上 `smtp_config` 的欄位定義——現在它連基本的型別檢查都沒有。

---

## F5（LOW）— Discord 頻道測試的盲打 SSRF

**嚴重度**：LOW（**面板從研究員報的 MEDIUM 下修**，投票為 LOW／LOW／MEDIUM）· **面板**：3/3 全票確認存在 · **confidence**：medium · **CWE-918**
**位置**：`app/notify_config/service/notify_config_test_service.py:86`，位於 `NotifyConfigTestService._test_discord`

### 問題在說什麼（白話）

「測試 Discord 頻道」按鈕會拿使用者填的 webhook 網址去發一則訊息。程式**逐字照用那個網址**——沒有檢查它是不是真的 Discord、沒有限制通訊協定、沒有限制主機、沒有限制轉址。

所以使用者可以填 `http://10.0.0.15:8500/...` 這種內網位址，**我們的伺服器就替他去打**。伺服器位在客戶的網路裡面，能連到很多外面連不到的東西——內部管理介面、雲端 metadata 端點（`169.254.169.254`）、localhost 上的管理埠。

### 為什麼只是 LOW

**回應內容不會回給呼叫端。** 呼叫端只看得到「成功」或 `NOTIFY_CHANNEL_TEST_FAILED`，也就是「對方回 204 沒有」這一個位元，加上回應快慢。**這叫盲打（blind）SSRF**——可以拿來**探測**「哪台機器活著、哪個埠開著」並**觸發**有副作用的端點，但**偷不走資料**。面板正是據此把研究員報的 MEDIUM 下修為 LOW。

### 🔴 還有一條更低權限的變體（別只修測試端點）

測試路徑要 `notify_config.update` ＋ 帳號層 super admin。但**存起來的那條路徑只要 `notify_config.update`**：

```
PUT NOTIFY_CONFIG/DISCORD   把內網 URL 存成 secret，enabled=true
   → 之後每一次工作流事件或檢測完成要發通知時
   → app/notification/service/notification_service.py:90 就會去打那個位址
```

**這條的權限門檻更低，而且是自動反覆觸發的**（每次通知都打一次），不需要攻擊者手動按測試。**只修測試端點等於沒修。**

### 建議修法

在使用前驗證 webhook URL，且**寫入路徑與測試路徑都要套**：

1. 強制 https，主機必須落在白名單網域（`discord.com` / `discordapp.com`）
2. DNS 解析後拒絕非公網位址（RFC1918 私有網段、loopback、link-local、IPv6 ULA）
3. 對外請求關閉自動轉址（否則白名單可被一次 302 繞過）

---

## 範圍外的四條（密鑰專項檢查撈到的）

> **這四條不在本棒的 18 個目標檔案內。** 研究員為了確認可達性讀到範圍外，密鑰專項檢查順帶撈出來的。**工具自己標了 out of scope。** 它們是真的、值得處理，但**它們的存在不構成「那幾棵樹被掃過」**——`.env.test`、`docs/`、`scripts/` 從未被當成掃描目標。

## F2（HIGH，範圍外）— `.env.test` 內的四把對外 API 金鑰【已處理】

**面板**：3/3 全票 · **位置**：`.env.test` 四行

**是什麼**：`.gitignore` 排除了 `.env.*`，卻又特地寫了一行 `!.env.test` 把它放回來，理由註明「這是範本、沒有真憑證」。**實際上不是**——裡面是四把完整長度、符合各家格式的對外 AI 服務 API 金鑰。檔案在 HEAD 受版控，且已存在於 `origin/main` 與另外十一個推上去的 branch。

**出事會怎樣**：任何拿到 repo 的人（承包商、外流的 CI 快取、鏡像）都能用貴公司帳號呼叫這些服務——**直接的財務損失**；還能讀走服務端存的執行軌跡，那裡面例行含有應用提示詞與客戶資料。**不需要任何網路位置，讀得到 repo 就是全部的門檻。**

### ✅ 處置狀態：已完成（2026-09-08）

**決策者於掃描當天將四把金鑰全數撤銷並重新簽發**，檔案已撤出版控（commit `5746cef1` / `56c1dc43`）。

**財務與資料風險已解除**——即使舊字串仍散落在歷史紀錄裡，那些金鑰已經不能用了。

---

## F3（MEDIUM，範圍外）— 同檔的 JWT 密鑰與服務密碼【已處理】

**面板**：3/3 全票 · **位置**：`.env.test` 四行

**是什麼**：同一個 `.env.test` 裡還釘死了 JWT 簽章密鑰，以及資料庫、Redis、MinIO 的密碼（都指向具名的內部主機）。

**出事會怎樣**：**JWT 密鑰最危險——拿到就能自己偽造一張「我是管理員」的登入憑證**，這條完全不需要網路位置，只要打得到目標 API 就行。DB 密碼則能直接 psql 進去讀租戶資料，繞過所有應用層授權；註解掉的那一行還是 `cmmgr`，那是**繞過 RLS 的系統管理員帳號**。

**同一組密碼也出現在 F4 的腳本裡**，代表密碼被跨環境重複使用——一處外洩、多處受害。

### ✅ 處置狀態：已完成（2026-09-08）

**與 F2 同批處理**：值已輪替、檔案已撤出版控（commit `5746cef1` / `56c1dc43`）。

---

## F4（MEDIUM，範圍外）— POC 資料庫密碼硬編在文件腳本裡【未處理】

**面板**：3/3 全票 · **位置**：`docs/system-design/scripts/generate_db_schema_docx.py:34`

**是什麼**：一支產生資料庫結構文件的腳本，把完整的連線設定（主機、埠、資料庫名、`cm_app` 密碼）直接寫死在程式碼裡，指向 **POC 環境**。

**出事會怎樣**：讀得到 repo 又連得到那台機器的人，複製那個連線設定就能直接 `psql` 進 POC 資料庫——**專案自己的 CLAUDE.md 就寫明「POC 等同 production」**（對外 demo、客戶試玩）。讀得到客戶的展示資料，視 RLS 而定可能也改得到。

### 🔴 處置狀態：未處理，**密碼仍然有效**

**建議修法**：改從環境變數讀（`config/config.py` 早就有 `DB_HOST` / `DB_USER` / `DB_PASSWORD`），輪替該密碼，並清掉 git 歷史裡的字面值。

**對應卡：CM-1608**（與 F8 併卡——同一類「腳本內硬編密碼」，且需要同一批輪替動作）。

---

## F8（MEDIUM，範圍外）— blsadmin 管理員密碼硬編在三支腳本【未處理】

**面板**：3/3 全票（**於第二輪完整面板驗出**，即被額度中斷後交棒重驗的那一條）
**位置**：
- `scripts/e2e_test_module_frame_with_docx.py:38` — **沒有環境變數逃生門**，寫死就是寫死
- `scripts/migrate_2026-08-09_fr062_existing_tenant_licenses.py:80`
- `scripts/seed_2026-08-01_fr059_detection_profiles.py:65`

**是什麼**：`blsadmin` 這個系統管理員帳號的登入密碼，以字面值出現在三支腳本裡。

**後兩支特別要注意寫法**：它們是 `os.getenv("...", "<真密碼>")`——**環境變數沒設時，default 就是真密碼，而且會靜默使用**。這種寫法看起來像「有做環境變數化」，實際上在最常見的情境（沒人記得設那個變數）下，用的就是硬編的那組。

**出事會怎樣**：拿到 repo 就等於拿到一組**可用的管理員帳密**。拿去登入任何還在用這組密碼的環境（很可能包含 DEV，也可能包含用同一套 runbook 佈建的其他環境），立刻取得管理員 session。

**🔴 而管理員正好有權改 SMTP 設定——這條可以直接接上 F1。** 兩條合起來是完整的憑證竊取鏈：拿 repo → 用硬編密碼登入 → 用 F1 的測試信端點把平台 SMTP 憑證導到自己機器。

### 🔴 處置狀態：未處理，**密碼仍然有效**

**建議修法**：三支腳本全部拔掉字面值，改成**沒有 default 的環境變數**（沒設就報錯退出，不要靜默 fallback）；在所有仍在使用該密碼的環境上輪替 `blsadmin`。

**對應卡：CM-1608**（與 F4 併卡）。

---

## F2 的延伸：首腦查證，已撤銷金鑰仍散在版控 15 個檔

**這是工具沒報、首腦另外查出來的**，範圍比 F2 描述的更廣。

F2 那批金鑰的**字串本身**，除了 `.env.test` 之外，還出現在版控裡的 **15 個檔案**（其中 13 個受版控）：

| 位置 | 檔數 | 說明 |
|---|---|---|
| `docs/conversation-history/` | 13 | 對話紀錄原始 dump，當時把含金鑰的檔案內容整段帶進去了 |
| `docs/security-reports/2026-07-25/trivy-backend.json`<br>`docs/security-reports/2026-07-25/trivy-backend-final.json` | 2 | 諷刺的是——**這是資安掃描報告本身把撈到的憑證原文記了下來** |

其中 **9 個檔**含的是與 `.env.test` 完全同一把。

### 這件事現在有多嚴重

**沒有財務風險。** 那些金鑰在 2026-09-08 已被決策者全數撤銷，字串留著也叫不動任何服務。

**但它會污染未來每一次掃描。** 每一輪資安掃描的密鑰檢查都會再撈出這 15 個檔，每一次都要有人重新判斷「這是不是真的」——而判斷成本不低（要去查那把金鑰是否已撤銷）。**留著等於每次掃描都製造一批必須人工排除的假陽性**，久了就會養出「掃到憑證先當假的」的壞習慣，真正的那次就會被漏掉。

`trivy-backend*.json` 那兩個檔還有個額外的教訓：**掃描工具的產出本身可能含憑證原文，歸檔前要過濾。**

**對應卡：CM-1607。**

---

## 首腦已確認、本次未觸及的部分

母卡「首腦讀碼後的重點面」列了四個宿主側方向，本次實質觸及兩個：

| 母卡重點 | 本次狀態 |
|---|---|
| ⑧ 測試寄信把既存 SMTP 密碼送到使用者指定的伺服器 | ✅ **確認為問題**（F1）——正是這一棒開來找的東西 |
| ⑨ Discord / Telegram 的同款回填模式 | **部分**——Discord 從 SSRF 角度掃到了（F5），但「回填的 secret 本身會不會外洩」這個角度（webhook URL 本身即機密、Telegram token 拿到即可冒充 bot）**未觸及** |
| ⑩ 背景 thread 寄信的租戶設定讀取是否 fail closed | **未觸及**——無結論。多處在背景 thread 呼叫寄信、無 user context 時實際走哪條讀取路徑，本次沒有答案 |
| ⑪ 繞 RLS 的設定直讀（`system_config_root_reader.py`）的呼叫點盤點 | **未觸及**——該檔在掃描範圍內，但沒有系統性的呼叫點盤點結論 |

**⑨⑩⑪ 三條的空白必須讀作「沒有結論」，不是「沒有問題」。** ⑩ 尤其值得追——「A 租戶的信用 B 租戶的郵件伺服器寄出去」屬跨租戶洩漏，嚴重度天然不低，而它現在是完全空白的。

## 附件

- 工具產物在 BE repo 的 `CLAUDE-SECURITY-*` 執行目錄（自帶單行 `*` 的 `.gitignore`，不入版控）
- stamp：`verification.status: verified`，`findings.total: 6`（high 2 / medium 3 / low 1），`verification_runs: 2`，`unreviewed_candidate_sites: 0`，`lostCandidates` 空
