---
title: A02 安全設定錯誤
eyebrow: Guidant AI 資安檢視總報告 · OWASP Top 10（2025）
h1: A02 安全設定錯誤（Security Misconfiguration）
lede: 命中這一類的 **25 件**，還原成模組頁的 **30 條原始條目**。每一條用白話寫出：在哪裡、攻擊面、信任邊界、元件端點、駭客怎麼打、得手什麼、修正的做法。
chips:
  - { text: "🟠 10", kind: warn }
  - { text: "🟡 13", kind: accent }
  - { text: "⚪ 7", kind: plain }
  - { text: "已修 25／拆除 1／不修 4", kind: ok }
---

## 這一類是什麼

**程式本身沒寫錯，是設定、預設值或擺放位置錯了**——系統、程式或雲端服務的設定在安全上不正確；沒有一套可重複的強化流程時，風險更高。OWASP 官方涵蓋的代表弱點有：設定錯誤（CWE-16）、密碼放在設定檔（CWE-260）、除錯程式碼沒關（CWE-489）、環境變數洩漏資訊（CWE-526）。

這次掃描的 30 條，在本專案長成四種形狀：

- **鑰匙放在別人拿得到的地方**（15 條）：密碼、金鑰、通行證寫進版控文件、測試設定檔、腳本的預設值、派工資料表、啟動指令參數，或從查詢設定的功能直接回給畫面。拿得到程式庫或資料庫備份的人，不用破解任何東西就有一把能用的鑰匙。
- **預設值往寬鬆那邊倒**（8 條）：沒有身分就當最高權限、設定打錯字就退回開發設定、紀錄詳細度寫死最高、監控連線一載入就自動開、上傳資料夾預設公開、隔離規則寫好了卻沒打開開關、雲端資料夾預設「任何人都能編輯」、日誌保存期限的排程沒接上。
- **客戶主機管理員改一個設定，保護就關掉**（2 條）：防竄改要核對的目錄、商務授權的總開關，都只是一個環境變數。
- **開發與內部用的東西跟著出貨**（5 條）：開發小工具、三個環境並列的授權公鑰、每套相同的原廠帳號、沒有加密選項的日誌轉送、鎖定畫面顯示的識別碼。

共同特徵是：**每一條單獨看都「只是設定」，但被打下來時往往直接拿到整把鑰匙**，而且出事前沒有任何錯誤訊息。

> **為什麼是 30 條不是 25 件**：[總結](../SUMMARY.html)問題總表把「同一組密碼、同一批檔案」被不同模組各撈到一次的併成一件（例如 #12 是同一組資料庫管理員密碼，被意見回饋、寄信通知、公告三個模組各記一條）。本頁拆回模組頁原始條目，一條都不合併。標題括號內是總表編號，可回去對照。

::: {.callout .warn}
**展開時對照修正 commit，發現兩處與模組頁狀態寫法不一致，照實寫在條目裡**

- **「密碼已換發」**：寄信通知 M16-7、M16-8 與公告 M17-8、M17-11 的狀態欄寫「密碼已換發」，但清理 commit `e8e134a4e` 明寫「依決策者裁示：資料庫管理密碼與系統管理員密碼本輪不換發，待正式環境上版前統一處理」，與意見回饋 M23-1 的說法一致。本頁這四條照 commit 寫「版控殘留已清、換發排在正式上版前」。
- **M22-2 的「讀取端補權限檢查」**：密鑰走的是「一律不回傳」（CM-2063）；寄信、帳號目錄、登入政策三組的讀取已補總部層級守門（CM-2401）。其他各客戶自己一份的設定（儲存位址、帳號、桶名）讀取入口仍只驗登入。
:::

## 條目清單

