---
title: A07 身分驗證失效
eyebrow: Guidant AI 資安檢視總報告 · OWASP Top 10（2025）
h1: A07 身分驗證失效（Authentication Failures）
lede: 命中這一類的 **24 件**，還原成 **29 條原始條目**（28 條出自模組頁、1 條只在總表）。每一條用白話寫出：在哪裡、攻擊面、信任邊界、元件端點、駭客怎麼打、得手什麼、修正的做法。
chips:
  - { text: "🔴 2", kind: crit }
  - { text: "🟠 12", kind: warn }
  - { text: "🟡 11", kind: accent }
  - { text: "⚪ 4", kind: plain }
  - { text: "已修 26／不修 3", kind: ok }
---

## 這一類是什麼

**系統被騙，把不合法的人當成合法使用者**。OWASP Top 10:2025 對這一類的定義涵蓋暴力猜測、弱密碼或預設密碼、缺多因子驗證；判定依據包括驗證不當、關鍵功能缺驗證、不限次數嘗試、**寫死的帳密**、預設帳密、**加密連線卻不驗對方憑證**、密碼重設流程弱（定義見[兩套分類是什麼](../SUMMARY-owasp-stride.html#owasp-def)）。

在本專案，29 條分成四個形狀：

- **驗證流程本身有洞（9 條）**：代理程式通道不問身分、忘記密碼直接交出重設鑰匙、公司帳號登入可匿名通過、綁外部帳號不確認本人、Google 登入是空殼、雙因子驗證碼可無限猜、授權序號太短可猜、子公司管理員能改全公司的登入規則、系統產生的密碼不合自家規定。這一組是「門本身沒關好」，兩件最嚴重的問題都在這裡。
- **帳密金鑰寫進版控（14 條）**：資料庫管理員密碼、系統管理員密碼、AI 服務金鑰、程式碼平台通行證、套件倉庫帳密、雲端硬碟密鑰、登入憑證簽章金鑰，散在對話紀錄、交接文件、腳本裡。拿得到程式庫的人（含離職者、外包）不必破解任何東西，照著抄就能登入。修法一律三步：**先堵住再寫進去的路 → 清掉已寫進去的 → 換發**。
- **存著的憑證被送到不該去的地方（5 條）**：連郵件伺服器、GitHub、快取伺服器時有加密但不確認對方是誰；「寄測試信」與「AI 服務設定」可以把存著的密碼或原廠金鑰導到操作者指定的主機。
- **預設帳密（1 條）**：每套安裝都種下同一組原廠最高權限帳號，裁定為刻意設計。

> **條目編號**：標題用模組頁編號（例如 M13-1 是登入與權限模組頁第 1 條），括號內是[總結](../SUMMARY.html)問題總表編號。總表把「同一組密碼、同一套修法」併成一件（例如資料庫管理員密碼在三個模組頁各記一條，總表併成 #12），本頁拆回模組頁原始條目，所以是 29 條不是 24 件。總表 #137 只出現在總表、沒有模組頁逐條表，標題改用總表編號。嚴重度一律用總表燈號，與模組頁可能不同。

## 條目清單

| # | 條目 | 嚴重度 | 出自 | 狀態 |
|---:|---|---|---|---|
| 1 | [M01-1 代理程式五條通訊管道全部不檢查對方是誰](#m01-1) | 🔴 最嚴重 | [遠端代理程式](../M01-remote-agent.html) | ✅ 已修 |
| 2 | [M13-1 忘記密碼把重設鑰匙直接回傳給任何人](#m13-1) | 🔴 最嚴重 | [登入與權限](../M13-iam.html) | ✅ 已修 |
| 3 | [M15-5 程式碼平台通行證四把寫在版控文件裡](#m15-5) | 🟠 高 | [AI 儀表板](../M15-ai-dashboard.html) | ✅ 已修 |
| 4 | [M16-5 測試設定檔推上版控，帶著四把真的 AI 服務金鑰](#m16-5) | 🟠 高 | [通知](../M16-notification.html) | ✅ 已修 |
| 5 | [M17-9 四家 AI 服務金鑰躺在九個對話紀錄檔裡](#m17-9) | 🟠 高 | [公告](../M17-bulletin.html) | ✅ 已修 |
| 6 | [M23-1 資料庫管理員密碼散在 249 個檔案裡](#m23-1) | 🟠 高 | [意見回饋](../M23-issue.html) | ✅ 已修 |
| 7 | [M16-7 產生文件的腳本寫死展示環境資料庫連線與密碼](#m16-7) | 🟠 高 | [通知](../M16-notification.html) | ✅ 已修 |
| 8 | [M17-8 可繞過客戶隔離的資料庫管理帳號密碼寫在文件裡](#m17-8) | 🟠 高 | [公告](../M17-bulletin.html) | ✅ 已修 |
| 9 | [M22-1 子公司管理員能改全公司的登入規則與帳號目錄](#m22-1) | 🟠 高 | [系統設定](../M22-system-core.html) | ✅ 已修 |
| 10 | [M13-2 公司帳號登入可匿名通過、可注入、加密不認人](#m13-2) | 🟠 高 | [登入與權限](../M13-iam.html) | ✅ 已修 |
| 11 | [M13-3 綁定外部帳號不確認是本人](#m13-3) | 🟠 高 | [登入與權限](../M13-iam.html) | ✅ 已修 |
| 12 | [M13-5 用 Google 登入是沒接通的空殼](#m13-5) | 🟠 高 | [登入與權限](../M13-iam.html) | ✅ 已修 |
| 13 | [M13-6 雙因子驗證碼可以無限次猜](#m13-6) | 🟠 高 | [登入與權限](../M13-iam.html) | ✅ 已修 |
| 14 | [M18-3 開通序號只有 8 個字，猜得到別人的授權](#m18-3) | 🟠 高 | [授權管理](../M18-license.html) | ✅ 已修 |
| 15 | [M16-8 系統管理員密碼寫死在三支腳本裡](#m16-8) | 🟡 中 | [通知](../M16-notification.html) | ✅ 已修 |
| 16 | [M17-11 資料庫調整腳本「沒設變數就用真密碼」](#m17-11) | 🟡 中 | [公告](../M17-bulletin.html) | ✅ 已修 |
| 17 | [M17-10 套件倉庫與檔案儲存帳密寫在交接文件裡](#m17-10) | 🟡 中 | [公告](../M17-bulletin.html) | ✅ 已修 |
| 18 | [M15-6 套件倉庫管理員帳密＋儲存金鑰＋登入帳密同在一份交接文件](#m15-6) | 🟡 中 | [AI 儀表板](../M15-ai-dashboard.html) | ✅ 已修 |
| 19 | [M23-3 Google 雲端硬碟密鑰與加密金鑰散在 15 個檔案裡](#m23-3) | 🟡 中 | [意見回饋](../M23-issue.html) | ✅ 已修 |
| 20 | [M23-6 打包前端映像檔的腳本寫著套件倉庫帳密](#m23-6) | 🟡 中 | [意見回饋](../M23-issue.html) | ✅ 已修 |
| 21 | [M16-3 連郵件伺服器有加密但不認人](#m16-3) | 🟡 中 | [通知](../M16-notification.html) | ✅ 已修 |
| 22 | [M17-7 每套安裝都種下同一組最高權限帳號](#m17-7) | 🟡 中 | [公告](../M17-bulletin.html) | 🚫 裁定不修 |
| 23 | [M23-4 帶著 GitHub 通行證連線卻不確認對方是真的 GitHub](#m23-4) | 🟡 中 | [意見回饋](../M23-issue.html) | ✅ 已修 |
| 24 | [M16-1 寄測試信把存著的郵件密碼送到操作者指定的主機](#m16-1) | 🟡 中 | [通知](../M16-notification.html) | 🚫 裁定不修 |
| 25 | [M07-13 開關一放開，原廠 AI 金鑰就送到客戶指定的伺服器](#m07-13) | 🟡 中 | [證據自動分類](../M07-evidence-classification.html) | ✅ 已修 |
| 26 | [M16-6 登入憑證簽章金鑰、資料庫密碼、儲存金鑰同在一個測試設定檔](#m16-6) | ⚪ 低 | [通知](../M16-notification.html) | ✅ 已修 |
| 27 | [M17-6 登入憑證簽章金鑰明文躺在對話紀錄裡](#m17-6) | ⚪ 低 | [公告](../M17-bulletin.html) | ✅ 已修 |
| 28 | [總表#137 系統自動產生的密碼比規定少一個字元](#summary-137) | ⚪ 低 | [總表](../SUMMARY.html) | ✅ 已修 |
| 29 | [M04-6 主系統連快取伺服器時不確認對方是誰](#m04-6) | ⚪ 低 | [共用地基](../M04-common.html) | 🚫 裁定不修 |

---

## M01-1 🔴 代理程式五條通訊管道全部不檢查對方是誰 {#m01-1}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🔴 最嚴重（總表 #1；STRIDE：冒充身分、資料外洩、竄改資料、權限提升） |
| **在哪裡** | 遠端代理程式模組：裝在客戶機房的代理程式與後端之間的五條通道——報到、領工作、確認收到、回報結果、下載證據檔 |
| **攻擊面位置** | 後端對外開放的「代理程式控制面」API。這一面設計上就**不帶使用者登入**（代理程式是機器、不是人），所以任何連得到後端的人都打得到，不需要帳號 |
| **信任邊界位置** | **客戶機房 ↔ 我們後端**。應該在後端入口用「簽過章的身分憑證」確認對方是哪一台；當時外層的雙向憑證已退役，後端**讀請求裡自報的機器編號就相信**，等於邊界沒有守衛 |
| **元件端點位置** | 套件 `jedi_remote_agent/api/routing.py`：`POST /agents/register`（報到）、`POST /agents/heartbeat`（領工作）、`POST /agents/tasks/{uid}/ack`（確認收到）、`POST /agents/tasks/{uid}/result`（回報結果）；主專案 `core/plugins/remote_agent.py` 掛的 `GET /agents/files/{uid}`（`api/remote_agent/routes/agent_file_route.py:AgentFileDownloadRoute`，下載證據檔）。處理在 `app/service/agent_enrollment_service.py`、`agent_task_service.py`、`agent_control_auth_service.py` |
| **駭客怎麼打** | ① 在任何連得到後端的位置，不需要帳號；② 照代理程式平常的請求格式，發一個「我是代理程式 X，有工作嗎」；③ 後端不驗證「你真的是 X」，直接回一整套稽核用的高權限鑰匙與主機帳密；④ 同一招打回報結果通道，可以替 X 回報「掃描全部正常」或抹掉結果，打下載通道則拿走客戶上傳來掃的檔案；⑤ 管理員在畫面按「撤銷」只斷了一部分通道，其他照用 |
| **得手什麼** | 客戶整套主機的明文帳密、冒充任一家客戶的機器、竄改或抹除稽核證據 |
| **修正的做法** | ① **報到時多發一張「控制面通行證」**：用後端既有的代理程式簽章私鑰簽，內含機器編號、客戶編號、用途標記（讓同一把鑰匙簽的其他短效票不能拿來冒充）、效期；② **其餘四條通道每個請求都要帶這張證**（`agent_pass_required` 守門）：驗簽 → 只認證裡的編號 → **每個請求都查一次資料庫確認這台沒被撤銷、沒被停用**，身分與客戶歸屬從證裡推、不再讀請求內容；主專案那支下載路由也改掛同一支守門、拿掉自報編號的標頭；③ **路由表改成三級守門**（管理員／代理程式／報到），認不得的等級在啟動時就報錯，不會有端點靜默變成不設防；④ **確認收到／回報結果**：交易內再驗一次撤銷狀態，並比對「這張單子是不是派給這台」，不符回「找不到」；⑤ **領工作**：拿掉「沒帶編號就用指紋跨客戶撈第一筆」的退路；⑥ **報到**：被撤銷的列一律拒絕、不再洗回正常，帶舊編號重新登記必須附「用舊私鑰簽這次請求」的持有證明；驗收時又補一道：簽憑證前檢查申請書上的機器名稱必須等於本次報到的機器，堵住「拿自己申請的真憑證去冒認別台」 |
| **狀態** | ✅ 已修（CM-2052，套件 commit `c4acc687`＋`9c049540`，主專案 commit `7a3bdf1ec`）。驗證：套件 150 測試，DEV 實跑 12 項（撤銷後四條通道與下載全 403、只帶自報編號 401） |

## M13-1 🔴 忘記密碼把重設鑰匙直接回傳給任何人 {#m13-1}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🔴 最嚴重（總表 #2；STRIDE：冒充身分、權限提升） |
| **在哪裡** | 登入與權限模組：登入頁的「忘記密碼」功能 |
| **攻擊面位置** | **不需要登入**就能打的公開 API（忘記密碼本來就是給還沒登入的人用的），任何人在登入頁就構成攻擊面 |
| **信任邊界位置** | **匿名網路 ↔ 身分系統**。忘記密碼的設計是「系統把重設鑰匙寄到本人信箱」，鑰匙只該走信箱這條路；當時 API 回應直接把鑰匙交給了按下送出的人，等於邊界的另一側（信箱）被繞過 |
| **元件端點位置** | 套件 `jedi_iam/api/routing.py`：`POST /forget-password`（`api/routes/change_password_route.py:ForgetPasswordRoute.post`）→ `app/service/user_change_password_service.py:request_change_password()` 回傳的實體含一次性重設憑證 `uid`，route 整個 dump 進回應；拿到 `uid` 後可直接打 `POST /user/change-pwd-by-req` 改密碼 |
| **駭客怎麼打** | ① 任何人打開登入頁，不需要帳號；② 在「忘記密碼」輸入目標的信箱（例如原廠管理員的信箱）按送出；③ 系統在回應裡直接附上重設密碼的鑰匙——不必收信、不必知道舊密碼；④ 拿鑰匙去打「用請求憑證改密碼」，把目標帳號的密碼改成自己的；⑤ 目標若是原廠最高權限管理員，等於拿到全部客戶的資料 |
| **得手什麼** | 接管任何知道信箱的帳號，包含原廠最高權限管理員 |
| **修正的做法** | ① **回應改成固定空白**：忘記密碼成功與失敗一律回同一個空內容，不再把重設實體 dump 出去，鑰匙只留在服務內部組信件連結用；② 同步從「改密碼」回應 schema 拿掉 `uid` 欄位；③ 新增三條測試：成功不含鑰匙、遮罩開啟時失敗與成功回應逐位元組相同（不讓人靠回應差異探信箱存不存在）、遮罩關閉時仍照舊回 404；用突變測試確認斷言有牙齒；④ 前端已確認沒有讀取回應內容，零影響 |
| **狀態** | ✅ 已修（CM-1575，套件 jedi-iam commit `a9edf1d3`）。已核對：該功能成功與失敗都回同一個空白結果 |

## M15-5 🟠 程式碼平台通行證四把寫在版控文件裡 {#m15-5}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #10；STRIDE：資料外洩、冒充身分） |
| **在哪裡** | AI 儀表板模組檢視時查到：開發過程留下的對話紀錄文件裡，有外部程式碼平台（GitLab）的個人存取通行證 |
| **攻擊面位置** | **拿得到程式庫（含歷史版本）的人**——內部開發者、離職者、外包，或程式庫本身外流。不需要登入我們的產品，也不是設計上暴露的面 |
| **信任邊界位置** | **版控 ↔ 祕密存放處**。通行證只該存在部署環境的環境變數裡；當時被貼進會 commit 的文件，跟著程式庫流到所有拿得到程式碼的人手上。刪檔案收不回，歷史版本裡一直在 |
| **元件端點位置** | 外流位置：`docs/conversation-history/` 下 3 個對話紀錄檔（`2026-04-28-to-04-30-survey-answer-arc/part-verbatim-03-of-03.md`、`2026-05-13-spec2-phase-e-decision/part-01-of-01-...md` 等）。通行證在產品裡的用途：套件 `jedi_issue/infra/gitlab.py:get_gitlab_client()` 讀 `GITLAB_PRIVATE_TOKEN` 連 GitLab 開問題單 |
| **駭客怎麼打** | ① 拿到程式庫的一份副本（在職或離職的開發者、外包、外洩的備份）；② 在歷史版本裡搜尋通行證的固定前綴，幾秒就找到四把；③ 拿通行證直接呼叫 GitLab API，系統以為是我們公司；④ 讀寫平台上的專案、問題單，甚至推程式碼 |
| **得手什麼** | 以我們公司的身分讀寫程式碼平台上的專案與問題單 |
| **修正的做法** | ① **四把到平台後台全數撤銷**（舊值即刻失效，外流的字串變成廢鑰匙）；② **清版控殘留**：把 3 個檔裡的真實通行證換成佔位文字，其中一處是卡片沒列、執行時多找到的；另一份本來就是假範例值的保留不動；③ 全庫用通行證樣式重新搜尋一次，確認清除後零命中 |
| **狀態** | ✅ 已修正（四把已撤銷；殘留由 CM-2048 清除，commit `e8e134a4e`） |

## M16-5 🟠 測試設定檔推上版控，帶著四把真的 AI 服務金鑰 {#m16-5}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #11；STRIDE：資料外洩） |
| **在哪裡** | 通知模組檢視時查到：專案根目錄的測試用設定檔 `.env.test` 被推上版控，裡面有 OpenAI、Anthropic、Google、LangSmith 四家 AI 服務的真金鑰 |
| **攻擊面位置** | **拿得到程式庫的人**。這份檔案從 2026-03 起就被 `.gitignore` 的例外規則放行入版控，存在了六個月 |
| **信任邊界位置** | **版控 ↔ 祕密存放處**。`.gitignore` 本來就排除 `.env*`，但另一條「這份是範本、可入版控」的例外把它放了進去——**前提是「不含真實憑證」，而這個前提從一開始就不成立**，例外規則替金鑰開了門 |
| **元件端點位置** | 外流檔案：`.env.test`（已撤出版控）；`.gitignore` 的 `!.env.test` 例外（已移除）。金鑰在產品裡的用途：`app/system_config/service/ai_provider_key_resolver.py` 解析不到租戶金鑰時退回環境變數 |
| **駭客怎麼打** | ① 拿到程式庫；② 打開 `.env.test`，四把金鑰整齊列在裡面；③ 拿去呼叫各家 AI 服務，費用算我們公司的；④ LangSmith 那把還能讀走服務端存的歷史執行紀錄，裡面例行含有客戶資料 |
| **得手什麼** | 用公司帳號燒錢呼叫 AI 服務，並讀走含客戶資料的執行紀錄 |
| **修正的做法** | ① **四把金鑰由決策者於 2026-09-08 全數撤銷重發**；② `.env.test` 用 `git rm --cached` 撤出版控（本機副本保留），並移除 `.gitignore` 的 `!.env.test` 例外，讓原本就有的排除規則生效（commit `5746cef10`）；③ 同樣的四把金鑰在九個對話紀錄檔裡的殘留另外清掉（見 [M17-9](#m17-9)）。教訓寫在 commit：之前清過一次只換了兩項就把整份當範本放行，沒有逐項檢查 |
| **狀態** | ✅ 已修正（金鑰已撤銷換發；撤出版控 commit `5746cef10`，殘留由 CM-2048 清除，commit `e8e134a4e`） |

## M17-9 🟠 四家 AI 服務金鑰躺在九個對話紀錄檔裡 {#m17-9}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #11，與 M16-5 同一組金鑰；STRIDE：資料外洩。模組頁評中） |
| **在哪裡** | 公告模組檢視時查到：同一組四把 AI 服務金鑰，除了測試設定檔，還被開發對話貼進九個對話紀錄檔 |
| **攻擊面位置** | **拿得到程式庫的人**。就算設定檔撤出版控，這九個檔還在 |
| **信任邊界位置** | **版控 ↔ 祕密存放處**。對話紀錄是「把開發過程原樣歸檔」的文件，歸檔時沒有過濾祕密，設定檔裡的值就隨著對話內容跨進了版控 |
| **元件端點位置** | 外流位置：`docs/conversation-history/` 下 9 個對話紀錄檔；來源檔 `.env.test`（已撤出版控，見 [M16-5](#m16-5)） |
| **駭客怎麼打** | ① 拿到程式庫；② 在對話紀錄裡搜尋各家金鑰的固定前綴；③ 找到四把金鑰，用法同 M16-5 |
| **得手什麼** | 同 M16-5：燒公司的 AI 額度、讀走含客戶資料的執行紀錄 |
| **修正的做法** | ① 金鑰已撤銷換發（同 M16-5）；② 9 個對話紀錄檔裡的金鑰字串全部清除；③ 全庫交叉搜尋確認零命中 |
| **狀態** | ✅ 已修正（金鑰已撤銷換發；殘留由 CM-2048 清除，commit `e8e134a4e`） |

## M23-1 🟠 資料庫管理員密碼散在 249 個檔案裡 {#m23-1}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #12；STRIDE：資料外洩、權限提升） |
| **在哪裡** | 意見回饋模組檢視時查到：資料庫系統管理帳號（可繞過客戶隔離的那一個）的密碼，散在 249 個進版控的文件裡，其中一份把主機、帳號、密碼湊成可直接複製貼上的連線指令 |
| **攻擊面位置** | **拿得到程式庫的人**；再加上連得到資料庫的網路位置。同一組密碼同時通行開發環境、出貨基線資料庫、兩座標示退役但仍活著的舊資料庫，也是快取服務的密碼 |
| **信任邊界位置** | **版控 ↔ 祕密存放處**，以及**一般應用帳號 ↔ 系統管理帳號**。產品平常用受隔離規則約束的帳號；這組是繞過所有客戶隔離的管理帳號，它的密碼跨進了版控，客戶隔離這條線對拿到它的人就不存在 |
| **元件端點位置** | 外流位置：`docs/conversation-history/`（112 檔）、`docs/features/`（67）、`docs/features-site/`（65）、`docs/issues/`、`docs/claude/memory/`、`docs/analysis/2026-05-28-poc-db-migration-plan.md`（可直接執行的連線指令）。還在寫入的路徑：`docs/system-design/scripts/generate_db_schema_docx.py` 的 `DB_CONN` |
| **駭客怎麼打** | ① 拿到程式庫；② 打開那份遷移計畫，複製三行連線指令；③ 在連得到資料庫的位置貼上執行，以系統管理身分登入；④ 每一家客戶的資料可讀可寫，連出貨基線庫也能改——改進去的東西會跟著安裝檔裝到客戶機器上 |
| **得手什麼** | 繞過全部客戶隔離，讀寫每一家客戶的資料，並可汙染出貨基線 |
| **修正的做法** | 三步、順序不能顛倒：① **先堵還在寫入的路**：`generate_db_schema_docx.py` 改從環境變數 `DB_SCHEMA_DOCX_DB_PASSWORD` 讀，沒設就直接報錯，不退回任何寫死值；② **清掉已寫進去的**：250 個檔的密碼字串換成「請查 .env 或部署文件」，可直接執行的連線指令優先處理，記憶檔兩份手動改寫，文件站整站重新產出（含搜尋索引）；③ **換密碼**：決策者裁定排在正式環境上版前統一換。驗證：全庫含站點產物搜尋密碼值零命中 |
| **狀態** | ✅ 已修（CM-2049，commit `5d4c14221`）；殘留已清、再寫進去的路已堵，**密碼換發排在正式上版前** |

## M16-7 🟠 產生文件的腳本寫死展示環境資料庫連線與密碼 {#m16-7}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #12，另見 #71；與 M23-1 同一組密碼；STRIDE：資料外洩、權限提升。模組頁評中） |
| **在哪裡** | 通知模組檢視時查到：一支產生資料庫結構文件的腳本，把客戶展示環境（依規定等同正式環境）的完整連線資訊寫在程式碼裡 |
| **攻擊面位置** | **拿得到程式庫的人**，加上連得到展示機的內網位置。它是 249 個檔裡**唯一一支會被執行的腳本**，其餘都是文件，所以也是「會繼續把密碼寫出去」的那個源頭 |
| **信任邊界位置** | **版控 ↔ 祕密存放處**。連線資訊該從環境設定讀；當時直接寫成程式常數，每次有人改這支腳本就再 commit 一次 |
| **元件端點位置** | `docs/system-design/scripts/generate_db_schema_docx.py`：模組層級的 `DB_CONN = dict(host=..., dbname=..., user=..., password=...)` |
| **駭客怎麼打** | ① 拿到程式庫；② 打開這支腳本，主機、資料庫名、帳號、密碼一行全有；③ 在內網直接連進展示環境的資料庫；④ 讀寫客戶試玩會看到的所有資料 |
| **得手什麼** | 直接連進等同正式環境的展示機資料庫 |
| **修正的做法** | ① 腳本改成密碼從環境變數 `DB_SCHEMA_DOCX_DB_PASSWORD` 讀，沒設就丟錯中止，不退回任何預設值（commit `5d4c14221`，CM-2049，與 M23-1 同批）；② 版控殘留字串清除（CM-2048／CM-2049）；③ 換發：模組頁記為已換發，但 CM-2049 commit 註明「密碼暫不換發、排正式上版前換」——兩處說法不一致，以 M23-1 的狀態為準 |
| **狀態** | ✅ 已修正（腳本改讀環境變數 commit `5d4c14221`；殘留清除 CM-2048 commit `e8e134a4e`） |

## M17-8 🟠 可繞過客戶隔離的資料庫管理帳號密碼寫在文件裡 {#m17-8}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #12，與 M23-1 同一組密碼；STRIDE：資料外洩、權限提升。模組頁評中） |
| **在哪裡** | 公告模組檢視時查到：資料庫系統管理帳號的密碼寫在開發文件裡 |
| **攻擊面位置** | **拿得到程式庫的人**，加上連得到開發環境或出貨基線資料庫的位置 |
| **信任邊界位置** | **版控 ↔ 祕密存放處**，以及**一般應用帳號 ↔ 可繞過隔離的管理帳號**。出貨基線庫是所有安裝檔的來源，這條線一破，寫進去的東西會跟著出貨 |
| **元件端點位置** | 外流位置：`docs/claude/memory/`、`docs/system-design/`、`docs/features/` 等開發文件（與 M23-1 的 249 檔同批）。受影響的帳號：資料庫的系統管理帳號（`cmmgr`，可繞過客戶隔離） |
| **駭客怎麼打** | ① 拿到程式庫；② 在文件裡找到管理帳號密碼；③ 連進開發環境或出貨基線資料庫；④ 繞過所有客戶隔離讀寫資料，或在基線庫埋東西等著隨安裝檔出貨 |
| **得手什麼** | 開發環境與出貨基線庫的完整讀寫權 |
| **修正的做法** | ① 與 M23-1 同一批清除：文件裡的密碼字串換成「請查 .env 或部署文件」；② 文件站重新產出；③ 換發：模組頁記為已換發，但 CM-2048、CM-2049 兩個 commit 都註明資料庫管理密碼「本輪不換發、排正式上版前統一處理」——兩處說法不一致，以 M23-1 的狀態為準 |
| **狀態** | ✅ 已修正（殘留由 CM-2048 commit `e8e134a4e`、CM-2049 commit `5d4c14221` 清除） |

## M22-1 🟠 子公司管理員能改全公司的登入規則與帳號目錄 {#m22-1}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #22；STRIDE：權限提升、冒充身分；主分類 A01 存取控制失效） |
| **在哪裡** | 系統設定模組：登入政策（雙因子、鎖定、登入憑證效期）、員工帳號目錄連線（LDAP）、寄信設定、日誌轉送——這幾組設定全系統只有一份 |
| **攻擊面位置** | **任何一家客戶（含子公司）的管理員**。這些設定的修改權限是發給各客戶管理員的，開一個新客戶就多一個打得到的人 |
| **信任邊界位置** | **客戶租戶 ↔ 全站共用設定**。權限只回答「這個人能不能改設定」，沒回答「他站的層級有沒有資格改全公司那一份」；而設定實體只存在最上層一列，子公司管理員按下儲存就改到了全站 |
| **元件端點位置** | 主專案 `PUT /system/security-policy`（`api/system_config/routes/security_policy_route.py` → `SecurityPolicyAppService.update_policy`）、`PUT/DELETE /system/config/{group}/{key}`（`SystemConfigGroupRoute` → `GuardedSystemConfigService`）；套件 jedi-system-core `POST /system/config`、`PUT/DELETE /system/config/{uid}`（經 `core/plugins/system_core.py:assert_config_capability`）；日誌轉送 `PUT /log-forwarding`、`POST /log-forwarding/test`（`core/plugins/api_log.py:_forwarding_guard`）。守門在 `common/authz/tenant_hq.py` |
| **駭客怎麼打** | ① 某家子公司的管理員登入；② 打開系統設定的登入政策，關掉雙因子、把帳號鎖定次數設成無限、把登入憑證效期從五分鐘拉到一個月；③ 更狠的：打開員工帳號目錄設定，把目錄伺服器位址改成自己架的機器；④ 之後全公司每個人用公司帳號登入，帳密都送到他的機器，他回什麼「驗證通過」系統就信什麼——等於接管所有人的登入，而且畫面上看不出被改過 |
| **得手什麼** | 接管全公司所有人的登入，並拿走他們輸入的公司帳密 |
| **修正的做法** | ① **新增第七條守門軸「總部層級」**（`common/authz/tenant_hq.py`）：全公司共用的三組設定（寄信、帳號目錄、登入政策）列在 `common/constant/company_wide_config.py`，寫入時除了查權限，還要判「他是不是總部」，子公司一律 403 `GRC_403066`，讀取不卡；② **每個寫入入口都掛**：套件三條設定路由、主專案依群組讀寫那條、登入政策專屬端點、日誌轉送寫入那欄——守門放在服務層而不只在路由，因為寫入入口不只一條，逐條補必漏（commit `e90d8e3cb`，驗收補 `a25933de8` 讓日誌轉送先查權限再查層級）；③ **後續修正總部的判定**（CM-2202，commit `6cfcb116e`）：原本用「組織樹第二層」認總部，但最上層底下第二層不只一家，任何一家都能改全站；改成落地安裝版只認「安裝精靈建立的那個客戶租戶」、雲端多租戶版只認原廠，平台管理員一律放行，查不到身分往拒絕倒 |
| **狀態** | ✅ 已修（CM-2054，commit `e90d8e3cb`＋`a25933de8`；判定修正 CM-2202，commit `6cfcb116e`）。驗證：平台／總部／子公司 × 五組設定 × 讀寫矩陣實跑，子公司寫入一律 403；判定修正後 DEV 實打兩種部署模式 |

## M13-2 🟠 公司帳號登入可匿名通過、可注入、加密不認人 {#m13-2}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #28；STRIDE：冒充身分、資料外洩；另命中 A05 注入、A04 加密） |
| **在哪裡** | 登入與權限模組：登入頁「用公司帳號登入」（串接客戶公司內部的帳號目錄伺服器） |
| **攻擊面位置** | **不需要登入**的登入 API；客戶啟用公司帳號登入後，任何打得到登入頁的人都構成攻擊面。加密不認人那一項還要求攻擊者站在我們與目錄伺服器之間的網路上 |
| **信任邊界位置** | **使用者輸入 ↔ 客戶的帳號目錄伺服器**。系統應該用使用者輸入的密碼向目錄伺服器證明身分；當時三道都沒守：用匿名身分連上去就算過、帳號字串原樣拼進查詢、加密連線不檢查對方憑證 |
| **元件端點位置** | 套件 jedi-iam `POST /login`（`provider=ldap`）→ `infra/adapter/authenticate_adapter/ldap/ldap_adapter.py:LdapAdapter.authenticate()`／`_verify_open_ldap_user()`／`connect()`／`_get_ldap_server()`；設定測試 `POST /ldap/connect-test`；設定存在系統設定 `THIRD_PARTY_LOGIN` 群組 |
| **駭客怎麼打** | ① 在登入頁選「公司帳號登入」；② 帳號填目標員工、密碼隨便填——系統用匿名身分連目錄伺服器，搜得到人就算登入成功，密碼從沒被比對過；③ 或把帳號填成 `*`，查詢條件被改寫成「任何人」，搜到第一個就讓你進去；④ 若站在網路中間，拿一張自簽憑證冒充目錄伺服器，系統照樣連過來，員工輸入的公司密碼整批落到攻擊者手上 |
| **得手什麼** | 不知道密碼就能以任一員工身分登入，或攔下員工的公司帳密 |
| **修正的做法** | ① **兩段式驗證**：先用服務帳號（或匿名）查出使用者在目錄裡的完整名稱，再用這個名稱＋使用者輸入的密碼真正登入一次，失敗就算密碼錯；② **查詢條件跳脫**：兩處查詢都改用 `escape_filter_chars`，`*` 與特殊字元不再能改寫查詢；③ **加密連線驗憑證**：初版做成主機環境變數放 CA 檔，首腦裁定形狀不對（這組設定由客戶在設定頁自助填，憑證不該拆到主機上），重做成設定頁兩個欄位——「驗證伺服器憑證」開關與選填的 CA 憑證內容，驗證時 `CERT_REQUIRED`，空白就走系統信任庫，憑證不過時「測試連線」顯示專屬錯誤碼 `LOGIN_401016`；④ 前端設定頁加開關與 CA 欄位。之後決策者另裁（CM-2239）：新建設定時「驗證伺服器憑證」**預設不勾**、拿掉未勾警告——開關與 CA 欄位仍在，要驗得由客戶自己勾 |
| **狀態** | ✅ 已修（CM-1560，套件 commit `9b353ee5`＋`20c67131`，主專案 `f503fba33`，前端 `78903b2`；預設值調整前端 `16141ec`）。已核對：兩段式驗證、跳脫、憑證驗證三項都在，用 DEV 真實目錄伺服器驗過 |

## M13-3 🟠 綁定外部帳號不確認是本人 {#m13-3}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #29；STRIDE：冒充身分、權限提升） |
| **在哪裡** | 登入與權限模組：個人設定 → 綁定外部帳號（公司帳號、Google） |
| **攻擊面位置** | **任何已登入的員工**，最低權限即可 |
| **信任邊界位置** | **使用者 ↔ 別人的帳號**。綁定是在「某個使用者」底下掛一個外部身分，應該先確認「操作的人就是這個使用者，或有管理帳號的權限」；當時只檢查「目標使用者存在」與「這個外部身分沒被綁走」 |
| **元件端點位置** | 套件 jedi-iam `POST /auth-provider`（`UserAuthProviderCreateRoute`）、`DELETE /auth-provider/{uid}`（`UserAuthProviderRoute`）→ `app/service/user_auth_provider_service.py:register_ad_user()`／`register_ldap_user()`／`register_google_user()`／`delete_user_auth_provider()` |
| **駭客怎麼打** | ① 一般員工用自己的帳號登入；② 呼叫綁定 API，目標填管理員的使用者編號、外部帳號填自己的公司帳號；③ 系統沒問「你是不是那個人」，綁定成功；④ 登出，改用「公司帳號登入」並輸入自己的公司帳密——系統查到這個外部身分綁在管理員名下，就讓他以管理員身分進來；⑤ 解綁 API 同理，可以把別人既有的綁定拆掉讓他登不進來 |
| **得手什麼** | 任何員工自助升級成管理員 |
| **修正的做法** | ① 新增 `_assert_caller_can_manage(user_uid)`：呼叫者就是那個使用者本人，或持有「修改使用者」權限，才放行，否則 403；② 四個方法各自呼叫，**守門放在任何外部驗證與資料庫寫入之前**（不讓攻擊者靠回應差異探別人的目錄帳號）；解綁先查出紀錄拿到所屬使用者再判；③ 新增 8 條測試（本人放行、有權限放行、無權限 403），突變測試確認拔掉守門會紅 |
| **狀態** | ✅ 已修（CM-1562，套件 jedi-iam commit `76a8c2dd`）。已核對：每一支綁定與解綁前都先過本人或管理權限檢查 |

## M13-5 🟠 用 Google 登入是沒接通的空殼 {#m13-5}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #31；STRIDE：冒充身分） |
| **在哪裡** | 登入與權限模組：登入頁「用 Google 登入」與個人設定「綁定 Google 帳號」 |
| **攻擊面位置** | **不需要登入**的登入 API（只要在請求裡指定 Google 這個登入方式）；綁定那一半要任一已登入帳號 |
| **信任邊界位置** | **使用者自稱 ↔ Google 驗證**。這條路上「你是誰」應該由 Google 簽發的憑證證明；當時從沒向 Google 驗證過——登入不驗密碼、綁定時把呼叫者填的值直接存成 Google 帳號編號 |
| **元件端點位置** | 套件 jedi-iam `POST /login`（`provider=google`）→ `infra/adapter/auth_factory.py` → `authenticate_adapter/google_auth/google_auth_adapter.py:GoogleAuthAdapter.authenticate()`；`POST /auth-provider`（`provider=google`）→ `user_auth_provider_service.py:register_google_user()`。綁定端序列化器 `jedi_iam/api/serializers/user_auth_provider.py`（原在主專案，CM-1832 上移套件） |
| **駭客怎麼打** | ① 在綁定 API 指定 Google、自己填一個 Google 帳號編號，系統原樣存下；② 或直接打登入 API 指定 Google，宣稱自己是某個已綁定的 Google 帳號；③ 系統不向 Google 查證，宣稱什麼就是什麼；④ 一旦這個入口對外開放，誰都能自稱是任何人 |
| **得手什麼** | 自稱任何綁過 Google 的使用者並登入 |
| **修正的做法** | 決策者裁定**關閉入口、不刪程式碼**：① 登入方式對照表拔除 Google，未知的登入方式改回 405 而不是掉進 500；② 登入請求的「登入方式」欄位加白名單（密碼、AD、LDAP），Google 在格式檢查層就 400；③ 綁定的派送表移除 Google 分支，主專案綁定端也同步加白名單（只收 AD、LDAP）；④ Google 那支程式保留，檔頭寫明關閉原因與「重新啟用前必須先補 Google 憑證驗證，不能只把對照表加回去」 |
| **狀態** | ✅ 已修（CM-1561，套件 jedi-iam commit `d28e3647`，主專案 commit `d08bc34c1`）。手測：登入與綁定帶 Google 都收到 400／405 |

## M13-6 🟠 雙因子驗證碼可以無限次猜 {#m13-6}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #32；STRIDE：冒充身分） |
| **在哪裡** | 登入與權限模組：登入頁輸入密碼後的第二步——輸入信箱收到的或驗證器 App 產生的六位數驗證碼 |
| **攻擊面位置** | **已經知道某人密碼的人**（密碼外洩、撞庫、釣魚）。雙因子存在的意義就是「密碼外流也還有一道」，所以這一面正是它要防的對象 |
| **信任邊界位置** | **只過第一道的人 ↔ 完整登入**。驗證碼只有一百萬種組合，安全性全靠「猜錯幾次就作廢」；密碼登入錯三次會鎖，但驗證碼走的是另一套程式，完全沒有次數限制 |
| **元件端點位置** | 套件 jedi-iam `POST /otp-verify`（`MfaVerifyRoute`）、`POST /totp-verify`（`TotpVerifyRoute`）、`POST /otp-resend`；服務 `mfa/app/service/email_service.py:EmailMfaService.verify_otp()`、`mfa/app/service/totp_service.py:TotpMfaService.verify_otp()` |
| **駭客怎麼打** | ① 用外流的密碼登入，系統要求輸入驗證碼；② 寫一支程式從 000000 開始一直送；③ 信箱驗證碼有 300 秒有效期，系統每次只回「錯誤」從不鎖；④ 猜中就完整登入，雙因子形同虛設 |
| **得手什麼** | 有密碼就能繞過雙因子接管帳號 |
| **修正的做法** | ① **信箱驗證碼**：每人一個失敗計數，超過 5 次就刪掉當前驗證碼並回專屬錯誤碼 `MFA_401002`；計數的存活時間跟著驗證碼走，驗證成功或發新碼時歸零；② **驗證器 App**：密鑰是永久的、沒有碼可刪，改成失敗超過 5 次後在登入鎖定時間（沿用既有設定，預設 15 分鐘）內一律拒絕，碼對了也不比對；③ 快取客戶端加「只在第一次建立時設存活時間」的計數方法，避免每次失敗都把窗口往後延；④ 重發驗證碼有冷卻時間（換瀏覽器、清畫面、直接打 API 都繞不過），而登入首發不吃冷卻（CM-2269 補） |
| **狀態** | ✅ 已修（CM-1557，套件 jedi-iam commit `b51b9149`）。已核對：失敗五次即鎖、驗證碼作廢，重發有冷卻 |

## M18-3 🟠 開通序號只有 8 個字，猜得到別人的授權 {#m18-3}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #35；STRIDE：冒充身分；主分類 A04 加密機制失效） |
| **在哪裡** | 授權簽發站（獨立系統）：客戶在線上開通頁輸入開通序號，換回綁定自己機器的授權檔 |
| **攻擊面位置** | **不需要登入**，而且是簽發站唯一一個對外公開的入口——設計上就暴露給所有客戶的機器 |
| **信任邊界位置** | **網路上任何人 ↔ 某張授權**。持有序號就等於持有那張授權，所以序號本身就是密碼；當時序號只有 8 個十六進位字元（約 43 億組），而「猜錯鎖定」是用被猜的序號當計數鍵——攻擊者每次猜不同序號，永遠到不了門檻 |
| **元件端點位置** | 簽發站 `POST /api/activation/activate`（`src/license_center/web/views/activation.py`）→ `modules/licensing/service.py` 產生與比對序號；失敗計數 `modules/auth/service.py`、`repository.py` |
| **駭客怎麼打** | ① 不需要任何帳號，直接打開通 API；② 用程式大量送隨機的 8 碼序號配上自己機器的指紋；③ 系統鎖定是按「這個序號錯幾次」計，每次換一個序號就永遠不鎖；④ 撞中一張別人尚未兌換的序號，那張授權就綁到攻擊者的機器上 |
| **得手什麼** | 領走別人付費的授權 |
| **修正的做法** | ① **序號改用約 128 位元的隨機值**（`secrets.token_urlsafe(16)`），並加 30 天效期，未兌換的序號不再永久有效；② **失敗鎖定改成按來源 IP 計、存資料庫**，另加全站失敗預算擋分散式猜測，連帶解掉原本計數表每次請求都寫、無上限的問題；③ 序號與機器指紋欄位加長度上限；④ 紀錄裡的序號摘要拿掉明文前綴（舊序號恰好 8 碼時前綴等於全洩）；⑤ 補發改成產新序號、舊序號作廢，查詢只認未作廢的 |
| **狀態** | ✅ 已修（簽發站 CM-1583，commit `a68200c`）。驗證：連猜 6 個不同序號第 6 次回 429，正常序號開通成功後重用回 409 |

## M16-8 🟡 系統管理員密碼寫死在三支腳本裡 {#m16-8}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #72；STRIDE：資料外洩、權限提升） |
| **在哪裡** | 通知模組檢視時查到：三支資料調整／測試腳本寫死系統管理員帳號密碼，其中兩支寫成「環境變數沒設就用這個」 |
| **攻擊面位置** | **拿得到程式庫的人**；另一面是**執行腳本的人自己**——變數忘了設，腳本會靜默用真密碼跑下去，操作者不會被告知 |
| **信任邊界位置** | **版控 ↔ 祕密存放處**。「沒設就用預設值」本來是方便，但預設值填的是真密碼，等於把祕密當成程式的一部分出貨 |
| **元件端點位置** | `scripts/migrate_2026-08-09_fr062_existing_tenant_licenses.py`、`scripts/seed_2026-08-01_fr059_detection_profiles.py`（兩支「沒設就用真密碼」）；另有 `scripts/smoke_test_ssp_docx_parser.py`、`scripts/smoke_test_ssp_docx_preselect.py`、`scripts/e2e_test_module_frame_with_docx.py` 三支測試腳本同樣帶密碼 |
| **駭客怎麼打** | ① 拿到程式庫；② 打開任一支腳本，預設值那一行就是管理員密碼；③ 用它登入產品，取得系統管理員身分 |
| **得手什麼** | 一組可用的系統管理員帳密 |
| **修正的做法** | ① 兩支 migration／seed 腳本拿掉寫死的預設值，**環境變數沒設就中止並印出設定方式**，不再靜默用真密碼；② 三支測試腳本改用環境變數帶入；③ 文件與對話紀錄裡的殘留字串（約 100 多檔）全數清除，改標「密碼請查 .env 或部署文件」；④ 換發：模組頁記為已換發，但 CM-2048 commit 註明「系統管理員密碼本輪不換發，待正式上版前統一處理」——兩處說法不一致 |
| **狀態** | ✅ 已修正（CM-2048，commit `e8e134a4e`）。手測：兩支腳本未設環境變數時確實中止並提示 |

## M17-11 🟡 資料庫調整腳本「沒設變數就用真密碼」 {#m17-11}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #72，與 M16-8 同一組；STRIDE：資料外洩、權限提升） |
| **在哪裡** | 公告模組檢視時查到：一支資料庫調整腳本寫「環境變數沒設就用這個密碼」，那個密碼是真的管理員密碼 |
| **攻擊面位置** | **拿得到程式庫的人**；以及忘了設變數就執行的維運人員 |
| **信任邊界位置** | **版控 ↔ 祕密存放處**。同 M16-8 |
| **元件端點位置** | `scripts/migrate_2026-08-09_fr062_existing_tenant_licenses.py`（環境變數讀取那一行的預設值） |
| **駭客怎麼打** | ① 拿到程式庫；② 打開這支腳本，讀出預設值；③ 用它以管理員身分登入 |
| **得手什麼** | 一組可用的系統管理員帳密 |
| **修正的做法** | ① 拿掉寫死的預設值，環境變數沒設就中止並提示（與 M16-8 同一個 commit）；② 版控殘留字串清除；③ 換發時程同 M16-8（commit 註明排正式上版前） |
| **狀態** | ✅ 已修正（CM-2048，commit `e8e134a4e`） |

## M17-10 🟡 套件倉庫與檔案儲存帳密寫在交接文件裡 {#m17-10}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #73；STRIDE：竄改資料、權限提升；主分類 A03 軟體供應鏈失效） |
| **在哪裡** | 公告模組檢視時查到：一份功能交接文件寫著公司內部套件倉庫與檔案儲存服務的帳密 |
| **攻擊面位置** | **拿得到程式庫的人**，加上連得到公司內網套件倉庫的位置（三樣都是內網的東西，外人拿到也要先進內網） |
| **信任邊界位置** | **版控 ↔ 祕密存放處**，以及**套件倉庫 ↔ 打包流程**。套件倉庫是所有內部套件的來源，打包機照單全收；發布權一外流，供應鏈的根就不在我們手上 |
| **元件端點位置** | `docs/features/FR-039-2606-distributed-file-agent/handoff/2026-06-18-FR039-handoff.md`（及其 `.html` 與 `docs/features-site/` 的鏡像頁、搜尋索引）；另有 7 個對話紀錄檔散著同一組儲存服務密碼 |
| **駭客怎麼打** | ① 拿到程式庫，打開那份交接文件；② 用套件倉庫的帳密登入內網倉庫；③ 推一個同名、版號更高、帶後門的內部套件；④ 下次打包自動抓到新版，後門就跟著安裝檔裝到客戶機器上 |
| **得手什麼** | 在客戶拿到的安裝檔裡植入後門 |
| **修正的做法** | ① 帳密換發（模組頁記錄）；② 按順序清殘留：先修來源 md（順手更正文件裡「已加入 gitignore」的錯誤敘述——那段文字其實早已 commit 進追蹤中的文件）→ 重建 HTML → 同步文件站鏡像並 strict 建置 → 補清對話紀錄；③ 用較寬的搜尋多清出 7 個卡片沒列的檔 |
| **狀態** | ✅ 已修正（帳密已換發；殘留由 CM-2048 清除，commit `e8e134a4e`） |

## M15-6 🟡 套件倉庫管理員帳密＋儲存金鑰＋登入帳密同在一份交接文件 {#m15-6}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #73，與 M17-10 同一份文件；STRIDE：竄改資料、權限提升） |
| **在哪裡** | AI 儀表板模組檢視時查到：同一份交接文件裡，套件倉庫管理員帳密、檔案儲存服務金鑰、一組畫面登入帳密三樣並列 |
| **攻擊面位置** | **拿得到程式庫的人**，加上公司內網位置（三樣都是內網環境的東西，所以評為中） |
| **信任邊界位置** | **版控 ↔ 祕密存放處**。三把不同用途的鑰匙寫在同一頁，一次外流三條路：供應鏈、證據檔、產品登入 |
| **元件端點位置** | `docs/features/FR-039-2606-distributed-file-agent/handoff/2026-06-18-FR039-handoff.md`（同 M17-10）。儲存金鑰在產品裡的用途：檔案儲存設定（`STORAGE_CONFIG`），讀寫稽核證據 |
| **駭客怎麼打** | ① 拿到程式庫，打開交接文件；② 套件倉庫帳密：照 M17-10 推帶後門的套件；③ 儲存金鑰：直接讀寫儲存服務裡的稽核證據；④ 畫面登入帳密：直接登入系統 |
| **得手什麼** | 植入供應鏈後門、讀寫稽核證據、登入系統 |
| **修正的做法** | ① 三樣帳密換發，舊值失效；② 交接文件與鏡像頁殘留清除（同 M17-10 那批）。模組頁特別提醒：同一組套件倉庫密碼還出現在前端打包腳本與三支 CI 設定檔，只清那四個檔、漏掉交接文件就等於沒清——那四個檔的處理見 [M23-6](#m23-6) |
| **狀態** | ✅ 已修正（三樣已換發；殘留由 CM-2048 清除，commit `e8e134a4e`） |

## M23-3 🟡 Google 雲端硬碟密鑰與加密金鑰散在 15 個檔案裡 {#m23-3}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #74；STRIDE：資料外洩；另命中 A04 加密、A02 設定） |
| **在哪裡** | 意見回饋模組檢視時查到：Google 雲端硬碟整合用的應用程式密鑰、應用程式編號、通行證加密金鑰，散在 15 個對話紀錄檔裡 |
| **攻擊面位置** | **拿得到程式庫的人**。這組密鑰是 Google 後台唯一一組、所有客戶環境共用（比對過展示機與開發環境的雜湊值完全相同），影響範圍比其他金鑰外流都大 |
| **信任邊界位置** | **版控 ↔ 祕密存放處**，以及**我們 ↔ 客戶的雲端硬碟**。客戶授權我們讀他的雲端硬碟，換來的通行證用加密金鑰加密存在資料庫；加密金鑰一外流，資料庫裡的通行證就只剩一層薄紙 |
| **元件端點位置** | 外流位置：`docs/conversation-history/` 下 15 個檔。值在產品裡的用途：`config/config.py`（`GOOGLE_DRIVE_OAUTH_CLIENT_SECRET`、`DRIVE_TOKEN_ENCRYPTION_KEY`）、`infra/cloud_integration/crypto/fernet_crypto.py`（加解密通行證）、`app/system_config/service/google_drive_app_config_resolver.py` |
| **駭客怎麼打** | ① 拿到程式庫，在對話紀錄裡找到應用程式密鑰與加密金鑰；② 若再拿到資料庫（例如配合 [M23-1](#m23-1) 那組管理員密碼），用加密金鑰解開所有客戶存的雲端硬碟通行證；③ 拿通行證讀取客戶雲端硬碟裡的檔案 |
| **得手什麼** | 配合資料庫存取時，可讀所有客戶連結的雲端硬碟檔案 |
| **修正的做法** | ① 15 個檔裡三個值的明文換成佔位文字，用 `.env` 現行值對全庫搜尋取聯集，修改後再逐值確認零命中；② 只清版控字串，不動金鑰本身與程式邏輯；③ 金鑰重發、每個環境改用各自的加密金鑰、既有通行證重新加密，另案處理（換之前要先寫好重新加密程式，否則客戶既有整合會全部失效） |
| **狀態** | ✅ 已修（CM-2051，commit `ccbb27148`）；15 檔殘留已清，**金鑰重發與通行證重新加密另案** |

## M23-6 🟡 打包前端映像檔的腳本寫著套件倉庫帳密 {#m23-6}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #75；STRIDE：竄改資料、資料外洩；主分類 A03 軟體供應鏈失效） |
| **在哪裡** | 出貨打包流程：打包前端映像檔的腳本，以及前端程式庫的三支 CI 流程設定檔 |
| **攻擊面位置** | **拿得到後端或前端任一程式庫的人**。帳號是管理員層級，不是限定唯讀的專用帳號 |
| **信任邊界位置** | **版控 ↔ 祕密存放處**，以及**打包流程 ↔ 套件倉庫**。帳密以「看起來像亂碼、一行指令就能還原」的編碼寫在腳本預設值，並透過建置參數傳進映像檔——建置參數還會留在建置紀錄與映像檔歷史裡 |
| **元件端點位置** | 後端 `scripts/build/build_fe_image.sh`（`NEXUS_AUTH` 預設值，以 `--build-arg` 傳入）；前端 `Dockerfile`；前端 `.gitlab-ci.yml`、`.gitlab-ci-on-premises.yml`、`.gitlab-ci-terraform.yml` |
| **駭客怎麼打** | ① 拿到任一程式庫；② 打開打包腳本，把預設值那串解碼，得到套件倉庫管理員帳密；③ 登入套件倉庫，推一個帶後門的前端套件；④ 下次打包前端映像檔時自動裝進去，客戶拿到的安裝檔就帶著它 |
| **得手什麼** | 往客戶安裝檔的打包流程裡塞東西 |
| **修正的做法** | ① **打包腳本**刪掉寫死的帳密，改用 BuildKit secret 在建置當下掛入 npm 設定檔（`--secret id=npmrc`），不再走建置參數；找不到設定檔就警告並退回公開套件來源（commit `237ffbf86`）；② **前端 Dockerfile** 改讀掛入的 secret（前端 commit `7e8f545`）；③ **三支 CI 設定檔**刪掉明文帳密，改讀專案 CI 變數（遮罩保存）並走 BuildKit secret（前端 commit `48cc2bd`）；④ 驗證：對兩顆映像檔的歷史與建置紀錄搜尋帳密片段皆零命中 |
| **狀態** | ✅ 已修，1.21.0 出貨（CM-2230、CM-2252）。兩個程式庫已查無明文；套件倉庫帳密是否輪換由決策者另裁 |

## M16-3 🟡 連郵件伺服器有加密但不認人 {#m16-3}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #97；STRIDE：資料外洩、冒充身分；主分類 A04 加密機制失效） |
| **在哪裡** | 通知模組：系統寄出所有信件（登入驗證碼、新帳號初始密碼信、通知信、測試信）時與郵件伺服器之間的加密連線 |
| **攻擊面位置** | **站在我們與郵件伺服器之間網路路徑上的人**。郵件服務在外部雲端時比較容易做到 |
| **信任邊界位置** | **我們的伺服器 ↔ 郵件伺服器**。客戶在設定頁選了加密，合理相信連線安全；但程式用的語言內建元件預設就不驗對方憑證，我們也沒有主動打開——**不是我們關掉的，是我們沒有打開** |
| **元件端點位置** | 套件 `jedi_notification/infra/smtp_mail/smtp_mail_adapter.py`（`starttls()` 不帶驗證設定）、`app/dto/smtp_email_config_dto.py`；主專案 `app/notification/service/test_mail_service.py`、`notification_service.py` 組寄信設定；入口 `POST /mail/test` 與所有寄信流程 |
| **駭客怎麼打** | ① 站到我們與郵件伺服器之間；② 攔下加密連線，拿一張自簽憑證冒充郵件伺服器；③ 系統不檢查憑證、照樣送出郵件帳密；④ 接下來每一封信都經過他——包含登入驗證碼與新帳號的初始密碼，帳號接管的材料整批到手 |
| **得手什麼** | 郵件帳密，以及每一封信的內容（含驗證碼與初始密碼） |
| **修正的做法** | ① 寄信設定多兩個欄位：「驗證伺服器憑證」開關與選填的信任憑證；開關打開時加密連線改帶 `CERT_REQUIRED`＋主機名稱檢查的設定，填了信任憑證就只信那一份；驗證失敗回「寄送失敗」並記一行看得懂的訊息（套件 commit `e12287e2`）；② 主專案組寄信設定時把這兩個欄位帶進去（commit `e3c726896`），**升級前的舊設定沒這兩個鍵，一律當「不驗」**，避免升級當天接自簽伺服器的客戶寄不出信；③ 前端設定頁加開關與信任憑證欄位（前端 commit `b391966`）；④ 決策者另裁（CM-2239）：新建設定時開關**預設不勾**、拿掉關閉時的風險說明與舊設定頂部提示——要驗得由客戶自己勾（前端 commit `16141ec`） |
| **狀態** | ✅ 已修（CM-2111）。注意：總表與模組頁寫「新建設定預設驗證、舊設定頂部提示」，之後 CM-2239 已改成預設不勾、不提示，現況以前端 `16141ec` 為準 |

## M17-7 🟡 每套安裝都種下同一組最高權限帳號 {#m17-7}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #98；STRIDE：權限提升；另命中 A02 設定） |
| **在哪裡** | 安裝程式：每裝一套都會建立同一組原廠最高權限帳號（原廠系統殼帳號），不強制首次登入就改密碼 |
| **攻擊面位置** | **知道或破解出那組密碼的人**，加上連得到任一套安裝的登入頁。帳號刻意設計成刪不掉、停不掉 |
| **信任邊界位置** | **原廠 ↔ 客戶環境**。這個帳號是原廠進入客戶環境支援用的鑰匙；每套都同一把，等於一把鑰匙開所有客戶的門 |
| **元件端點位置** | `scripts/init/06-admin.sql`（只存 bcrypt 雜湊，建立 tenant 1 底下的最高權限帳號）；刪除／停用／降權的守門 `app/auth/service/user_app_service.py:assert_root_admin_mutable()`；登入 `POST /login` |
| **駭客怎麼打** | ① 從一套安裝裡取得那組帳號的雜湊（例如進得了某台主機的資料庫）或從原廠內部文件外流；② 離線破解出密碼；③ 拿這組帳密登入任何一套裝出去的系統，都是最高權限 |
| **得手什麼** | 每一套已出貨安裝的最高權限帳號 |
| **修正的做法** | **裁定不修**。理由：① 這是原廠系統殼帳號，密碼只有原廠知道、不交給客戶，用於原廠支援；② 客戶日常使用的管理員帳號由客戶在設定精靈自行建立、自行設密碼，安裝程式不經手、不印出任何密碼；③ 版控只存 bcrypt 雜湊，明文不入版控、不進手冊、不進安裝紀錄 |
| **狀態** | 🚫 裁定不修 |

## M23-4 🟡 帶著 GitHub 通行證連線卻不確認對方是真的 GitHub {#m23-4}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #100；STRIDE：資料外洩、冒充身分；主分類 A04 加密機制失效） |
| **在哪裡** | 意見回饋模組：使用者送出意見回饋時，系統帶公司的 GitHub 通行證去 GitHub 開問題單、同步附件與成員 |
| **攻擊面位置** | **站在我們與 GitHub 之間網路路徑上的人**。使用者送一筆意見回饋就會觸發連線 |
| **信任邊界位置** | **我們的伺服器 ↔ GitHub**。帶著公司通行證連外，應該先確認對方真的是 GitHub；程式裡兩處明確寫了「關掉憑證驗證」 |
| **元件端點位置** | 套件 `jedi_issue/infra/github.py:get_github_client()`、`jedi_issue/infra/issue/adapter/github/github_issue_adapter.py`（兩處 `Github(..., verify=False)`）；觸發入口 `POST /feedback`（`FeedbackRoute`） |
| **駭客怎麼打** | ① 站到我們與 GitHub 之間；② 等任何使用者送出一筆意見回饋；③ 系統連 GitHub 時不驗憑證，連到他的假 GitHub；④ 請求標頭裡的公司 GitHub 通行證就到他手上 |
| **得手什麼** | 公司的 GitHub 通行證 |
| **修正的做法** | ① 拿掉兩處 `verify=False`，恢復 PyGithub 預設的憑證驗證；② 順手核對 GitLab 那一半與附件姊妹檔，沒有同樣的關閉開關 |
| **狀態** | ✅ 已修（CM-2066，套件 jedi-issue commit `4f54c9b4`） |

## M16-1 🟡 寄測試信把存著的郵件密碼送到操作者指定的主機 {#m16-1}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #107；STRIDE：資料外洩；主分類 A01 存取控制失效） |
| **在哪裡** | 通知模組：郵件設定頁的「寄測試信」按鈕 |
| **攻擊面位置** | **持有「改寄信設定」權限的人**——這個權限刻意下放到各個下層客戶的管理員（共 9 個角色）。技術門檻低，只要一台會收郵件連線的主機 |
| **信任邊界位置** | **操作者填的主機 ↔ 系統存著的密碼**。寄信設定全系統只有一份、存在最上層；密碼欄不改時系統會拿存著的真密碼去試寄，但伺服器位址那一欄是操作者自己填的——存著的密碼跟著操作者的位址走了 |
| **元件端點位置** | 套件 jedi-notification `POST /mail/test`（`api/routes/mail_route.py:SendMailTestRoute`）→ 主專案 `app/notification/service/test_mail_service.py:send_test_mail()` |
| **駭客怎麼打** | ① 下層客戶的管理員打開郵件設定頁；② 把伺服器位址改成自己控制的主機、密碼欄留空不動；③ 按「寄測試信」；④ 系統用最上層存著的真密碼去連他的主機，帳密到手；⑤ 拿它以最上層的名義對外寄信，會通過寄件人驗證，收件人看不出是偽造的 |
| **得手什麼** | 最上層單位的郵件帳密，可冒名寄出通過驗證的釣魚信 |
| **修正的做法** | **權限面裁定不修**：寄信設定屬客戶最上游帳號的權責，要不要把權限下放給子單位是客戶自己的決定；影響範圍縮在單一客戶內部。**程式面已加強**：密碼欄沒改時，比對這次送來的伺服器位址與連接埠跟存著的是否相同，不同就回 400 `NOTIFY_400005` 要求重新輸入密碼，不再沿用存著的那一組（兩邊轉字串再比，前端送數字、舊資料可能存字串）；新增 4 條測試，突變測試確認拿掉這道檢查會紅 |
| **狀態** | 🚫 裁定不修（權限面）；程式面加強已做（CM-2111，commit `e3c726896`） |

## M07-13 🟡 開關一放開，原廠 AI 金鑰就送到客戶指定的伺服器 {#m07-13}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #172；STRIDE：資料外洩；主分類 A01 存取控制失效） |
| **在哪裡** | 證據自動分類模組：「AI 服務設定」頁與跑證據分類時組 AI 服務連線的那一段；以及新客戶建立時抄設定的那一段 |
| **攻擊面位置** | **客戶管理員**，前提是「AI 金鑰只限平台管理員」這個開關被放開。檢視當時開關關著打不到；開關說明卻寫「放開時改一下即可」 |
| **信任邊界位置** | **客戶設定 ↔ 原廠金鑰**。金鑰與服務位址應該成對來自同一層；當時兩者各自「先找客戶、找不到再找原廠」，就能組出「原廠的金鑰＋客戶填的位址」。另外新客戶建立時把原廠那格 AI 設定整格照抄，每家客戶手上都有一份原廠金鑰副本 |
| **元件端點位置** | 設定寫入 `PUT /system/config/{group}/{key}`（`AI_PROVIDER_CONFIG`）；觸發分類 `POST /evidence-batches/{batch_uid}/classify`（套件 jedi-evidence-classification）；組連線 `core/plugins/evidence_classification.py:container_env()`；解析 `app/system_config/service/ai_provider_key_resolver.py`；新客戶抄設定 `app/system_config/service/tenant_storage_config_seeder.py:seed_from_root()`；開關 `common/constant/ai_service_config.py` |
| **駭客怎麼打** | ① 開關放開後，客戶管理員打開「AI 服務設定」；② 把 OpenAI 的服務位址填成自己的機器、金鑰欄留空；③ 上傳一批證據按「自動分類」；④ 系統找不到客戶的金鑰就拿原廠的，位址卻用客戶填的；⑤ 原廠金鑰放在授權標頭送到他的機器上 |
| **得手什麼** | 原廠的 AI 服務金鑰 |
| **修正的做法** | ① **位址跟著金鑰走**：新增 `resolve_credentials(tenant_id, provider)` 一次回傳「金鑰＋位址」，位址只取自金鑰所在的同一層，那層沒填就用官方預設；金鑰來自環境變數時位址只認原廠那格，客戶那格的位址永遠不能搭原廠金鑰；證據分類組連線改用它；② **新客戶不抄金鑰**：抄 AI 設定前先拿掉每家的金鑰欄位，拿完變空的整格不抄；③ **開關說明改寫**成「放開前確認兩道防線仍在」，不再寫「改一下即可」（前端同名說明另在前端 commit `f2a4680`）；④ 新增 6 條測試，突變測試確認改回舊規則會紅 |
| **狀態** | ✅ 已修，1.21.0 出貨（CM-2192，commit `21874f869`）。打折處 commit 已註明：沒真的跑一次分類容器，以組出的環境變數判定 |

## M16-6 ⚪ 登入憑證簽章金鑰、資料庫密碼、儲存金鑰同在一個測試設定檔 {#m16-6}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #122；STRIDE：冒充身分、權限提升；另命中 A04 加密、A02 設定） |
| **在哪裡** | 通知模組檢視時查到：同一個測試設定檔 `.env.test` 裡還有三樣——簽發登入憑證的金鑰、資料庫密碼、檔案儲存服務金鑰 |
| **攻擊面位置** | **拿得到程式庫的人**，而且只影響我們自己的開發環境：安裝程式每裝一套都會各自隨機產生新的一份，這三樣不會跟著出貨 |
| **信任邊界位置** | **版控 ↔ 祕密存放處**。登入憑證的簽章金鑰是「誰簽的憑證系統就信」的那一把，它一跨進版控，拿到的人就能自己簽任何人的身分 |
| **元件端點位置** | 外流檔案 `.env.test`（已撤出版控，commit `5746cef10`）；金鑰用途 `JWT_SECRET_KEY`（登入憑證簽章）；安裝時隨機產生在 `scripts/installer/install.sh`（`gen_key`） |
| **駭客怎麼打** | ① 拿到程式庫，打開測試設定檔；② 用簽章金鑰自己簽一張「我是管理員」的登入憑證；③ 帶著它打開發環境的任何 API，不需要密碼也不需要雙因子；④ 資料庫密碼與儲存金鑰則可直接讀寫開發環境資料 |
| **得手什麼** | 開發環境的任意身分、資料庫與證據檔讀寫權 |
| **修正的做法** | ① 測試設定檔撤出版控、移除 `.gitignore` 例外（commit `5746cef10`）；② 換發：模組頁記三樣都已撤銷換發，但 CM-2048 commit 註明簽章金鑰屬開發機專用、安裝時每套重新產生、**不需換發**（同 [M17-6](#m17-6) 的裁定）；③ 版控殘留字串清除 |
| **狀態** | ✅ 已修正（CM-2048，commit `e8e134a4e`；撤出版控 `5746cef10`） |

## M17-6 ⚪ 登入憑證簽章金鑰明文躺在對話紀錄裡 {#m17-6}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #122，與 M16-6 同一把；STRIDE：冒充身分、權限提升） |
| **在哪裡** | 公告模組檢視時查到：簽發登入憑證用的金鑰明文出現在版控的對話紀錄裡（34 處） |
| **攻擊面位置** | **拿得到程式庫的人**；影響範圍只有我們的開發環境，因為安裝程式每套各自隨機產生一把新的 |
| **信任邊界位置** | **版控 ↔ 祕密存放處**。同 M16-6。這條也是決策者定下判嚴重度原則的案例：先問「客戶端用的是不是同一把」，不是先問「這把還活著嗎」或「散在幾個檔」 |
| **元件端點位置** | 外流位置：`docs/conversation-history/` 下 30 個檔；金鑰用途 `JWT_SECRET_KEY`；安裝時隨機產生 `scripts/installer/install.sh`（`gen_key`） |
| **駭客怎麼打** | ① 拿到程式庫，在對話紀錄裡找到簽章金鑰；② 自己簽任何人、任何客戶、任何角色的登入憑證；③ 拿去打開發環境 |
| **得手什麼** | 開發環境裡任意身分 |
| **修正的做法** | ① 30 個對話紀錄檔清除金鑰字串，純字串清理、無程式改動；② 決策者裁定開發環境那把不需更換（每套安裝各自隨機產生，不會出貨）；③ 同批另清出 47 檔已過期的登入憑證字串（commit `8497acee3`） |
| **狀態** | ✅ 已修正（CM-2048，commit `e8e134a4e`） |

## 總表#137 ⚪ 系統自動產生的密碼比規定少一個字元 {#summary-137}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #137；STRIDE：冒充身分） |
| **在哪裡** | 登入與權限模組：系統替使用者自動產生初始密碼的工具（批次匯入帳號、新增帳號時沒給密碼）。模組頁無逐條表，依總表描述與修正 commit 撰寫 |
| **攻擊面位置** | 不是攻擊者主動打的面。影響的是「系統自產密碼合不合自家密碼政策」：政策要求至少 12 碼，產生器給 11 碼 |
| **信任邊界位置** | **系統自產的值 ↔ 自家密碼政策**。系統自己產的密碼本該必然合規；產生器保證三類字元後，補字數時仍減 4（早期含符號共 4 個，拿掉符號時減數沒跟著改），於是永遠短一碼 |
| **元件端點位置** | 套件 `jedi_iam/common/utils/common_util.py:generate_complex_password()`；呼叫端 `app/service/user_batch_import_service.py`（`POST /user/batch-upload`）、`app/service/user_service.py`、`domain/service/user_domain_service.py` |
| **駭客怎麼打** | ① 無需攻擊者；② 管理員用 Excel 匯入一批帳號，沒附密碼；③ 系統替每人產生 11 碼密碼，比政策少一碼；④ 驗密碼政策時被擋，錯誤訊息卻指向「使用者輸入不合規」，讓人以為是範本填錯 |
| **得手什麼** | 不是誰得手，而是自產密碼強度低於自家規定、匯入失敗且難查 |
| **修正的做法** | ① 補字數改用 `length - len(password)`，不再寫死減數，日後增減保證字元自動跟上；② 實測修正前產 200 次全部不合規、修正後 500 次全部 12 碼且零不合規；③ 程式內留一行陷阱註解說明錯誤訊息為什麼會誤導；④ 主專案另有一份零呼叫者、帶同樣缺陷的複本，直接刪除（commit `ce410ffb3`），問卷與日誌套件裡的同名複本也刪掉（套件 commit `0f0baf9c`，CM-2068） |
| **狀態** | ✅ 已修（套件 jedi-iam commit `edc9118d`；主專案死複本刪除 `ce410ffb3`） |

## M04-6 ⚪ 主系統連快取伺服器時不確認對方是誰 {#m04-6}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #139；STRIDE：資料外洩、冒充身分。模組頁評中） |
| **在哪裡** | 共用地基檢視時查到：主系統連內部快取伺服器（暫存登入狀態、驗證碼、AI 聊天紀錄）時，加密連線寫死成不驗對方憑證 |
| **攻擊面位置** | **已經進到這台主機或容器內部網路的人**，而且部署要開了快取加密（預設沒開）。那條連線跑在同一台主機的容器內部網路，不對外 |
| **信任邊界位置** | **主系統 ↔ 快取伺服器**。這條線在容器內部；要攔截它得先進主機，而到了那一步直接讀設定檔就有密碼，攔線沒有意義——這是裁定不修的理由 |
| **元件端點位置** | 原始位置：主專案 `common/util/redis_client_util.py` 的 `ssl_cert_reqs=False`（呼叫端 `infra/survey/adapters.py`、`core/plugins/ai_bot.py`）。目前仍在的同類連線：`config/config.py` 的 `REDIS_URL` 寫死 `redis://`、不理會 `REDIS_SSL`，由 `core/extensions.py` 的 `FlaskRedis` 在 `core/app_factory.py` 使用 |
| **駭客怎麼打** | ① 先進到跑產品的那台主機或它的容器網路；② 冒充快取伺服器、攔下主系統連過來的加密連線；③ 拿到快取帳密與經過的登入狀態、驗證碼——但同一個位置直接讀設定檔就有帳密，這一步多餘 |
| **得手什麼** | 快取帳密與暫存的登入狀態（攻擊者在這個位置本來就拿得到） |
| **修正的做法** | **裁定不修**（理由見信任邊界欄）。實查程式碼補充：寫死不驗的那支主專案複本，已在處理登入與權限模組同類問題時整支刪除，兩個呼叫點改用套件裡開加密就強制驗憑證與主機名稱的版本（CM-2043，commit `d61d5342d`；套件側修法 CM-1565，jedi-iam commit `4c7579c6`）；仍未動的是 `REDIS_URL` 那條，連加密都沒開，屬同一裁定範圍 |
| **狀態** | 🚫 裁定不修（不驗憑證那支複本已隨 CM-2043 刪除；`REDIS_URL` 那條維持現狀） |