| # | 條目 | 嚴重度 | 出自 | 狀態 |
|---:|---|---|---|---|
| 1 | [M15-5 外部程式碼平台的存取通行證四把寫在版控文件裡](#m15-5) | 🟠 高 | [AI 儀表板](../M15-ai-dashboard.html) | ✅ 已修 |
| 2 | [M16-5 測試用設定檔被推上版控，裡面是四把真的 AI 服務金鑰](#m16-5) | 🟠 高 | [寄信與通知](../M16-notification.html) | ✅ 已修 |
| 3 | [M17-9 四家 AI 服務金鑰完整躺在九個對話紀錄檔裡](#m17-9) | 🟠 高 | [公告](../M17-bulletin.html) | ✅ 已修 |
| 4 | [M23-1 資料庫管理員密碼散在 249 個檔案裡](#m23-1) | 🟠 高 | [意見回饋](../M23-issue.html) | ✅ 已修 |
| 5 | [M16-7 產生文件的腳本寫死資料庫連線資訊（含密碼）](#m16-7) | 🟠 高 | [寄信與通知](../M16-notification.html) | ✅ 已修 |
| 6 | [M17-8 可繞過客戶隔離的資料庫管理帳號密碼寫在文件裡](#m17-8) | 🟠 高 | [公告](../M17-bulletin.html) | ✅ 已修 |
| 7 | [M02-5 任何登入者查一支設定功能就拿到雲端儲存帳密明文](#m02-5) | 🟠 高 | [檔案上傳下載](../M02-file-upload.html) | ✅ 已修 |
| 8 | [M22-2 查設定的三個入口只驗登入，吐出儲存帳密](#m22-2) | 🟠 高 | [系統設定](../M22-system-core.html) | ✅ 已修 |
| 9 | [M20-1 13 張標有客戶歸屬的資料表，隔離沒有真正生效](#m20-1) | 🟠 高 | [客戶資料隔離](../M20-tenant-isolation.html) | ✅ 已修 |
| 10 | [M04-2 沒有登入身分時，客戶資料隔離整個關掉](#m04-2) | 🟠 高 | [共用基礎](../M04-common.html) | ✅ 已修 |
| 11 | [M03-9 開始掃描時，解密後的主機帳密多存一份明文進派工表](#m03-9) | 🟡 中 | [弱點檢測整合](../M03-detection.html) | ✅ 已修 |
| 12 | [M16-8 管理員密碼寫死在三支腳本，兩支「變數沒設就用真密碼」](#m16-8) | 🟡 中 | [寄信與通知](../M16-notification.html) | ✅ 已修 |
| 13 | [M17-11 資料庫調整腳本「變數沒設就用這個密碼」](#m17-11) | 🟡 中 | [公告](../M17-bulletin.html) | ✅ 已修 |
| 14 | [M23-3 Google 雲端硬碟應用程式密鑰與加密金鑰散在 15 個檔](#m23-3) | 🟡 中 | [意見回饋](../M23-issue.html) | ✅ 已修 |
| 15 | [M04-8 環境設定值打錯字，悄悄退回開發用紀錄設定](#m04-8) | 🟡 中 | [共用基礎](../M04-common.html) | ✅ 已修 |
| 16 | [M08-3 一個設定能把「要核對哪個目錄」整個換掉](#m08-3) | 🟡 中 | [防竄改檢查](../M08-integrity.html) | ✅ 已修 |
| 17 | [M09-3 日誌轉送只有兩個選項、兩個都是明文](#m09-3) | 🟡 中 | [系統日誌](../M09-log.html) | ✅ 已修 |
| 18 | [M17-7 安裝程式每套都種下同一組最高權限帳號](#m17-7) | 🟡 中 | [公告](../M17-bulletin.html) | 🚫 裁定不修 |
| 19 | [M18-7 三個環境共用同一份公鑰清單](#m18-7) | 🟡 中 | [授權管理](../M18-license.html) | 🚫 已裁記錄不修 |
| 20 | [M09-5 日誌按月分區失效，保存期限沒生效](#m09-5) | 🟡 中 | [系統日誌](../M09-log.html) | ✅ 已修 |
| 21 | [M18-12 落地版商務授權總開關只是一個設定值](#m18-12) | 🟡 中 | [授權管理](../M18-license.html) | ✅ 已修 |
| 22 | [M24-3 上傳檔案資料夾開在 `/static/` 公開路徑下](#m24-3) | 🟡 中 | [主系統自己的程式](../M24-host.html) | ✅ 已修 |
| 23 | [M24-8 雲端硬碟資料夾設成「知道連結的任何人都能編輯」](#m24-8) | 🟡 中 | [主系統自己的程式](../M24-host.html) | 🚫 裁定不修 |
| 24 | [M04-11 開發用小工具混在正式出貨的套件裡](#m04-11) | ⚪ 低 | [共用基礎](../M04-common.html) | 🗑️ 已拆除 |
| 25 | [M16-6 測試設定檔裡還有簽章金鑰、資料庫密碼、儲存金鑰](#m16-6) | ⚪ 低 | [寄信與通知](../M16-notification.html) | ✅ 已修 |
| 26 | [M17-6 簽發登入憑證的金鑰明文躺在對話紀錄裡（34 處）](#m17-6) | ⚪ 低 | [公告](../M17-bulletin.html) | ✅ 已修 |
| 27 | [M04-9 兩種紀錄的詳細程度被寫死成最詳細](#m04-9) | ⚪ 低 | [共用基礎](../M04-common.html) | ✅ 已修 |
| 28 | [M07-7 AI 服務金鑰放在啟動指令的參數上](#m07-7) | ⚪ 低 | [證據自動分類](../M07-evidence-classification.html) | ✅ 已修 |
| 29 | [M04-10 程式一載入就自動對外連一條不加密的監控連線](#m04-10) | ⚪ 低 | [共用基礎](../M04-common.html) | ✅ 已修 |
| 30 | [M08-5 鎖定畫面把機器指紋與事件編號顯示給任何人](#m08-5) | ⚪ 低 | [防竄改檢查](../M08-integrity.html) | 🚫 裁定不修 |

---

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

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #10；STRIDE：資料外洩、冒充身分） |
| **在哪裡** | AI 儀表板模組檢視時順手做的密碼掃描撈到的（與儀表板功能無關）：公司程式碼託管平台（GitLab）的個人存取通行證，明文寫在開發對話紀錄與文件站搜尋索引裡 |
| **攻擊面位置** | 主專案程式碼倉庫的版本紀錄。**任何讀得到程式庫歷史的人**（在職同仁、外包、離職者、拿到備份的人）都構成攻擊面，不需要系統帳號；這是設計上就不該存在的一面 |
| **信任邊界位置** | **「讀得到程式碼的人」↔「以公司身分操作程式碼平台」**。通行證應只存在各人機器的憑證管理裡；當時被當成一般文字貼進對話紀錄、跟著歸檔進版控，邊界從文件這一側被打穿 |
| **元件端點位置** | 外流處：`docs/conversation-history/2026-04-28-to-04-30-survey-answer-arc/part-verbatim-03-of-03.md`、`docs/conversation-history/2026-05-13-spec2-phase-e-decision/part-01-of-01-phase-e-defer-and-phase-f-kickoff.md`、`docs/features-site/site/search/search_index.json`（文件站產物）。被打穿的服務：公司 GitLab 的 API（通行證可呼叫的專案與問題單端點），不是本產品的端點 |
| **駭客怎麼打** | ① 拿到程式庫的人在歷史紀錄裡搜「通行證」的固定前綴字樣；② 把撈到的字串放進請求標頭呼叫程式碼平台的 API；③ 平台認的是通行證、不認人，直接以我們公司同仁的身分回應；④ 讀取或改寫平台上的專案原始碼、問題單，甚至推一段程式碼進分支 |
| **得手什麼** | 以公司身分讀寫程式碼平台上的專案與問題單 |
| **修正的做法** | ① **撤銷**：四把通行證已在平台後台全數撤銷，舊值失效（屬平台操作，不在 commit 裡）；② **清出版控**：CM-2048 把三個檔裡的真實通行證改成遮蔽字樣（比卡片列的多抓到一個位置），另一份本來就是假範例的值保留不動；③ 全 repo 以通行證樣式交叉搜尋，確認清除後 0 命中 |
| **狀態** | ✅ 已修（CM-2048，commit `e8e134a4e`）。歷史 commit 仍有舊值，因已撤銷不改寫歷史 |

## M16-5 🟠 測試用設定檔被推上版控，裡面是四把真的 AI 服務金鑰 {#m16-5}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #11；STRIDE：資料外洩） |
| **在哪裡** | 寄信通知模組檢視時順手做的密碼掃描撈到的（與寄信無關）：主專案根目錄一支測試用設定檔，裝著四家對外 AI 服務（Anthropic、OpenAI、Google、LangSmith）的真金鑰 |
| **攻擊面位置** | 主專案程式碼倉庫。**任何拿得到程式庫的人**都構成攻擊面，不需要系統帳號、也不需要連得到我們任何一台機器——AI 服務在公網上 |
| **信任邊界位置** | **「讀得到程式碼的人」↔「以公司帳號呼叫外部 AI 服務」**。金鑰應只放在不進版控的 `.env`；那支測試設定檔從一開始就不是範本，卻被當範本提交、帶著真金鑰待了六個月 |
| **元件端點位置** | 外流處：主專案根目錄 `.env.test`（已刪）。被打穿的服務：四家 AI 服務商的公網 API；其中 LangSmith 那把能讀服務端存的執行紀錄 |
| **駭客怎麼打** | ① 拿到程式庫的人打開那支測試設定檔；② 把四把金鑰各自設進自己的程式；③ 服務商只認金鑰，費用記在我們公司帳上；④ 用 LangSmith 那把列出服務端保存的歷史執行紀錄——裡面例行含有送去 AI 的客戶資料 |
| **得手什麼** | 用公司帳號燒錢，並讀走含客戶資料的 AI 執行紀錄 |
| **修正的做法** | ① **撤出版控**：`.env.test` 刪除，改提交 `.env.test.sample`（只有鍵名、沒有值），`.gitignore` 補上規則，開發者安裝說明與產生說明文件的腳本同步改成「複製 sample 再自填」（commit `5746cef10`）；② **撤銷換發**：四把金鑰已在各服務商後台撤銷重發（屬平台操作）；③ **清殘留**：CM-2048 清掉 9 個對話紀錄檔裡的同一批金鑰字串，全 repo 交叉搜尋 0 命中 |
| **狀態** | ✅ 已修（撤出版控 commit `5746cef10`；殘留清理 CM-2048，commit `e8e134a4e`）。與 M17-9 同一批，總表只算一次 |

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

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #11；STRIDE：資料外洩）。模組頁評 🟡 中，總表與 M16-5 併成一件後依高的那條 |
| **在哪裡** | 公告模組檢視時順手做的密碼掃描撈到的（與公告無關）：M16-5 那支測試設定檔的四把金鑰，在開發過程中被貼進對話，歸檔成九個對話紀錄檔 |
| **攻擊面位置** | 主專案程式碼倉庫的 `docs/conversation-history/`。任何讀得到程式碼的人都構成攻擊面，不需要系統帳號；就算來源設定檔刪了，這九份仍在 |
| **信任邊界位置** | **「讀得到程式碼的人」↔「以公司帳號呼叫外部 AI 服務」**。對話紀錄歸檔前沒有遮蔽這一步，金鑰從開發者終端機直接越界進版控 |
| **元件端點位置** | 外流處：`docs/conversation-history/` 下九個對話紀錄檔（CM-2048 commit 的異動清單可查）。被打穿的服務：同 M16-5，四家 AI 服務商公網 API |
| **駭客怎麼打** | ① 拿到程式庫的人在對話紀錄資料夾搜各家金鑰的固定前綴；② 撈出完整金鑰；③ 照 M16-5 的方式呼叫服務、讀 LangSmith 上的歷史執行紀錄；④ 就算有人只刪了來源設定檔，這條路照樣通 |
| **得手什麼** | 同 M16-5：用公司帳號燒錢、讀走含客戶資料的歷史執行紀錄 |
| **修正的做法** | ① 金鑰已撤銷換發（與 M16-5 同一次）；② CM-2048 清掉九個對話紀錄檔的金鑰字串，改成遮蔽字樣；③ 全 repo 交叉搜尋 0 命中，並抽查被批次清理的檔案確認上下文沒有被誤傷 |
| **狀態** | ✅ 已修（CM-2048，commit `e8e134a4e`） |

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

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #12；STRIDE：資料外洩、權限提升） |
| **在哪裡** | 意見回饋模組檢視時撈到的（全 repo 範圍）：資料庫管理帳號（可繞過客戶隔離的那個）的密碼，寫在 249 個進版控的檔裡——大多是對話紀錄、維運腳本、遷移計畫；其中一份把主機、帳號、密碼湊成一條可直接貼上執行的連線指令 |
| **攻擊面位置** | 主專案程式碼倉庫。任何讀得到程式碼的人都構成攻擊面；要真的用上，還需要連得到公司內網的資料庫主機。檢視當時這組密碼通行四處：開發庫、**出貨基線庫**、兩座標示退役但還活著的舊庫 |
| **信任邊界位置** | **「讀得到程式碼的人」↔「資料庫管理員」**。這個帳號設計上就繞過資料庫的客戶隔離，是整套系統權限最高的一層；密碼應只在 `.env` 與部署文件，當時散在文件與對話裡 |
| **元件端點位置** | 外流處：`docs/conversation-history/`（112 檔）、`docs/features/`（67）、`docs/features-site/`（65）、`docs/issues/`、`docs/claude/`、`docs/analysis/2026-05-28-poc-db-migration-plan.md`（那份可直接貼上執行的連線指令）、`docs/system-design/scripts/generate_db_schema_docx.py`（唯一會被執行的腳本）。被打穿的服務：PostgreSQL 以 `cmmgr` 帳號連線（開發庫本機 5432、基線庫 188:25432） |
| **駭客怎麼打** | ① 拿到程式庫的人打開那份遷移計畫，整段連線指令複製下來；② 在連得到公司內網的位置執行，以管理員身分連進資料庫；③ 這個帳號不受客戶隔離規則限制，每一家客戶的資料都可讀可寫；④ 連進出貨基線庫改一筆預設資料，往後每一套裝出去的系統都會帶著它 |
| **得手什麼** | 全部客戶資料的讀寫權，以及動到往後每一套安裝檔的能力 |
| **修正的做法** | ① **先堵住再寫進去的路**：產生資料表文件的腳本改從環境變數 `DB_SCHEMA_DOCX_DB_PASSWORD` 讀密碼，沒設就報錯，不退回任何寫死的值；② **清掉已寫進去的**：250 個檔的密碼字串換成「請查 .env 或部署文件」，那份完整連線指令優先處理，兩支記憶檔改寫成指引文字；③ 文件站用產生工具整站重建（含搜尋索引），確保鏡像也乾淨；④ 全 repo（含網站產物）搜尋 0 命中；⑤ **換密碼**：決策者裁定排在正式環境上版前統一做，不重寫版本歷史 |
| **狀態** | ✅ 已修（CM-2049，commit `5d4c14221`）——殘留已清、寫入路徑已堵；密碼換發排在正式上版前 |

## M16-7 🟠 產生文件的腳本寫死資料庫連線資訊（含密碼） {#m16-7}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #12；STRIDE：資料外洩、權限提升）。模組頁評 🟡 中，總表與 M23-1 併成一件後依高的那條 |
| **在哪裡** | 寄信通知模組檢視時撈到的（與寄信無關）：一支產生「資料庫結構說明文件」的腳本，把客戶展示環境的主機、帳號、密碼直接寫在程式碼裡；它是那 249 個檔裡**唯一一支會被執行的** |
| **攻擊面位置** | 主專案程式碼倉庫。任何讀得到程式碼的人都構成攻擊面；要用上還需連得到展示環境的資料庫。展示環境依規定等同正式環境 |
| **信任邊界位置** | **「讀得到程式碼的人」↔「展示環境資料庫」**。連線資訊應由執行的人在自己的環境提供；腳本把它寫死，等於把連線權交給每個讀程式碼的人 |
| **元件端點位置** | `docs/system-design/scripts/generate_db_schema_docx.py`（連線那一段，`psycopg2.connect(...)`）。被打穿的服務：當時展示環境的 PostgreSQL（2026-08-29 展示環境改成安裝版後已改用獨立密碼） |
| **駭客怎麼打** | ① 讀程式碼的人打開這支腳本；② 照抄連線參數，或乾脆直接執行它；③ 連進客戶會看到的展示環境資料庫；④ 讀或改展示資料，下次對客戶 demo 時就是被動過的內容 |
| **得手什麼** | 客戶看得到的展示環境資料庫的讀寫權 |
| **修正的做法** | ① 腳本改成從環境變數 `DB_SCHEMA_DOCX_DB_PASSWORD` 讀密碼、連線帳號改用受隔離的 `cm_app`，沒設環境變數就拋錯中止，不退回寫死值；② 已用獨立腳本驗證沒設時會拋錯、程式碼裡不再有密碼；③ 版控殘留字串隨 M23-1 的 250 檔一起清掉 |
| **狀態** | ✅ 已修（CM-2049，commit `5d4c14221`；模組頁標的 CM-2048 `e8e134a4e` 是同批的另一部分）。展示環境已改安裝版、密碼與外流的那組不同 |

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

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #12；STRIDE：資料外洩、權限提升）。模組頁評 🟡 中，總表與 M23-1 併成一件後依高的那條 |
| **在哪裡** | 公告模組檢視時順手做的密碼掃描撈到的（與公告無關）：同 M23-1 那組資料庫管理帳號密碼，這一條特指寫在開發文件（記憶檔、系統設計文件、功能文件）裡的那幾份 |
| **攻擊面位置** | 主專案程式碼倉庫，任何讀得到程式碼的人；要用上需連得到內網資料庫 |
| **信任邊界位置** | **「讀得到程式碼的人」↔「資料庫管理員」**，同 M23-1。出貨基線庫是所有安裝檔的來源，這條邊界的另一側不只是開發資料，還有往後每一家客戶 |
| **元件端點位置** | 外流處：`docs/claude/memory/`（4 檔）、`docs/system-design/`（2 檔）、`docs/features/` 與 `docs/conversation-history/`（約 101 檔）。被打穿的服務：開發庫與 188:25432 基線庫的 `cmmgr` 帳號 |
| **駭客怎麼打** | ① 讀程式碼的人打開開發記憶檔或系統設計文件；② 照抄帳號密碼；③ 連進開發庫或出貨基線庫，繞過所有客戶隔離；④ 在基線庫動手腳，跟著安裝檔進客戶機房 |
| **得手什麼** | 開發庫全部資料，以及出貨基線庫的改寫權 |
| **修正的做法** | ① CM-2048 清掉記憶檔、系統設計文件、功能文件與對話紀錄裡的字串，改標「密碼請查 .env 或部署文件」；② CM-2049 再把其餘 250 檔清完並堵住腳本寫死的路（見 M23-1）；③ **換密碼**：依清理 commit 所載，決策者裁示本輪不換、排在正式環境上版前統一做（模組頁「密碼已換發」與 commit 不符，以 commit 為準） |
| **狀態** | ✅ 已修（CM-2048，commit `e8e134a4e`；CM-2049，commit `5d4c14221`）——殘留已清；換發排在正式上版前 |

## M02-5 🟠 任何登入者查一支設定功能就拿到雲端儲存帳密明文 {#m02-5}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #14；STRIDE：資料外洩、竄改資料） |
| **在哪裡** | 檔案上傳下載模組：「儲存設定」頁背後讀系統設定的功能，回傳的設定內容裡帶著物件儲存（MinIO／SeaweedFS）的存取密鑰原文 |
| **攻擊面位置** | 已登入使用者可打的系統設定 API。**任何最低權限的帳號**都構成攻擊面，不必是管理員。設定表本身有客戶隔離，拿到的是自己那家的帳密——但**落地版一客戶一套，那一組就是那家的全部** |
| **信任邊界位置** | **後端 ↔ 瀏覽器**。存取密鑰是後端連儲存空間用的，應該停在後端，不該越過這條線進到瀏覽器；當時兩份遮蔽名單（哪幾類設定要遮、哪些欄位名要遮）都漏了儲存設定，密鑰原樣回給畫面 |
| **元件端點位置** | `GET /api/1.0/system/config/{group}/{key}`（宿主 `api/system_config/routes/system_config_route.py` 的 `SystemConfigGroupRoute`）、`GET /api/1.0/system/configs/{group}`、`GET /api/1.0/system/config/{uid}`（套件 `jedi_system_core/api/routes/system_config_route.py`）；三條都經 `app/system_config/service/guarded_system_config_service.py` 的 `GuardedSystemConfigService`；後端真正用密鑰連線的是 `app/upload_file/service/managed_file_upload_service.py` |
| **駭客怎麼打** | ① 任一能登入的員工；② 打一次 `GET …/system/config/STORAGE_CONFIG/CONFIG`；③ 回應裡的 `secret_key`／`minio_secret_key` 就是明文；④ 拿這組帳密用標準 S3 工具直連儲存空間，把所有上傳檔案列出、下載、覆蓋、刪除——完全繞過系統裡所有權限檢查 |
| **得手什麼** | 整座檔案儲存空間的讀寫刪權限 |
| **修正的做法** | 照「五步、順序不能換」施工：① **先加安全網**：存檔時密鑰欄位沒帶或留空，就沿用既有值、絕不覆蓋成空；前端送回來的遮罩旗標 `has_secret` 一律剝掉不落地；② **前端改遮罩顯示**：儲存設定頁密鑰欄顯示 `••••••••`，沒改就不送（FE commit `756783b`）；③ **後端預設不回真值**：`GuardedSystemConfigService._mask_storage` 掛進單一遮罩分派點 `_mask_secrets`，儲存設定讀出去一律拿掉兩個密鑰鍵、換成 `has_secret: true/false`——不是往名單加字，是預設就不回；④ **背景工作另開一支**：上傳服務要真值連線，DI 另註冊 `storage_backend_config_service`（`reveal_storage_secret=True`）只注入上傳服務，對外那支永遠遮；⑤ 套件側遮罩名單也補上 `STORAGE_CONFIG` 與兩個鍵名，當第二層（CM-2054） |
| **狀態** | ✅ 已修（CM-2063，BE commit `7c904eb6e`、FE commit `756783b`；名單第二層 CM-2054，套件 commit `16a3e41c`）。驗證：相關測試 94 passed，遮罩／沿用／帶新值／列不存在四種情形實測 |

## M22-2 🟠 查設定的三個入口只驗登入，吐出儲存帳密 {#m22-2}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #14；STRIDE：資料外洩、竄改資料） |
| **在哪裡** | 系統設定模組：讀取系統設定的三個入口，同一支檔案裡的新增、修改、刪除都有權限檢查，讀取卻只檢查有沒有登入 |
| **攻擊面位置** | 已登入使用者可打的系統設定 API，任何帳號都構成攻擊面，**不必是管理員、不必有任何權限、不必知道任何編號**。同一條路還會吐出寄信伺服器與員工帳號目錄的位址、埠號、登入帳號 |
| **信任邊界位置** | **API 層 ↔ 服務層的權限檢查**。寫入路徑在 route 上用 `assert_config_capability(group, action)` 依設定類別查能力點；讀取路徑沒有這一道，檔案自己的說明還寫著「讀取只要登入即可」 |
| **元件端點位置** | `GET /api/1.0/system/config/{group}/{key}`（宿主 `api/system_config/routes/system_config_route.py` `SystemConfigGroupRoute.get`，只有 `@jwt_required()`）、`GET /api/1.0/system/configs/{group}` 與 `GET /api/1.0/system/config/{uid}`（套件 `jedi_system_core/api/routes/system_config_route.py` 的 `SystemConfigListRoute`／`SystemConfigDetailRoute`，只有 `@auth_required`）；守門函式 `core/plugins/system_core.py:assert_config_capability` |
| **駭客怎麼打** | ① 任一能登入的帳號（例如沒有任何設定權限的稽核員）；② 依序打三個讀取入口，填儲存、寄信、帳號目錄等設定類別名；③ 系統只看有沒有登入就回整列設定；④ 修正前拿到儲存密鑰明文，可直連儲存空間；寄信與帳號目錄的位址與帳號則是對內釣魚、挑攻擊目標的材料 |
| **得手什麼** | 修正前：儲存空間的鑰匙；現在：儲存位址、帳號、桶名與寄信／帳號目錄的連線資訊 |
| **修正的做法** | ① **實際採用「密鑰一律不回傳」收口**（CM-2063，見 M02-5）：三個入口共用 `GuardedSystemConfigService` 的單一遮罩點，誰來問都拿不到密鑰真值；② AI 服務與雲端硬碟兩組另有「只限平台管理員」的讀取守門（`_assert_restricted_group_access`）；③ 寄信、帳號目錄、登入政策三組的讀取補上總部層級守門（CM-2401）：`GuardedSystemConfigService` 三條讀取路徑非總部一律 403、清單查詢濾掉這三組，安全政策頁讀取同樣補上——寄信與帳號目錄的連線資訊只剩總部與平台管理員讀得到；④ 其他各客戶自己一份的設定，讀取入口仍只有 `@jwt_required()`，沒有能力點檢查 |
| **狀態** | ✅ 已修（密鑰：CM-2063，commit `7c904eb6e`；三組共用設定的讀取：CM-2401，BE `779837919`、FE `afb45a2`）。其他設定讀取入口的能力點檢查未補，屬殘留 |

## M20-1 🟠 13 張標有客戶歸屬的表隔離沒生效，看得到別家資料 {#m20-1}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #24；STRIDE：資料外洩） |
| **在哪裡** | 客戶資料隔離：13 張標有客戶歸屬欄位的業務資料表 |
| **攻擊面位置** | 任一家客戶的登入使用者，透過任何會讀到這些表的正常功能。不需要特殊操作 |
| **信任邊界位置** | **租戶 ↔ 租戶**（資料庫隔離牆）。客戶歸屬欄位都在，牆卻沒立：3 張規則寫好但開關沒開、1 張規則不完整、9 張連規則都沒有 |
| **元件端點位置** | 資料表 `compliance.projects`、`tenant_drive_integrations`、`job_executions`、`information_systems`、`poams`、`evidence_classification_runs`、`evidence_classification_ground_truth`、`framework_parse_jobs`、`user_roles`、`user_tenants`、`workflow_executions`、`workflow_templates`、`log_forwarding_settings` 的隔離規則；migration 在 `scripts/sql/2026-09-14-fr094-cm17*.sql` |
| **駭客怎麼打** | ① 任一家客戶的使用者；② 照常打開專案清單、待辦、任務執行紀錄；③ 資料庫對這些表沒有隔離，查詢結果不分客戶；④ 畫面上混進別家客戶的專案、任務與稽核資料（當時實測：一把指向不存在客戶的鑰匙查得到 213 筆專案、11,065 筆任務執行紀錄） |
| **得手什麼** | 別家客戶的專案、待辦、任務執行與稽核資料 |
| **修正的做法** | 分組處理：① 只需開開關的兩張（專案、雲端硬碟連動），專案先補「平台管理員」分支再開（CM-1768）；② 先補規則再開的八張，套四條標準規則（CM-1769、CM-1770）；③ 工作流程執行紀錄補查詢規則、拿掉部門條件（CM-1771）；④ 流程範本先回填歸屬再開（CM-1772）；⑤ 日誌轉送設定重新歸類為平台層設定，靠寫入權限收到只給平台管理員（見 M09-1）。重測四個數字全部歸零 |
| **狀態** | ✅ 已修（1.21.0 出貨，12 張已補；BE `0ca61108c`／`197951723`／`b5ca22bd1`／`b502b0b47` 等 FR-094 系列） |

## M04-2 🟠 沒有登入身分的路徑被當成最高權限，一次看到全部客戶 {#m04-2}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #27；STRIDE：權限提升、資料外洩） |
| **在哪裡** | 共用基礎：每次開資料庫交易前設定「現在是誰在查」的那段 |
| **攻擊面位置** | 不需要登入身分的入口：登入流程本身、雲端硬碟通知回呼、任何漏掛認證的路由或沒還原身分的背景執行緒。部分入口設計上就對外暴露 |
| **信任邊界位置** | **應用程式 ↔ 資料庫**。資料庫的隔離規則靠應用程式告訴它「誰在查」；當時查不到身分就自動設成最高權限，等於邊界在「缺值」時整個打開 |
| **元件端點位置** | `jedi_common/session/database/db.py` 的 `session_scope()`；`jedi_common/session/auth/system_context.py`、`tenant_context.py`；主專案 `core/scheduler.py`、雲端硬碟 webhook／OAuth 回呼服務 |
| **駭客怎麼打** | ① 從不需要登入身分的入口進來，例如雲端硬碟通知回呼；② 這條路徑沒有使用者身分；③ 系統把「沒身分」當成最高權限的系統管理員，對資料庫宣告「看全部」；④ 那段程式的每一次查詢與寫入都橫跨所有客戶，任何小錯都從影響一家放大成影響全部 |
| **得手什麼** | 繞過客戶隔離、橫跨全部客戶的讀寫面 |
| **修正的做法** | ① 新增具名系統身分 `system_context(name)`，排程要跨客戶必須明說並留下具名警告（CM-1767）；② 拿掉「租戶欄位空值＝超級管理員」，簽章短票改填真實身分（CM-1787）；③ 完全沒有身分時改成什麼都看不到，並留一行警告指出是哪裡沒帶身分（CM-1788）；④ 雲端硬碟回呼等機器端點改用具名「租戶身分」（只看得到該租戶）；⑤ 九支排程全部接上具名身分，守衛測試涵蓋 |
| **狀態** | ✅ 已修（FR-094：套件 `2bd93918`／`9a0b4893`／`fc1cadb9`，BE `3af065133`／`1125ceee1`／`41e367f85`） |

## M03-9 🟡 開始掃描時把解密後的主機帳密多存一份明文 {#m03-9}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #70；STRIDE：資料外洩） |
| **在哪裡** | 弱點檢測整合模組：按下「開始掃描」、「立即重新指派」或「重跑」時，系統替每台目標主機開一張派工單 |
| **攻擊面位置** | 資料庫本身——需要拿到**資料庫備份或任一個唯讀帳號**。這一面不是對外開放的，但備份、維運帳號、診斷匯出都可能流出去 |
| **信任邊界位置** | **程式記憶體內 ↔ 資料庫落地**。客戶主機帳密在資料庫裡本來是加密存的，只在派工那一刻解開。解開後的明文應該只留在記憶體裡用完就丟；當時被順手塞進派工單的參數欄位寫回資料庫，跨過了「明文不落地」這條線 |
| **元件端點位置** | `POST /api/1.0/detection-tools/jobs/{job_uid}/execute`、`…/execution-groups/{group_uid}/assignments/{assignment_uid}/start-now`、`…/rerun` → `jedi_detection/app/service/detection_orchestration_service.py` 的 `_dispatch_one()`，寫入 `agent_tasks.params`；代理程式實際拿帳密走的是主專案 `infra/remote_agent/adapter/detection_task_payload_provider.py` 的 `_resolve_tool_credentials()` |
| **駭客怎麼打** | ① 取得一份資料庫備份，或一個只能讀的資料庫帳號；② 查派工資料表的參數欄位；③ 每一次掃描都留下一份客戶正式機房的主機密碼與掃描器管理員權杖，而且這張表沒有清理機制，越掃越多；④ 拿這些帳密直接登入客戶的主機 |
| **得手什麼** | 客戶正式機房的可用主機密碼與掃描器管理員權杖 |
| **修正的做法** | ① **刪掉 `_dispatch_one()` 裡寫入明文的那一行**（`params["_credentials"] = creds`）——查證過那份從來沒有任何程式讀過，代理程式拿到的是心跳時另一條路當場重新解密的；② 正常派工與「立即重新指派／重跑」共用同一個函式，一處刪除兩條路都修好；③ 查 DEV 既有殘留為 0 筆，不需補清理腳本；④ 主專案測試同步把「參數裡有帳密」的斷言改成「不該有」 |
| **狀態** | ✅ 已修（CM-2050／FR-114.4-3，套件 commit `e95465eb`，主專案測試 `1f70c5a3c`）。驗證：套件 106 測試、主專案相關 153 測試全過 |

## 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`） |

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

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #74；STRIDE：資料外洩） |
| **在哪裡** | 意見回饋模組檢視時順手做的密碼掃描：開發過程留下的對話紀錄裡，夾著 Google 雲端硬碟整合用的應用程式密鑰、通行證加密金鑰、應用程式編號 |
| **攻擊面位置** | 程式碼倉庫——**任何讀得到程式碼的人**（員工、外包、日後拿到原始碼的人）。要真正用上加密金鑰，還得另外拿到資料庫裡的密文 |
| **信任邊界位置** | **主機設定檔 ↔ 版本控制**。這兩樣東西應該只住在主機的設定檔與密碼庫裡，被開發時的對話紀錄帶進了版本控制。應用程式密鑰是 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`（應用程式憑證解析） |
| **駭客怎麼打** | ① 拿到程式碼倉庫的讀取權；② 搜尋對話紀錄，抄出加密金鑰與應用程式密鑰；③ 再配合任何一條拿到資料庫的路（例如資料庫密碼外流那一條），把資料庫裡加密存的客戶雲端硬碟通行證解開；④ 用通行證讀客戶雲端硬碟裡的檔案 |
| **得手什麼** | 客戶存在雲端硬碟裡的稽核證據檔案 |
| **修正的做法** | ① **清掉版控殘留**：以現行設定值對全庫逐字比對，15 個檔的三個值換成「見 .env 或部署文件」佔位字串，改完三值各自再搜一次確認零命中；② 不動金鑰本身、不動程式。**金鑰重發與「既有通行證重新加密」在 FR-114 裁定另開一張卡（D-b4-3），本頁撰寫時查不到這張卡的修正 commit**。現況佐證：安裝程式每裝一套會各自隨機產生加密金鑰（`scripts/installer/install.sh` 的 `gen_key`），應用程式密鑰也已改成可在系統設定裡逐套填寫（FR-110.1），外流的那一組只影響我們自己的環境 |
| **狀態** | ✅ 已修（CM-2051，commit `ccbb27148`）——15 檔殘留字串已清；金鑰重發與既有通行證重新加密另案，**本頁查無對應修正 commit** |

## M04-8 🟡 環境設定值打錯字時悄悄退回開發用的紀錄設定 {#m04-8}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #84；STRIDE：資料外洩） |
| **在哪裡** | 共用基礎模組：服務啟動時讀「現在是什麼環境」（`RUN_ENV`）來決定日誌怎麼記 |
| **攻擊面位置** | 不是外部能打的面：要能改主機上的部署設定才碰得到。真正會發生的是**客戶或維運手誤**，例如把 `prod` 寫成 `production` |
| **信任邊界位置** | **部署設定 ↔ 程式**。程式讀到認不得的值時，應該倒向嚴格的那一邊；當時倒向的是開發設定——多開「把日誌寫進資料庫」那條路、掛在七種紀錄類別上，等於打錯字讓客戶機變寬鬆 |
| **元件端點位置** | `jedi-common/jedi_common/logger/config_logger.py`：`dictConfig(_CONFIGS.get(RUN_ENV, …))` 的退路；兩套設定在同目錄 `config_dev.py`／`config_prod.py`。出貨端設定在 `docker/production/docker-compose.yml`（`RUN_ENV: ${RUN_ENV:-prod}`）與 `scripts/installer/install.sh` |
| **駭客怎麼打** | ①（多半是手誤，不是駭客）客戶機房的維運改部署設定，把環境值打成 `production`；② 服務照常啟動，程式認不得這個值；③ 自動退回開發用的日誌設定，開發才該有的紀錄路徑全開；④ 沒有任何錯誤或警告，誰都不知道這台機器的紀錄方式已經跟其他客戶不一樣 |
| **得手什麼** | 客戶機在不知情下改用開發用的紀錄設定，多記、多寫進資料庫 |
| **修正的做法** | ① 認不得的設定值改退到**正式設定**，不是開發設定；② **刻意不報錯、不擋啟動**——決策者原話「不能因為設定錯誤就不讓啟動，客服會瘋掉」，把打錯字的後果從「悄悄變寬鬆」反過來變成「悄悄變嚴格」；③ 動手前先查出貨端兩處（docker-compose 兩支服務、安裝程式）都已顯式寫 `prod`，確認不再依賴這個預設值；④ 完全沒設的情況維持開發，那只會發生在開發機 |
| **狀態** | ✅ 已修（CM-2065／FR-114.5C-4，套件 commit `a9562dd0`，1.21.0 出貨） |

## M08-3 🟡 一個設定就能換掉「要核對哪個目錄」 {#m08-3}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #90；STRIDE：竄改資料） |
| **在哪裡** | 防竄改檢查模組：開機逐檔核對時「以哪個目錄為準」 |
| **攻擊面位置** | 客戶主機上服務的啟動設定（環境變數）。需要**主機管理權限**，能改容器或服務的啟動環境 |
| **信任邊界位置** | **可被改的啟動設定 ↔ 防竄改的執法對象**。資源根目錄原本先看環境變數 `GUIDANT_RESOURCE_ROOT`、再看是不是正式打包版——開發用的覆寫開關優先權比「正式版」還高，執法對象可以被設定整個換走 |
| **元件端點位置** | `common/util/resource_path.py`：`resource_root()`、`is_packaged()`；開機閘門 `jedi_integrity/app/startup_gate.py`：`run_startup_gate()` 依此目錄核對 |
| **駭客怎麼打** | ① 有主機權限的人；② 單用這一招不成立——指到空目錄會因找不到清單而拒絕開機；③ 搭配 M08-1：準備一個自己的目錄放假清單，把環境變數指過去；④ 攻擊成本從「偽造一整份清單蓋住真目錄」降成「隨便準備一個目錄」 |
| **得手什麼** | M08-1 的放大器：讓防竄改核對一個攻擊者自備的目錄 |
| **修正的做法** | ① `resource_root()` 改成打包模式一律用執行檔所在目錄，完全不理環境變數；② 環境變數只在開發（源碼）模式生效；③ 出貨映像雖設了 `GUIDANT_RESOURCE_ROOT=/app`，但執行檔就在 `/app`，行為不變 |
| **狀態** | ✅ 已修，1.21.0 出貨（CM-2053，BE commit `0dda326b8`）。驗證：源碼模式環境變數生效、模擬打包時被忽略 |

## M09-3 🟡 日誌轉送只有兩個選項，兩個都是明文 {#m09-3}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #92；STRIDE：資料外洩） |
| **在哪裡** | 系統日誌模組：管理員在「日誌轉送」設定頁選傳送方式，把系統日誌送到客戶自己的監控系統 |
| **攻擊面位置** | 我們後端到客戶監控系統之間的**網路路徑**。攻擊者需要能看封包（同機房網段、被入侵的網路設備），不需要我們系統的帳號 |
| **信任邊界位置** | **我們後端 ↔ 客戶的監控系統**。日誌裡有帳號、操作、稽核事件，跨出後端這條線時應該能加密。缺口不在「沒加密」（明文轉送是業界常態），而在**沒給選擇**——兩種格式、兩種傳送方式，四種組合全是明文 |
| **元件端點位置** | `GET`／`PUT /api/1.0/log-forwarding`（`LogForwardingSettingRoute`）、`POST /api/1.0/log-forwarding/test`（`LogForwardingTestRoute`），路由表在 `jedi-log/jedi_api_log/forwarding/api/routing.py`；送出在 `forwarding/common/forwarder.py`、`gelf_handler.py`、`tls.py`；前端 `src/views/log-forwarding/LogForwardingForm.vue` |
| **駭客怎麼打** | ① 攻擊者在客戶機房同一個網段裡，或控制了一台中間的網路設備；② 側錄後端送往監控系統的封包；③ 每一筆日誌都是明文，直接讀到誰在什麼時間做了什麼稽核操作；④ 客戶就算自己的監控系統收得了加密，設定頁上也沒有可開的選項 |
| **得手什麼** | 整份系統日誌內容（帳號、操作、稽核事件） |
| **修正的做法** | ① **加一個獨立的「傳輸加密」開關**（`use_tls`），而不是多一種傳送方式——既有設定一律是關、行為不變；只有 TCP 能開，服務層與資料庫約束兩邊都擋；② **憑證驗證恆開、沒有「不驗」選項**：自簽憑證的客戶改貼信任憑證（`tls_ca_cert`），仍然驗證，只是換信任來源；③ **兩條送出路都做**：syslog 新增 `TlsSysLogHandler`，覆寫的是建連線那一步而不是送出那一步——斷線重連也會走同一步，攔錯位置會讓重連後悄悄退回明文；GELF 連上後才包加密，交握失敗往上報、絕不退回明文重試；④ 加密設定納入「設定有變就重掛」的判斷，否則畫面顯示已加密、線上仍是明文；⑤ 讀取時只回「有沒有貼憑證」，不回憑證內容 |
| **狀態** | ✅ 已修（CM-2056，套件 commit `984f06d1`，主專案 migration `38c5b131e`，前端 `eb220b0`）。驗證：自簽憑證起真的加密收件端實跑四種情形 |

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

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

## M18-7 🟡 三個環境的公鑰並列在同一份清單 {#m18-7}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #99；STRIDE：冒充身分、竄改資料） |
| **在哪裡** | 授權管理模組：產品驗授權檔簽章時用的公鑰清單，直接編在出貨的程式裡 |
| **攻擊面位置** | 授權檔上傳（需要平台管理員或已登入帳號），但真正的前提是**先拿到開發、測試或展示任一台簽發站的私鑰**——那幾把目前都在我們自己的機器上 |
| **信任邊界位置** | **我們的簽發站 ↔ 客戶端產品**。客戶端應該只認正式簽發站簽的檔；驗章是「看檔上的代號、去清單裡找對應公鑰」，而清單把三個環境的公鑰並列——任何一個環境簽的檔，其他環境都認。這原本是為了換鑰匙時新舊並存，被拿來並列三個不同環境，兩件事混在同一個機制裡 |
| **元件端點位置** | `POST /api/1.0/license/upload`、`POST /api/1.0/license/activation/upload`、`POST /api/1.0/license/activation/online`（路由表 `jedi_license_runtime/api/routing.py`）→ `jedi_license_runtime/common/engine.py` 的 `verify_payload()`／`_lookup_public_key()`，清單在 `jedi_license_runtime/common/public_keys.py` 的 `PUBLIC_KEYS`（三筆 kid） |
| **駭客怎麼打** | ① 取得開發環境簽發站的私鑰（例如從某位開發者的機器）；② 用它簽一張授權檔，內容寫成任意方案、任意期限；③ 上傳到正式環境的產品；④ 產品按代號在清單裡找到開發環境那把公鑰，驗章通過，照單生效 |
| **得手什麼** | 自己簽給自己的授權：任意方案、任意期限 |
| **修正的做法** | **裁定不修**（不是遺漏，是判斷過的取捨）：三把公鑰對應開發、測試、展示三台內部簽發站，私鑰都在內部管控；目前沒有正式簽發站，要成立得先拿到我們機器上的私鑰，那時問題早已不只授權。**失效條件已寫明**：正式簽發站建好、開始對外簽發後，這就是一條真正的繞過路徑——**交付首位客戶前，出貨版改成只認正式簽發站的公鑰**（與正式環境簽章鑰一起處理）。檢視時順手修好的一件事：打包流程裡「檢查公鑰有沒有放對」的守門，檢查的檔案位置早已不存在、每次都被跳過，已修正 |
| **狀態** | 🚫 裁定不修（已裁記錄；**交付首位客戶前必做**） |

## M09-5 🟡 日誌表按月分區失效，保存期限沒生效 {#m09-5}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #106；STRIDE：資料外洩） |
| **在哪裡** | 日誌模組：操作日誌與系統日誌兩張表按月分開存放，靠一支維護程式每月建新月份、清掉過期月份（操作日誌留 90 天、系統日誌留 180 天） |
| **攻擊面位置** | 不是攻擊面，是保存政策失效：該刪的日誌留著，留得越久，能讀到資料庫的人可以翻的歷史越長，而那段期間的日誌裡還有密碼原文 |
| **信任邊界位置** | **應用程式 ↔ 資料庫（保存期限）**。維護程式應該由排程定期呼叫，並以有權限建表的身分執行；當時從來沒有排程呼叫它，而且它設定成「用呼叫者身分執行」，應用程式的連線帳號沒有建表權限，接上也會每晚失敗 |
| **元件端點位置** | 資料庫函式 `public.maintain_log_partitions()`（原始定義在 `scripts/sql/2026-06-03-log-tables-partitioning.sql`）；資料表 `api_logs`、`system_logs` 及其 `_default` 備用分區；排程在 `core/scheduler.py:_log_partition_maintenance_tick` |
| **駭客怎麼打** | ① 無需攻擊者——分區從沒被建，新資料全堆進備用分區；② 90／180 天到期時沒有任何東西被清掉，也沒有任何錯誤訊息；③ 日後任何讀得到資料庫的人，能翻到早該刪掉、還含密碼原文的舊日誌 |
| **得手什麼** | 早該刪除的舊日誌（當時含密碼原文） |
| **修正的做法** | ① 排程加一支每日 03:30（UTC）的工作呼叫 `maintain_log_partitions()`；② 函式改成以擁有者身分執行（`ALTER FUNCTION … SECURITY DEFINER`），並把搜尋路徑釘死在 `public`，避免呼叫者建同名物件劫持函式內容（那會變提權）；這支隨出貨走，客戶機器同樣適用；③ 開發環境另一支只在開發庫跑的搬移腳本：把已堆在備用分區的當月資料搬回正確月份，用搬移不是刪除（那些是稽核紀錄）；④ 出貨基線重產，新裝機直接拿到正確版本（commit `8cc99148a`） |
| **狀態** | ✅ 已修（CM-1923，BE commit `68d35e602`；基線 `8cc99148a`）。驗證：開發環境實查本月與未來兩個月分區已建、備用表清空歸零 |

## M18-12 🟡 落地版商務授權總開關只是一個設定值 {#m18-12}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #202；STRIDE：權限提升） |
| **在哪裡** | 授權管理模組：落地版（裝在客戶機器上的版本）判斷「這家客戶買了哪些模組、授權過期要不要轉唯讀、子公司數量上限」的總開關 |
| **攻擊面位置** | 客戶機器上的環境設定檔 `.env`。需要**客戶那台主機的管理權限**——對落地版來說，這個人就是客戶自己的維運人員，設計上本來就拿得到 |
| **信任邊界位置** | **原廠的商務規則 ↔ 客戶的主機管理員**。授權檔有原廠簽章、防竄改會核對程式檔，但開關是一個環境變數，**防竄改不核對設定值**——簽章保護的那條線，從設定檔這一側被繞過 |
| **元件端點位置** | 唯一判斷點主專案 `common/authz/license.py` 的 `enforcement_enabled()`／`readonly_gate_enabled()`（四個執法路徑與唯讀守門 `common/middleware/license_readonly_mw.py:init_license_readonly_gate` 全經這兩支）；開關本身是 `config/config.py` 的 `LICENSE_ENFORCEMENT_ENABLED`、`LICENSE_READONLY_GATE_ENABLED` |
| **駭客怎麼打** | ① 客戶的主機管理員打開 `.env`；② 把 `LICENSE_ENFORCEMENT_ENABLED` 改成 `false`、重啟；③ 系統讀到開關就整套放行，不留任何警示；④ 沒買的模組照用、授權過期照常寫資料、子公司數量上限失效 |
| **得手什麼** | 不付錢使用沒買的模組與過期授權（受損的是原廠商務收入，不是客戶資料） |
| **修正的做法** | ① 決策者裁定：**落地版拿掉這兩個開關、一律執法**；真的要暫時放行，改由原廠補發測試用授權。原廠自己管機器的 SaaS 保留開關當保險絲；② 兩支判斷函式在 `DEPLOYMENT_MODE=host` 時一律回 `True`、忽略設定，呼叫點不動；③ 新增 `disabled_switches_ignored_on_host()`，開機時對「落地版被設成 false、已被忽略」的開關逐一記警告（不擋啟動），診斷包同步回報「落地版固定執法」並留下 `attempted_disable_ignored` 痕跡給原廠；④ 安裝程式不再寫這兩行 |
| **狀態** | ✅ 已修（CM-2205，commit `c8aa7d026`，1.21.0 出貨）。驗證：14 條單元測試＋突變；DEV 實測落地版關開關仍回 403、SaaS 關開關回 200 |

## M24-3 🟡 上傳的檔案整個資料夾開在網站公開路徑下 {#m24-3}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #203；STRIDE：資料外洩） |
| **在哪裡** | 主系統：Flask 預設替 `static` 資料夾註冊的公開路徑 |
| **攻擊面位置** | 不需帳號，直連後端埠即可。出貨的網站前門只放行兩條路徑擋著，有人為排錯開了後端埠或換代理設定就整包公開 |
| **信任邊界位置** | **網際網路 ↔ 後端**。所有下載都該走要登入的功能，框架卻預設多開了一條不驗身分的靜態檔路徑 |
| **元件端點位置** | 主專案 `core/app_factory.py`（Flask 建構參數）；路徑 `GET /static/{path}` |
| **駭客怎麼打** | ① 不需帳號；② 找到直接開出來的後端埠；③ 打 `/static/` 加上問卷答案附件、意見回饋附件或匯出檔的路徑；④ 檔案直接下來 |
| **得手什麼** | 使用者上傳的附件與匯出檔 |
| **修正的做法** | ① 建構時 `static_folder=None` 不註冊路由（卡上寫的 `static_url_path=None` 實測在 Flask 3.1 會退回預設、擋不住）；② 建構後再把資料夾路徑指回原處，讓問卷備份等寫檔功能照舊拿得到；③ 先確認全專案沒有地方組 `/static/` 網址 |
| **狀態** | ✅ 已修（8-I，CM-2219，BE `782ebb8b3`，1.21.0 出貨） |

## M24-8 🟡 雲端硬碟資料夾設成「知道連結誰都能編輯」 {#m24-8}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #205；STRIDE：資料外洩、竄改資料） |
| **在哪裡** | 主系統雲端硬碟整合：建立專案資料夾時設定的分享權限 |
| **攻擊面位置** | 公司任一登入帳號（查得到根資料夾編號）；拿到連結的任何人 |
| **信任邊界位置** | **本系統 ↔ Google 雲端硬碟**。資料夾權限設成「知道連結的任何人可編輯」，權限邊界交給連結保密 |
| **元件端點位置** | 主專案 `infra/cloud_integration/google_drive/google_drive_api_client.py` 的 `share_anyone_writer`（呼叫端 `create_folder_handler.py`、`archive_drive_file_handler.py`、`drive_sync_orchestration_service.py`）；根資料夾編號經 `GET /api/1.0/integrations/google-drive` 回傳 |
| **駭客怎麼打** | ① 公司任一登入帳號；② 從雲端硬碟整合資訊拿到根資料夾編號；③ 組成連結打開；④ 看、改、刪全公司的證據檔，被改的檔下一輪同步還會被當正常證據匯入 |
| **得手什麼** | 全公司的雲端證據檔讀寫權 |
| **修正的做法** | 裁定不修（決策者 10-01）：雲端硬碟的分享權限未來由權限管理控管，或交由客戶在自己的 Google 設定處理，產品不另外管 |
| **狀態** | 🚫 裁定不修 |

## M04-11 ⚪ 開發用小工具混在正式出貨的套件裡 {#m04-11}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #116；STRIDE：資料外洩） |
| **在哪裡** | 共用基礎套件：一支讓 AI 幫程式碼自動寫註解的開發小工具，檔頭自己寫明「不是產品程式碼」，卻跟著套件一起打包、裝到客戶機器上 |
| **攻擊面位置** | 客戶機器上的安裝檔內容。需要**主機或安裝包的存取權**；它沒有對外的網址、沒有被設成可執行指令，產品本身不會呼叫它 |
| **信任邊界位置** | **開發環境 ↔ 出貨產物**。開發輔助工具應該停在開發機；打包時沒把它擋在外面，連帶它需要的大型選配元件也得特地寫程式排除 |
| **元件端點位置** | 套件 `jedi-common/jedi_common/utils/gen_comment.py`（已刪）與 `jedi-common/pyproject.toml` 的 `[ai]` 選配依賴宣告（已刪）。五個程式庫全部零引用 |
| **駭客怎麼打** | ① 拿到客戶機器或安裝包的人瀏覽套件內容；② 找到這支會呼叫外部 AI 服務的工具；③ 它本身不洩資料，但多一支能對外連線、沒人維護的程式，就多一個日後被拿來利用的落腳點，也讓稽核時多一項「出貨內容不乾淨」的發現 |
| **得手什麼** | 無直接資料外洩；是出貨面積多出來的一塊 |
| **修正的做法** | **整支刪除**：① 刪掉 `gen_comment.py`；② 連帶移除只有它用得到的 `[ai]` 選配依賴宣告與套件說明裡對它的介紹；③ 同一 commit 順手刪掉一個每次連線都設定、但全系統沒有任何規則讀它的死開關 `app.can_manage_orgs`（總表 #117），避免讓人誤以為已有部門層級檢查 |
| **狀態** | 🗑️ 已拆除（FR-114.3-3／CM-2045，套件 commit `599ab7c5`，1.21.0 出貨）。驗證：相關 41 條測試通過 |

## M16-6 ⚪ 測試設定檔裡的登入憑證簽發金鑰、資料庫密碼、儲存金鑰 {#m16-6}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #122，與 M17-6 併計；STRIDE：冒充身分、權限提升） |
| **在哪裡** | 寄信與通知模組檢視時順手做的密碼掃描：一份被推上版控的測試用設定檔裡，除了四把 AI 服務金鑰，還有簽發登入憑證的金鑰、資料庫密碼、檔案儲存服務的金鑰 |
| **攻擊面位置** | 程式碼倉庫——任何讀得到程式碼的人。真正能用上，還得連得到**我們自己的開發環境** |
| **信任邊界位置** | **主機設定檔 ↔ 版本控制**。三樣東西應該只在主機設定檔裡；那份檔被 `.gitignore` 的例外放行進了版控，立論是「不含真實憑證的範本」，實際上帶著真值待了六個月。**影響面被安裝程式擋住**：每裝一套都各自隨機產生新的一份，外流的只是開發機那一份 |
| **元件端點位置** | 外流檔：`.env.test`（已撤出版控）。使用端：`config/config.py` 的 `JWT_SECRET_KEY`（登入憑證簽章，另由 `jedi_iam/authz/signed_token.py` 派生短效票）、`DB_SECRET`、儲存設定；每套隨機產生在 `scripts/installer/install.sh`（`JWT_SECRET_KEY="$(gen_key)"`） |
| **駭客怎麼打** | ① 拿到程式碼倉庫的讀取權，翻出那份測試設定檔；② 用簽發金鑰自己做一張「系統管理員」的登入憑證，不需要密碼也不需要雙因子；③ 拿這張憑證打我們的開發環境，或直接用資料庫密碼、儲存金鑰讀寫資料；④ 換到客戶環境就無效——客戶那套的金鑰是安裝時另外產生的 |
| **得手什麼** | 我們自己開發環境的管理員身分與資料 |
| **修正的做法** | ① 那份設定檔撤出版控，移除 `.gitignore` 的放行例外，四把對外 AI 金鑰由決策者撤銷重發（`5746cef10`）；② 版控裡其他地方的殘留字串清掉，兩支「環境變數沒設就用寫死密碼」的腳本改成沒設就中止並提示（CM-2048）；③ 簽發金鑰依決策者裁定**不需更換**：只影響開發環境，安裝時每套各自隨機產生 |
| **狀態** | ✅ 已修正（CM-2048，commit `e8e134a4e`；設定檔撤版控 `5746cef10`） |

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

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #122，與 M16-6 併計；STRIDE：冒充身分、權限提升） |
| **在哪裡** | 公告模組檢視時順手做的密碼掃描：同一把簽發登入憑證的金鑰，散在開發對話紀錄裡 34 處 |
| **攻擊面位置** | 程式碼倉庫——任何讀得到程式碼的人；能用上的地方只有我們自己的開發環境 |
| **信任邊界位置** | **主機設定檔 ↔ 版本控制**。同 M16-6。這一條留下了一個判斷教訓：統籌者一度主張升高風險（還在用、散 34 處），決策者只問「安裝時是不是每套動態產生」——是，所以外流的不會跟著出貨 |
| **元件端點位置** | 外流位置：`docs/conversation-history/` 下約 30 個檔。使用端與產生端同 M16-6：`config/config.py` 的 `JWT_SECRET_KEY`、`scripts/installer/install.sh` 的 `gen_key` |
| **駭客怎麼打** | ① 拿到程式碼倉庫讀取權，搜尋對話紀錄抄出金鑰；② 自己簽一張任意使用者、任意角色的登入憑證；③ 拿去打我們的開發環境，不需密碼與雙因子；④ 對客戶環境無效 |
| **得手什麼** | 我們自己開發環境任何人的身分 |
| **修正的做法** | ① 對話紀錄裡的殘留字串全數清除（純字串清理、無程式改動）；② 之後同卡收尾時另清 47 檔、195 處早已過期的登入憑證字串（`8497acee3`）；③ 依決策者裁定開發環境那把**不換**，理由同 M16-6 |
| **狀態** | ✅ 已修正（CM-2048，commit `e8e134a4e`、收尾 `8497acee3`） |

## M04-9 ⚪ 兩種紀錄的詳細程度被寫死成最詳細 {#m04-9}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #125；STRIDE：讓服務停擺） |
| **在哪裡** | 共用基礎套件：正式環境的紀錄設定，把資料庫對映紀錄與 MongoDB 事件紀錄寫死成最詳細（DEBUG），輸出到畫面的那一層也寫死最詳細 |
| **攻擊面位置** | 不需要攻擊者——系統一開機就自己產生大量紀錄。任何會觸發大量查詢的正常操作都會放大它 |
| **信任邊界位置** | **程式 ↔ 主機磁碟**。紀錄量應由部署者用 `LOG_LEVEL` 控制；寫死最詳細等於把「要記多少」從部署者手上拿走，客戶機的磁碟由程式單方面決定怎麼用 |
| **元件端點位置** | 套件 `jedi_common/logger/config_prod.py` 的 `loggers` 段（`sqlalchemy.orm`、`pymongo.event_loggers`）與 `handlers.app.level`；選哪份設定由環境變數 `RUN_ENV` 決定（出貨固定為 `prod`） |
| **駭客怎麼打** | ① 不需要任何帳號；② 系統開機一分鐘就自己寫出約 6,000 行資料庫對映紀錄；③ 有人反覆操作大量查詢的功能，紀錄量跟著暴增；④ 客戶機磁碟被紀錄檔吃滿，資料庫與上傳跟著寫不進去 |
| **得手什麼** | 磁碟被吃滿、服務寫入失敗（無資料外洩，紀錄只寫檔案不進資料庫） |
| **修正的做法** | 分兩次收口：① **第一次**（CM-2065）把兩處由 DEBUG 調回 INFO；② **實測發現只修一半**：資料庫對映紀錄本身就是 INFO 級，INFO 擋不住（實機開機一小時 7,191 行中約 6,050 行是它）；③ **第二次**（CM-2270）`sqlalchemy.orm` 改成 WARNING、輸出到畫面那一層改成跟隨 `LOG_LEVEL`（上限 INFO），保留客戶設 `LOG_LEVEL=DEBUG` 排查的路；④ MongoDB 事件紀錄維持 INFO——全系統沒有掛任何監聽，INFO 下零輸出 |
| **狀態** | ✅ 已修（CM-2065，套件 commit `a9562dd0`；CM-2270，套件 commit `bf97b904`）。驗證：改後開機 stdout 由 6,096 行降到 51 行、資料庫對映紀錄 0 行 |

## M07-7 ⚪ AI 服務金鑰放在啟動指令的參數上 {#m07-7}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #130；STRIDE：資料外洩） |
| **在哪裡** | 證據自動分類模組：後端啟動分類程式時，把 AI 服務金鑰當成啟動指令的參數交過去 |
| **攻擊面位置** | **同一台主機上的任何帳號**——啟動指令的參數對主機上所有使用者都看得到。另外，執行逾時時整條指令會被帶進錯誤紀錄，再經日誌轉送與回廠診斷包流出 |
| **信任邊界位置** | **後端程序 ↔ 同主機的其他使用者**。金鑰應該只在後端與分類程式之間私下交接；放在命令列等於貼在主機的公布欄上。評低的前提：出貨給客戶的落地版當時沒有安裝分類程式所需的執行環境，客戶端打不到；洩出去的是客戶自備的金鑰，試用環境額度很低 |
| **元件端點位置** | `POST /api/1.0/evidence-batches/{batch_uid}/classify`（`jedi_evidence_classification/api/routing.py`）→ 開發機路徑 `jedi_evidence_classification/infra/classifier_container_runner.py` 的 `run()`；落地版路徑 `infra/classifier_service_runner.py` 的 `ClassifierServiceRunner`；主專案包裝在 `core/plugins/_evidence_classification_runner.py` |
| **駭客怎麼打** | ① 攻擊者拿到同一台主機上任一個普通帳號；② 在分類程式執行時列出主機上的程序清單；③ 啟動指令裡就寫著金鑰原文；④ 或者等一次分類逾時，從錯誤紀錄、日誌轉送、回廠診斷包裡撿到整條指令 |
| **得手什麼** | 客戶的 AI 服務金鑰（可以用客戶的額度呼叫 AI 服務） |
| **修正的做法** | ① **開發機**：金鑰改寫進權限 600、放在不會掛進容器的目錄、跑完（含出錯）即刪的暫存設定檔，指令只帶 `--env-file 路徑`；逾時與找不到程式兩處拋錯補 `from None`，錯誤紀錄不再串帶整條指令（CM-2193）；② **落地版**：改成常駐分類服務，後端經內部網路請它分類，金鑰放在那一次請求的內容裡、只交給那一次的分類程式；分類容器拿不到資料庫與快取的憑證（CM-2223）；③ 主專案順手補回廠診斷包的日誌遮罩認得「鍵名=值」形狀 |
| **狀態** | ✅ 已修，1.21.0 出貨（CM-2061 驗證；開發機 CM-2193 套件 commit `fdf8c3eb`，落地版 CM-2223 主專案 `750b635de`＋套件 `ca67d9b6`）。驗證：DEV 實跑真 docker 逾時，錯誤紀錄不含金鑰 |

## M04-10 ⚪ 程式一載入就自動連一條不加密的監控連線 {#m04-10}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #140；STRIDE：資料外洩） |
| **在哪裡** | 共用基礎模組：所有套件共用的日誌工具，一被載入就自動啟動遙測（追蹤與指標），往本機 4317 埠送 |
| **攻擊面位置** | 主機本機的那個埠。目前沒有任何地方接這條線，只是不斷無效重試；誰在那台主機上架起接收端，誰就收得到。需要主機上的存取權 |
| **信任邊界位置** | **我們的程式 ↔ 遙測接收端**。要不要送遙測、送去哪，應該由宿主在部署時明確決定；當時是「載入就送」的副作用，而且連線設定寫死 `insecure=True`（不加密） |
| **元件端點位置** | 套件 `jedi_common/logger/custom_formatter.py` 的 `configure_otel()`（`OTLPSpanExporter`／`OTLPMetricExporter` 帶 `insecure=True`）；主專案 `core/app_factory.py` 呼叫 `configure_logging(otel_endpoint=...)`，值來自 `config/config.py` 的 `OTEL_EXPORTER_OTLP_ENDPOINT` |
| **駭客怎麼打** | ① 攻擊者在那台主機上取得一個普通帳號；② 在本機 4317 埠架一個接收端；③ 我們的程式一啟動就自動連上來，明文送出追蹤資料與指標；④ 從中讀到請求路徑、執行時間等運作細節 |
| **得手什麼** | 系統的追蹤資料與運作指標 |
| **修正的做法** | ① 套件把遙測從「載入就啟動」改成 `configure_otel(endpoint)` **明確呼叫才啟動**，沒給位址直接不啟用；② 主專案以標準環境變數 `OTEL_EXPORTER_OTLP_ENDPOINT` 決定，**預設空＝不啟用**、不建任何連線；③ 處理了一個連帶地雷：日誌格式裡的追蹤欄位原本由遙測元件補上，關掉後每行日誌會靜默消失，故格式器自己補預設值。**連線本身仍是不加密的**——只是現在要有人主動設定才會走到 |
| **狀態** | ✅ 已修（CM-1627，套件 commit `7f61a535`，主專案 `fdfe6290d`）。驗證：DEV 啟動後無 4317 連線、程序內無遙測執行緒 |

## M08-5 ⚪ 鎖定畫面把機器指紋與事件編號顯示給任何人 {#m08-5}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #142；STRIDE：資料外洩） |
| **在哪裡** | 防竄改檢查模組：偵測到程式被改、機器被鎖住後，後端改起一個「鎖定畫面」服務，畫面上顯示機器指紋與鎖定事件編號，供客戶報修 |
| **攻擊面位置** | 後端原本對外的服務埠。**任何打得開這個系統網址的人**都看得到，包含沒登入的人——機器鎖住後也沒有登入可言。這一面設計上就必須對外：前端要收得到「已鎖定」的訊息，鎖定畫面才出得來 |
| **信任邊界位置** | **任何來訪者 ↔ 原廠解鎖窗口**。真正的關卡不在畫面，在「拿這兩個值去換解鎖檔」那一步——解鎖必須經原廠人工窗口確認是既有客戶窗口本人 |
| **元件端點位置** | 套件 `jedi_integrity/infra/lockdown.py`：`enter_lockdown()` 在 `config.lockdown_port`（＝原本服務的埠）起一個 `ThreadingHTTPServer`，任何路徑都回鎖定內容；JSON 版由 `_lockdown_data()`／`_render_json()` 帶 `machine_fingerprint`、`tamper_event_id`，網頁版由 `_render_html()` 顯示 |
| **駭客怎麼打** | ① 任何連得到系統網址的人打開首頁；② 看到鎖定畫面上的機器指紋與事件編號；③ 拿這兩個值去找原廠要解鎖檔；④ 原廠人工窗口認的是既有客戶窗口本人，對方答不出身分就拿不到——**這一步過不去** |
| **得手什麼** | 兩個本來就要給人看的報修識別碼；換不到解鎖檔 |
| **修正的做法** | **裁定不修**（決策者）：① 那兩個值本質就是報修時要報給原廠的識別碼，設計上要給人看；② 換解鎖檔必須通過原廠人工窗口，解鎖是極少數事件，來找的必然是既有客戶窗口，攻擊者拿得到識別碼、拿不到客戶窗口的身分；③ 原報告「正式部署只讓內網看得到」的評低理由經查不成立，已換成上述理由，等級維持低；④ 原報告兩個但書一併移除：「簽發端只憑指紋加編號就核發」已由人工窗口回答；「比對竄改回報紀錄」作廢——離線的落地版客戶送不出回報，會把最需要解鎖的人擋在外面 |
| **狀態** | 🚫 裁定不修（理由已改為「報修識別碼＋原廠人工窗口」，等級維持低） |
