---
title: I 資料外洩
eyebrow: Guidant AI 資安檢視總報告 · STRIDE 威脅分類
h1: I 資料外洩（Information Disclosure）
lede: 命中這一類的 **112 件**，還原成模組頁的 **123 條原始條目**逐條展開。每一條用白話寫出：在哪裡、攻擊面、信任邊界、元件端點、駭客怎麼打、得手什麼、修正的做法。
chips:
  - { text: "🔴 1", kind: crit }
  - { text: "🟠 29", kind: warn }
  - { text: "🟡 54", kind: accent }
  - { text: "⚪ 39", kind: plain }
  - { text: "已修 104／拆除 10／不修 8／SaaS 前 1", kind: ok }
---

## 這一類是什麼

**資訊被不該看到的人取得**——讀到沒被授權的檔案或資料，或資料在傳輸途中被讀走。STRIDE 六類裡它破壞的是「機密性」。判定時問的是：**攻擊者打下這一條之後，能不能看到不該看的資料或密碼？**（定義見[分類總覽](../SUMMARY-owasp-stride.html#stride-i)）

這是六類裡最大的一類，因為幾乎所有「越權」最後的傷害都是「看到了」。這次命中的條目在本專案長成六種形狀：

- **只驗登入、不驗歸屬**（M02-1、M05-1～7、M06-1、M06-5、M10-1～5、M10-4／M12-1、M11-1、M11-4、M11-14～17、M15-2、M15-3、M19-1、M20-3、M06-17、M06-20～22 等，佔大半）：系統只問「你有沒有登入」，沒問「這筆是不是你的」。駭客要做的常常只是換個編號、或送一個什麼條件都不帶的空白查詢。
- **資料庫那道牆缺了或寫反**（M20-1、M02-6、M04-2、M20-2、M03-7、M18-8、M12-7、M20-5）：應用層漏一處時本該由資料庫擋住別家客戶，但牆沒開、沒依據、規則方向寫反，或沒身分時乾脆整個打開。
- **密碼與金鑰流到不該去的地方**（M15-5、M16-5、M17-9、M23-1、M16-7、M17-8、M16-8、M17-11、M23-3、M23-6、M04-1、M04-3、M16-2、M07-14、M24-5、M24-6、M24-13、M08-6、M03-9、M07-7）：寫進版本控制、日誌、回廠診斷包、命令列或資料表——不必打系統，拿到那份檔案就有鑰匙。
- **回應夾帶不該送的欄位**（M02-5、M22-2、M22-3、M15-4、M04-12、M11-31、M11-22、M11-32、M05-12、M08-5）：畫面只顯示三欄，後端整包送出去；或錯誤訊息把伺服器路徑、資料表結構說給攻擊者聽。
- **傳輸路上沒加密或不認人**（M13-2、M16-3、M23-4、M09-3、M02-11、M04-6、M04-10）：站在網路中間的人可以讀走，或假扮成對方把帳密收下。
- **以為刪了其實還在**（M02-9、M07-6、M09-5）：使用者按了刪除，衍生檔、工作目錄、過期日誌還留在主機上。

> **STRIDE 與 OWASP 的關係**：OWASP 講「程式哪裡寫錯」，STRIDE 講「攻擊者能拿它做什麼」。同一條問題通常兩邊都命中，所以這一頁的條目會與 [OWASP 各頁](../OWASP-Top-10/A01-broken-access-control.html)重複出現，內容相同——兩邊各自完整，讀者從哪一邊進來都看得完。

> **為什麼是 123 條不是 112 件**：[總結](../SUMMARY.html)問題總表把「同一套修法、同一個洞」的條目併成一件（例如資料庫管理員密碼那件 #12 由意見回饋、通知、公告三個模組各撈到一次）；本頁拆回模組頁原始條目，一個出處一條。標題括號內是總表編號，可回去對照。另外總表 #71 與 #12 指向同一條原始條目 M16-7，所以本頁出現的總表編號是 111 個。

> **狀態欄帶 ⚠️ 的條目**：是回查修正 commit 時發現總表或模組頁記的卡號、做法與實際 commit 不一致，本頁以 commit 為準並註明出入。

## 條目清單

| # | 條目 | 嚴重度 | 出自 | 狀態 |
|---:|---|---|---|---|
| 1 | [M01-1 代理程式五條通訊管道全部不檢查對方是誰](#m01-1) | 🔴 最嚴重 | [遠端代理程式](../M01-remote-agent.html) | ✅ 已修 |
| 2 | [M02-1 拿到檔案編號就能下載別人的檔案](#m02-1) | 🟠 高 | [檔案上傳下載](../M02-file-upload.html) | ✅ 已修 |
| 3 | [M03-2 「測試連線」沒檢查權限，按下去就把維運帳密解密送到指定主機](#m03-2) | 🟠 高 | [弱點檢測整合](../M03-detection.html) | ✅ 已修 |
| 4 | [M05-1 查填答歷史明細不檢查歸屬，送空白請求就整包讀走](#m05-1) | 🟠 高 | [問卷](../M05-survey.html) | ✅ 已修 |
| 5 | [M05-2 讀某份問卷的答案不檢查這份問卷是不是你的](#m05-2) | 🟠 高 | [問卷](../M05-survey.html) | ✅ 已修 |
| 6 | [M06-1 知道一個任務編號就能讀走稽核證明清單](#m06-1) | 🟠 高 | [稽核流程](../M06-flow-engine.html) | ✅ 已修 |
| 7 | [M06-2 流程留言的讀與寫都只檢查有沒有登入](#m06-2) | 🟠 高 | [稽核流程](../M06-flow-engine.html) | ✅ 已修 |
| 8 | [M07-1 舊線「預覽證據檔」填任何雲端硬碟檔案編號就把檔案抓回來](#m07-1) | 🟠 高 | [證據自動分類](../M07-evidence-classification.html) | 🗑️ 已拆除 |
| 9 | [M15-5 程式碼平台通行證四把寫在版控文件裡](#m15-5) | 🟠 高 | [AI 儀表板](../M15-ai-dashboard.html) | ✅ 已修 |
| 10 | [M16-5 測試設定檔推上版控，帶著四把真的 AI 服務金鑰](#m16-5) | 🟠 高 | [寄信與通知](../M16-notification.html) | ✅ 已修 |
| 11 | [M17-9 四家 AI 服務金鑰躺在九個對話紀錄檔裡](#m17-9) | 🟠 高 | [公告](../M17-bulletin.html) | ✅ 已修 |
| 12 | [M23-1 資料庫管理員密碼散在 249 個檔案裡](#m23-1) | 🟠 高 | [意見回饋](../M23-issue.html) | ✅ 已修 |
| 13 | [M16-7 產生文件的腳本寫死展示環境資料庫連線與密碼](#m16-7) | 🟠 高 | [寄信與通知](../M16-notification.html) | ✅ 已修 |
| 14 | [M17-8 可繞過客戶隔離的資料庫管理帳號密碼寫在文件裡](#m17-8) | 🟠 高 | [公告](../M17-bulletin.html) | ✅ 已修 |
| 15 | [M02-5 任何登入者打一支查設定的功能，就拿到雲端儲存的帳號密碼明文](#m02-5) | 🟠 高 | [檔案上傳下載](../M02-file-upload.html) | ✅ 已修 |
| 16 | [M22-2 任何登入帳號打一支查設定的功能，就拿到檔案儲存的帳號密碼明文](#m22-2) | 🟠 高 | [系統設定](../M22-system-core.html) | ✅ 已修 |
| 17 | [M09-1 任何一家客戶的管理員都改得動全系統的日誌轉送去向](#m09-1) | 🟠 高 | [系統日誌](../M09-log.html) | ✅ 已修 |
| 18 | [M09-2 不必登入就能在操作日誌匯出檔裡種公式](#m09-2) | 🟠 高 | [系統日誌](../M09-log.html) | ✅ 已修 |
| 19 | [M21-1 意見回饋匯出檔可以被種公式](#m21-1) | 🟠 高 | [意見回饋重新檢視（跨模組專項）](../M21-issue-rescan.html) | ✅ 已修 |
| 20 | [M15-1 打字就能誘導 AI 儀表板選中任何一支查詢](#m15-1) | 🟠 高 | [AI 儀表板](../M15-ai-dashboard.html) | ✅ 已修 |
| 21 | [M20-1 13 張標有客戶歸屬的資料表，資料庫那道隔離牆沒有真正生效](#m20-1) | 🟠 高 | [客戶資料隔離（跨模組專項）](../M20-tenant-isolation.html) | ✅ 已修 |
| 22 | [M02-6 存放檔案紀錄的資料表沒有客戶歸屬欄位，資料庫擋不住跨客戶](#m02-6) | 🟠 高 | [檔案上傳下載](../M02-file-upload.html) | ✅ 已修 |
| 23 | [M04-1 登入密碼與通行證原文寫進紀錄檔與資料庫](#m04-1) | 🟠 高 | [共用基礎](../M04-common.html) | ✅ 已修 |
| 24 | [M04-2 沒有登入身分時，客戶資料隔離整個關掉](#m04-2) | 🟠 高 | [共用基礎](../M04-common.html) | ✅ 已修 |
| 25 | [M13-2 公司帳號登入：匿名能過、帳號字串能騙查詢、加密連線不認人](#m13-2) | 🟠 高 | [登入與權限](../M13-iam.html) | ✅ 已修 |
| 26 | [M20-2 4 個資料查詢畫面整個繞過隔離機制](#m20-2) | 🟠 高 | [客戶資料隔離（跨模組專項）](../M20-tenant-isolation.html) | ✅ 已修 |
| 27 | [M10-4 不是這個專案的人，換網址就看到別人專案的稽核判定與改善計畫](#m10-4) | 🟠 高 | [任務與成員管理](../M10-task-platform.html) | ✅ 已修 |
| 28 | [M12-1 同公司不是這個專案的人，換個網址就看得到稽核判定與改善計畫](#m12-1) | 🟠 高 | [稽核輪次管理](../M12-compliance-audit.html) | ✅ 已修 |
| 29 | [M11-1 知道系統安全計畫編號，就能整份下載成 Excel——而且跨客戶](#m11-1) | 🟠 高 | [合規文件核心](../M11-oscal.html) | ✅ 已修 |
| 30 | [M24-2 重建專案雲端資料夾不檢查專案是不是你的，背景還用系統身分載入](#m24-2) | 🟠 高 | [主系統自己的程式](../M24-host.html) | ✅ 已修 |
| 31 | [M05-6 還原歷史版本時不檢查指定的版本是不是這份問卷的](#m05-6) | 🟡 中 | [問卷](../M05-survey.html) | ✅ 已修 |
| 32 | [M19-1 設備與資訊系統清冊的讀取只檢查登入、不檢查權限](#m19-1) | 🟡 中 | [設備與資訊系統清冊](../M19-asset.html) | ✅ 已修 |
| 33 | [M03-12 查掃描執行紀錄少了「你是不是這個專案的人」這道檢查](#m03-12) | 🟡 中 | [弱點檢測整合](../M03-detection.html) | ✅ 已修 |
| 34 | [M05-3 列出任務問卷不限範圍，送空白請求就回整個客戶的全部](#m05-3) | 🟡 中 | [問卷](../M05-survey.html) | ✅ 已修 |
| 35 | [M05-4 列出填答歷史不限範圍](#m05-4) | 🟡 中 | [問卷](../M05-survey.html) | ✅ 已修 |
| 36 | [M05-5 問卷討論列表送空查詢就把全公司討論撈回來](#m05-5) | 🟡 中 | [問卷](../M05-survey.html) | ✅ 已修 |
| 37 | [M05-7 多人同時填問卷的「房間」想進哪間就進哪間](#m05-7) | 🟡 中 | [問卷](../M05-survey.html) | ✅ 已修 |
| 38 | [M06-5 知道一個稽核輪次編號就能讀走別人的階段歷程](#m06-5) | 🟡 中 | [稽核流程](../M06-flow-engine.html) | ✅ 已修 |
| 39 | [M06-6 「查目前在哪個階段」用的專案編號和輪次編號兩者不核對](#m06-6) | 🟡 中 | [稽核流程](../M06-flow-engine.html) | ✅ 已修 |
| 40 | [M10-1 對專案成員查詢送空白查詢，一次拿到全公司成員名冊](#m10-1) | 🟡 中 | [任務與成員管理](../M10-task-platform.html) | ✅ 已修 |
| 41 | [M10-2 換一個編號，就讀到別人專案群組層與控制項層的人員配置](#m10-2) | 🟡 中 | [任務與成員管理](../M10-task-platform.html) | ✅ 已修 |
| 42 | [M10-3 不是專案的人直接開專案摘要報告，讀到稽核結論與歷史版本](#m10-3) | 🟡 中 | [任務與成員管理](../M10-task-platform.html) | ✅ 已修 |
| 43 | [M10-5 對任務指派清單送空白查詢，一次拿到全公司一萬多筆指派紀錄](#m10-5) | 🟡 中 | [任務與成員管理](../M10-task-platform.html) | ✅ 已修 |
| 44 | [M10-9 底層「查詢條件全沒填就不過濾」，空白查詢等於回整張表](#m10-9) | 🟡 中 | [任務與成員管理](../M10-task-platform.html) | ✅ 已修 |
| 45 | [M15-2 AI 儀表板呼叫的 26 支查詢完全不檢查「你能不能看這筆資料」](#m15-2) | 🟡 中 | [AI 儀表板](../M15-ai-dashboard.html) | ✅ 已修 |
| 46 | [M15-3 AI 儀表板查專案清單時把呼叫者當成管理員](#m15-3) | 🟡 中 | [AI 儀表板](../M15-ai-dashboard.html) | ✅ 已修 |
| 47 | [M17-2 AI 儀表板這條路繞過守門直接讀公告](#m17-2) | 🟡 中 | [公告](../M17-bulletin.html) | ✅ 已修 |
| 48 | [M20-3 查任務詳細資料收了專案編號卻從頭到尾沒用](#m20-3) | 🟡 中 | [客戶資料隔離（跨模組專項）](../M20-tenant-isolation.html) | ✅ 已修 |
| 49 | [M07-2 舊線查結果、查報表不檢查專案成員，查不到還退回直接讀雲端硬碟](#m07-2) | 🟡 中 | [證據自動分類](../M07-evidence-classification.html) | 🗑️ 已拆除 |
| 50 | [M07-3 舊線工作清單與單筆狀態不檢查專案成員，進度資料不分客戶](#m07-3) | 🟡 中 | [證據自動分類](../M07-evidence-classification.html) | 🗑️ 已拆除 |
| 51 | [M07-4 舊版證據分類的雲端硬碟搜尋條件用字串拼接](#m07-4) | 🟡 中 | [證據自動分類](../M07-evidence-classification.html) | 🗑️ 已拆除 |
| 52 | [M03-9 開始掃描時把解密後的主機帳密多存一份明文](#m03-9) | 🟡 中 | [弱點檢測整合](../M03-detection.html) | ✅ 已修 |
| 53 | [M16-8 系統管理員密碼寫死在三支腳本裡](#m16-8) | 🟡 中 | [寄信與通知](../M16-notification.html) | ✅ 已修 |
| 54 | [M17-11 資料庫調整腳本「沒設變數就用真密碼」](#m17-11) | 🟡 中 | [公告](../M17-bulletin.html) | ✅ 已修 |
| 55 | [M23-3 Google 雲端硬碟的密鑰與加密金鑰散在 15 個進版控的檔案](#m23-3) | 🟡 中 | [意見回饋](../M23-issue.html) | ✅ 已修 |
| 56 | [M23-6 打包腳本寫死套件倉庫帳密並進了版本控制](#m23-6) | 🟡 中 | [意見回饋](../M23-issue.html) | ✅ 已修 |
| 57 | [M03-6 「測試連線」要連到哪台主機由呼叫者自己指定](#m03-6) | 🟡 中 | [弱點檢測整合](../M03-detection.html) | ✅ 已修 |
| 58 | [M04-3 密碼裡有雙引號就只遮一半](#m04-3) | 🟡 中 | [共用基礎](../M04-common.html) | ✅ 已修 |
| 59 | [M04-8 環境設定值打錯字時悄悄退回開發用的紀錄設定](#m04-8) | 🟡 中 | [共用基礎](../M04-common.html) | ✅ 已修 |
| 60 | [M09-3 日誌轉送只有兩個選項，兩個都是明文](#m09-3) | 🟡 中 | [系統日誌](../M09-log.html) | ✅ 已修 |
| 61 | [M15-4 AI 儀表板把查到的資料整包送回，夾帶密碼鹽值與超級管理員旗標](#m15-4) | 🟡 中 | [AI 儀表板](../M15-ai-dashboard.html) | ✅ 已修 |
| 62 | [M16-2 寄信失敗把整組郵件設定含密碼寫進紀錄](#m16-2) | 🟡 中 | [寄信與通知](../M16-notification.html) | ✅ 已修 |
| 63 | [M16-3 連郵件伺服器有加密但不認人](#m16-3) | 🟡 中 | [寄信與通知](../M16-notification.html) | ✅ 已修 |
| 64 | [M23-4 連 GitHub 時把憑證驗證關掉](#m23-4) | 🟡 中 | [意見回饋](../M23-issue.html) | ✅ 已修 |
| 65 | [M03-7 資料庫的客戶隔離規則方向寫反](#m03-7) | 🟡 中 | [弱點檢測整合](../M03-detection.html) | ✅ 已修 |
| 66 | [M18-8 子單位帳號刪得掉母公司的停權紀錄與授權異動紀錄](#m18-8) | 🟡 中 | [授權管理](../M18-license.html) | ✅ 已修 |
| 67 | [M20-5 一整類業務資料表連客戶歸屬欄位都沒有，牆沒有東西可依據](#m20-5) | 🟡 中 | [客戶資料隔離（跨模組專項）](../M20-tenant-isolation.html) | 🕓 SaaS 前必做 |
| 68 | [M09-5 日誌表按月分區失效，保存期限沒生效](#m09-5) | 🟡 中 | [系統日誌](../M09-log.html) | ✅ 已修 |
| 69 | [M16-1 寄測試信把存著的郵件密碼送到操作者指定的主機](#m16-1) | 🟡 中 | [寄信與通知](../M16-notification.html) | 🚫 裁定不修 |
| 70 | [M11-12 可以把別人的程序書掛到自己的控制項，再從回應讀到檔案編號去下載](#m11-12) | 🟡 中 | [合規文件核心](../M11-oscal.html) | ✅ 已修 |
| 71 | [M11-22 框架 PDF 解析失敗時把原始錯誤訊息整段回給前端](#m11-22) | 🟡 中 | [合規文件核心](../M11-oscal.html) | ✅ 已修 |
| 72 | [M11-14 沒被授權看合規範本的員工，直接打網址就讀走全公司範本](#m11-14) | 🟡 中 | [合規文件核心](../M11-oscal.html) | ✅ 已修 |
| 73 | [M11-15 範本的稽核流程圖任何登入帳號都讀得到](#m11-15) | 🟡 中 | [合規文件核心](../M11-oscal.html) | ✅ 已修 |
| 74 | [M11-16 範本掛了哪些程序書任何人都列得出來，附檔案編號就能下載](#m11-16) | 🟡 中 | [合規文件核心](../M11-oscal.html) | ✅ 已修 |
| 75 | [M11-17 範本的「稽核目標預設內容」任何登入帳號都讀得到](#m11-17) | 🟡 中 | [合規文件核心](../M11-oscal.html) | ✅ 已修 |
| 76 | [M11-4 範本裡的人員、設備、系統元件、繼承授權與系統特性拿編號就讀得到，而且跨公司](#m11-4) | 🟡 中 | [合規文件核心](../M11-oscal.html) | ✅ 已修 |
| 77 | [M06-17 規劃頁的任務清單不問你是不是這個專案的人](#m06-17) | 🟡 中 | [稽核流程](../M06-flow-engine.html) | ✅ 已修 |
| 78 | [M07-13 開關一放開，原廠 AI 金鑰就送到客戶指定的伺服器](#m07-13) | 🟡 中 | [證據自動分類](../M07-evidence-classification.html) | ✅ 已修 |
| 79 | [M07-14 證據分類逾時，原廠 AI 金鑰明文寫進日誌](#m07-14) | 🟡 中 | [證據自動分類](../M07-evidence-classification.html) | ✅ 已修 |
| 80 | [M24-3 使用者上傳的檔案整個資料夾開在 /static/ 下，不用登入就能下載](#m24-3) | 🟡 中 | [主系統自己的程式](../M24-host.html) | ✅ 已修 |
| 81 | [M24-5 即時通訊逐筆封包紀錄夾帶通行證進診斷包](#m24-5) | 🟡 中 | [主系統自己的程式](../M24-host.html) | ✅ 已修 |
| 82 | [M24-8 所有雲端硬碟資料夾設成「知道連結的任何人都能編輯」](#m24-8) | 🟡 中 | [主系統自己的程式](../M24-host.html) | 🚫 裁定不修 |
| 83 | [M10-20 摘要報告匯出 PDF 時放行 SVG 插圖，伺服器會替人去連內網](#m10-20) | 🟡 中 | [任務與成員管理](../M10-task-platform.html) | ✅ 已修 |
| 84 | [M24-6 診斷包設定快照遇到多行值只遮第一行](#m24-6) | 🟡 中 | [主系統自己的程式](../M24-host.html) | ✅ 已修 |
| 85 | [M17-3 用網址直接開一則公告，完全不做任何檢查](#m17-3) | ⚪ 低 | [公告](../M17-bulletin.html) | ✅ 已修 |
| 86 | [M17-4 公告列表碰到沒有部門的帳號就整個不過濾](#m17-4) | ⚪ 低 | [公告](../M17-bulletin.html) | ✅ 已修 |
| 87 | [M03-8 權限矩陣取消勾選後，後端八支讀取照樣回全部](#m03-8) | ⚪ 低 | [弱點檢測整合](../M03-detection.html) | ✅ 已修 |
| 88 | [M04-12 共用的「資料轉成回覆內容」工具資料上有什麼就吐什麼](#m04-12) | ⚪ 低 | [共用基礎](../M04-common.html) | ✅ 已修 |
| 89 | [M06-11 流程範本的列表與單筆讀取沒有掛權限檢查](#m06-11) | ⚪ 低 | [稽核流程](../M06-flow-engine.html) | ✅ 已修 |
| 90 | [M04-11 開發用小工具混在正式出貨的套件裡](#m04-11) | ⚪ 低 | [共用基礎](../M04-common.html) | 🗑️ 已拆除 |
| 91 | [M06-14 三支接收檔案路徑、但現在完全沒人呼叫的程式](#m06-14) | ⚪ 低 | [稽核流程](../M06-flow-engine.html) | 🗑️ 已拆除 |
| 92 | [M06-16 套件裡兩組「接好但沒人用」的備用服務，完全沒有權限檢查](#m06-16) | ⚪ 低 | [稽核流程](../M06-flow-engine.html) | 🗑️ 已拆除 |
| 93 | [M21-2 任何登入帳號都拿得到全站使用者名冊（含電子郵件）](#m21-2) | ⚪ 低 | [意見回饋重新檢視（跨模組專項）](../M21-issue-rescan.html) | 🗑️ 已拆除 |
| 94 | [M02-9 本機儲存刪檔時漏清轉檔產生的 PDF](#m02-9) | ⚪ 低 | [檔案上傳下載](../M02-file-upload.html) | ✅ 已修 |
| 95 | [M02-11 連自己裝的檔案儲存空間時不加密](#m02-11) | ⚪ 低 | [檔案上傳下載](../M02-file-upload.html) | 🚫 裁定不修 |
| 96 | [M05-11 問卷資料夾列表可以叫出系統刻意隱藏的資料夾](#m05-11) | ⚪ 低 | [問卷](../M05-survey.html) | ✅ 已修 |
| 97 | [M05-12 問卷資料夾列表把資料庫原始錯誤整句吐回畫面](#m05-12) | ⚪ 低 | [問卷](../M05-survey.html) | ✅ 已修 |
| 98 | [M07-6 按「刪除整批」之後，判定結果與分類紀錄仍永久留在主機上](#m07-6) | ⚪ 低 | [證據自動分類](../M07-evidence-classification.html) | ✅ 已修 |
| 99 | [M07-7 AI 服務金鑰放在啟動指令的參數上](#m07-7) | ⚪ 低 | [證據自動分類](../M07-evidence-classification.html) | ✅ 已修 |
| 100 | [M08-6 回報竄改時原封不動送出紀錄檔最後 50 行](#m08-6) | ⚪ 低 | [防竄改檢查](../M08-integrity.html) | ✅ 已修 |
| 101 | [M22-3 儲存設定的密鑰沒在遮蓋名單裡，明文回給瀏覽器](#m22-3) | ⚪ 低 | [系統設定](../M22-system-core.html) | ✅ 已修 |
| 102 | [M04-4 兩張運作紀錄表沒有客戶隔離](#m04-4) | ⚪ 低 | [共用基礎](../M04-common.html) | 🚫 裁定不修 |
| 103 | [M04-5 完整的程式出錯內容被寫進那張沒有隔離的紀錄表](#m04-5) | ⚪ 低 | [共用基礎](../M04-common.html) | 🚫 裁定不修 |
| 104 | [M04-6 連暫存資料庫時寫死不確認對方身分](#m04-6) | ⚪ 低 | [共用基礎](../M04-common.html) | 🚫 裁定不修 |
| 105 | [M04-10 程式一載入就自動連一條不加密的監控連線](#m04-10) | ⚪ 低 | [共用基礎](../M04-common.html) | ✅ 已修 |
| 106 | [M08-5 鎖定畫面把機器指紋與鎖定事件編號顯示給任何連得到的人](#m08-5) | ⚪ 低 | [防竄改檢查](../M08-integrity.html) | 🚫 裁定不修 |
| 107 | [M17-5 公告與部門的關聯表沒有設定客戶資料隔離](#m17-5) | ⚪ 低 | [公告](../M17-bulletin.html) | 🚫 裁定不修 |
| 108 | [M11-28 範本的「設備／資訊系統對照」清單任何登入帳號都讀得到](#m11-28) | ⚪ 低 | [合規文件核心](../M11-oscal.html) | ✅ 已修 |
| 109 | [M11-31 從框架版本下載空白範本，附了全公司 email、設備與 IP](#m11-31) | ⚪ 低 | [合規文件核心](../M11-oscal.html) | ✅ 已修 |
| 110 | [M06-20 匯出任務 Excel 時，檢查的專案和實際匯出的專案可以是兩個](#m06-20) | ⚪ 低 | [稽核流程](../M06-flow-engine.html) | ✅ 已修 |
| 111 | [M06-21 匯入任務的「驗證」「重新驗證」兩步完全不檢查身分](#m06-21) | ⚪ 低 | [稽核流程](../M06-flow-engine.html) | ✅ 已修 |
| 112 | [M06-22 「查卡住狀態」誰都能查別的專案](#m06-22) | ⚪ 低 | [稽核流程](../M06-flow-engine.html) | ✅ 已修 |
| 113 | [M12-4 同一家客戶任何帳號都看得到任何專案的「任務設定樹」](#m12-4) | ⚪ 低 | [稽核輪次管理](../M12-compliance-audit.html) | 🗑️ 已拆除 |
| 114 | [M12-5 非成員知道專案編號就問得出「目前的系統安全計畫」編號與狀態](#m12-5) | ⚪ 低 | [稽核輪次管理](../M12-compliance-audit.html) | 🗑️ 已拆除 |
| 115 | [M11-32 Word 打不開時錯誤訊息帶出伺服器暫存檔路徑](#m11-32) | ⚪ 低 | [合規文件核心](../M11-oscal.html) | ✅ 已修 |
| 116 | [M11-30 建資源庫時不看框架版本發佈了沒，草稿也能被複製](#m11-30) | ⚪ 低 | [合規文件核心](../M11-oscal.html) | ✅ 已修 |
| 117 | [M12-7 兩張「解析工作單」資料表照抄了五月就判定壞掉的隔離寫法](#m12-7) | ⚪ 低 | [稽核輪次管理](../M12-compliance-audit.html) | ✅ 已修 |
| 118 | [M24-14 依名稱找雲端資料夾時沒處理反斜線](#m24-14) | ⚪ 低 | [主系統自己的程式](../M24-host.html) | ✅ 已修 |
| 119 | [M10-22 證據蒐集執行進度只驗登入，換專案編號就看到別人的完成數](#m10-22) | ⚪ 低 | [任務與成員管理](../M10-task-platform.html) | ✅ 已修 |
| 120 | [M24-18 預蒐診斷資料時照索引檔的路徑讀檔，沒限制在暫存目錄內](#m24-18) | ⚪ 低 | [主系統自己的程式](../M24-host.html) | ✅ 已修 |
| 121 | [M24-13 診斷包的連線紀錄網址欄沒遮](#m24-13) | ⚪ 低 | [主系統自己的程式](../M24-host.html) | ✅ 已修 |
| 122 | [M24-9 即時通知有一個頻道不驗身分，任何人都能加入任意房間](#m24-9) | ⚪ 低 | [主系統自己的程式](../M24-host.html) | ✅ 已修 |
| 123 | [M11-40 人員對帳收了公司編號卻沒用，會拿所有客戶的人員來比對](#m11-40) | ⚪ 低 | [合規文件核心](../M11-oscal.html) | ✅ 已修 |

---

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

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

## M02-1 🟠 拿到檔案編號就能下載別人的檔案 {#m02-1}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #3；OWASP：A01 存取控制失效） |
| **在哪裡** | 檔案上傳下載模組：下載功能 |
| **攻擊面位置** | 已登入使用者可打的檔案 API。任何最低權限的帳號都構成攻擊面——**不需要是任何專案的成員** |
| **信任邊界位置** | **使用者 ↔ 檔案服務**。檔案服務收到「下載編號 X」時，應該在交出檔案前先問「X 掛在哪個專案，呼叫者在那裡有沒有權限」；當時只檢查了「有登入」。跨客戶那一段後來由資料庫隔離擋住，但**同一客戶內跨專案、跨部門**這段邊界仍是空的 |
| **元件端點位置** | `jedi_file_upload/api/routing.py`：`GET /file/download/{uid}`（`UploadFileDownloadRoute`）→ `upload_file_service.get_upload_file(uid)`，沒有歸屬檢查 |
| **駭客怎麼打** | ① 公司內任一有帳號的員工；② 在自己看得到的頁面記下檔案編號的樣子；③ 把下載網址的編號換成別人專案的檔案編號；④ 系統只驗登入，檔案就下來了 |
| **得手什麼** | 別部門、別專案的稽核證據檔案 |
| **修正的做法** | 四個出口共用一套：① **套件不懂業務，改成「問登記表」**——新增一個介面，各業務模組（專案證據、SSP 文件、意見回饋附件…）各自登記「這個檔是不是我的、這個人能不能碰」的答案；② **三條定死規則**：沒有任何模組認領的孤兒檔**一律拒絕**（不是放行），認領者說不行就不行，拒絕一律回「找不到」而不是「無權限」（後者等於告訴對方這個編號有效、可以逐一試）；③ **讀與寫分開問**：下載／預覽／換發問「能不能看」，刪除問「能不能刪」——同專案看得到證據的人遠多於刪得掉的人；④ 下載路由掛守門，底層取檔那一層也掛一道（雙保險）；⑤ 留一條平台管理員例外口給大批歷史孤兒檔，免得維運只能改資料庫 |
| **狀態** | ✅ 已修（CM-2033，套件 commit `07563773`）。驗證：四種身分 × 六種檔案的矩陣對 DEV 實跑 |

## M03-2 🟠 「測試連線」沒檢查權限，按下去就把維運帳密解密送到指定主機 {#m03-2}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #4；OWASP：A01 存取控制失效） |
| **在哪裡** | 弱點檢測整合模組：「檢測工具設定」頁的「測試連線」按鈕 |
| **攻擊面位置** | 已登入使用者可打的檢測工具 API。只要能登入就按得下去，**不需要任何管理權限**；連到哪台主機也是請求裡自己填的 |
| **信任邊界位置** | **使用者 ↔ 客戶的維運帳密**。這顆按鈕會先把加密存著的主機帳密與掃描工具權杖解開再送出去，解密前應該先確認「你有沒有權限改這份設定」。同一支檔案裡的新增、修改、重置都有這道檢查，**只有測試連線漏掉**——是漏寫，不是設計 |
| **元件端點位置** | `POST /api/1.0/detection-tools/configs/{uid}/test-connection`（套件 `jedi_detection/api/routes/detection_tool_route.py` 的 `TenantDetectionToolConfigTestConnectionRoute.post`）→ `app/service/detection_tool_service.py` 的測試連線流程，經遠端代理程式對目標主機實連 |
| **駭客怎麼打** | ① 公司裡任何一個能登入的帳號；② 打開檢測工具設定頁，記下某份工具設定的編號；③ 直接送測試連線請求，目標主機填成自己控制的機器；④ 系統只驗登入，把 SSH／Windows 遠端管理密碼與三套掃描工具權杖解密後送過去；⑤ 拿到的帳密可以直接登入客戶正式機房的主機 |
| **得手什麼** | 整家公司用來掃描主機的維運帳密與掃描工具權杖 |
| **修正的做法** | ① 測試連線路由補上 `@capability_required(plugin_update_capability)`，寫法照抄同一支檔案裡新增、更新、重置三處現成的宣告；② 旁邊同樣沒掛權限的「查引用次數」確認是純計數、不解密、不對外連線，維持原樣；③ 「連到哪裡」不設白名單（第 6 條另修：主機數上限、回應收斂、記稽核日誌），所以這道權限檢查就是唯一的防線 |
| **狀態** | ✅ 已修（CM-2042，套件 commit `5a9baf2b`） |

## M05-1 🟠 查填答歷史明細不檢查歸屬，送空白請求就整包讀走 {#m05-1}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #5；OWASP：A01 存取控制失效） |
| **在哪裡** | 問卷模組：查某一版填答的歷史明細（每一題當時怎麼答、稽核審核意見） |
| **攻擊面位置** | 已登入使用者可打的問卷 API。同一家公司內任何有帳號的人都構成攻擊面，**不需要是任何專案的成員**，也不需要知道任何編號 |
| **信任邊界位置** | **使用者 ↔ 問卷服務**。服務應該先確認「這份問卷屬於哪個專案、你是不是那個專案的人」；當時這支服務連可以反查歸屬的依賴都沒有，查詢條件全部可以不填，而底層「條件全空就不過濾」，於是一次回整張表 |
| **元件端點位置** | `POST /api/1.0/project-survey/history-details`（套件 `jedi_survey/api/routes/question_answer_history_detail_route.py` 的 `QuestionAnswerHistoryDetailsRoute`）→ `app/service/question_answer_history_detail_service.py`；守門在 `app/common/task_survey_guard.py` |
| **駭客怎麼打** | ① 任一員工登入；② 對歷史明細送一個什麼條件都不帶的空白請求；③ 系統不問你是誰、也不限範圍；④ 回來的是全公司每一份問卷每一版的答案，以及稽核人員寫的審核意見——包含受檢單位坦承的缺失 |
| **得手什麼** | 全公司所有問卷、每一版的作答內容與稽核審核意見 |
| **修正的做法** | ① 歷史明細的查詢鍵（歷史編號或問卷編號）改成**二擇一必填**，兩者同時帶時必須一致；② 由歷史紀錄反查它屬於哪一份任務問卷，再經新增的 `assert_task_survey_reader` 確認呼叫者是該專案任一參與者；③ 檢查通過後把問卷編號**強制寫回查詢條件**，避免另帶條件繞出去；④ 主專案替這支服務接上原本沒有的四個依賴（BE `4a5650c5d`），不補線守門會靜默失效；⑤ 後續 CM-2114 把「任務屬於哪個專案」改成問宿主注入的唯一判斷（讀輪次鏈），修正還沒指派人的任務問卷被誤擋 |
| **狀態** | ✅ 已修（CM-2034，套件 commit `83fc34e5`、BE `4a5650c5d`；歸屬判斷 CM-2114，套件 `034eef0e`、BE `49cbe0b7b`） |

## M05-2 🟠 讀某份問卷的答案不檢查這份問卷是不是你的 {#m05-2}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #6；OWASP：A01 存取控制失效） |
| **在哪裡** | 問卷模組：打開一份任務問卷讀取作答內容 |
| **攻擊面位置** | 已登入使用者可打的問卷 API。只需要一個問卷編號，而第 3 條「列出任務問卷」那支就會給 |
| **信任邊界位置** | **使用者 ↔ 問卷服務**。取出問卷之後應該確認呼叫者是那個專案的參與者；同一支服務裡另外三支寫入方法都有這道檢查，**只有這支讀取漏掉** |
| **元件端點位置** | `GET /api/1.0/project-survey/answers/{uid}`（套件 `jedi_survey/api/routes/question_answer_route.py` 的 `TaskSurveysAnswersRoute.get`）→ `app/service/question_answer_service.py` 的 `get_answer_list_by_task_survey_uid()` |
| **駭客怎麼打** | ① 任一員工登入；② 先用第 3 條拿到別部門問卷的編號；③ 把編號放進讀答案的網址；④ 系統不問歸屬，別部門的作答內容、分數與審核意見全部回來 |
| **得手什麼** | 別部門、別專案問卷的作答內容、分數與審核意見 |
| **修正的做法** | ① 取出任務問卷後補一行 `assert_task_survey_reader`：由任務反查專案（不信呼叫端給的專案編號），委派任務平台的「是不是專案參與者」檢查；② 反查不到專案時一律拒絕；③ 讀取門檻定在「該專案任一參與者」，寫入維持原本「被指派人本人或專案管理者」，讀比寫寬一級；④ 後續 CM-2114 改用宿主注入的唯一歸屬判斷 |
| **狀態** | ✅ 已修（CM-2034，套件 commit `83fc34e5`；歸屬判斷 CM-2114 `034eef0e`） |

## M06-1 🟠 知道一個任務編號就能讀走稽核證明清單 {#m06-1}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #7；OWASP：A01 存取控制失效） |
| **在哪裡** | 稽核流程模組：任務底下的「稽核證明」（佐證）清單 |
| **攻擊面位置** | 已登入使用者可打的佐證 API，只需要一個任務執行編號 |
| **信任邊界位置** | **使用者 ↔ 稽核流程服務**。讀佐證前應該確認呼叫者是這個任務所屬專案的參與者；同一支檔案裡新增、更新、刪除都有，**只有讀取漏掉**。這條與檔案上傳那塊（[M02-1](#m02-1)）是同一條鏈：那邊是「拿到檔案編號就能下載」，這邊是「連檔案編號都送給你」 |
| **元件端點位置** | `GET /api/1.0/job-evidences`、`GET /api/1.0/job-evidence/{uid}`（`api/flow_engine/routes/job_evidence_route.py` 的 `JobEvidencesRoute`、`JobEvidenceRoute`）→ `app/flow_engine/service/job_evidence_service.py` 的 `get_job_evidences_by_job_execution_uid()`、`get_job_evidences_uid()` |
| **駭客怎麼打** | ① 任一登入帳號拿到別人專案的一個任務編號（任務清單、通知信都看得到）；② 查這個任務的佐證清單；③ 拿到檔名、描述、雲端硬碟連結、上傳者帳號、檔案指紋與檔案編號；④ 拿檔案編號去打下載，取走檔案本體 |
| **得手什麼** | 別人專案的稽核證明清單，以及通往檔案本體的檔案編號 |
| **修正的做法** | ① 兩支讀取方法補 `assert_project_participant`，接的是新增／更新／刪除已經在用的同一條檢查，沒有另立新函式；② 檔案下載那一頭另由 [M02-1](#m02-1) 的歸屬檢查擋住，兩頭都補上 |
| **狀態** | ✅ 已修（BE commit `e2c32258d`，卡號 CM-2035）。⚠️ 總表與模組頁狀態欄寫的是 CM-2059，但 CM-2059 的 commit 修的是流程範本驗證；實際修這條的是 CM-2035 那顆 commit |

## M06-2 🟠 流程留言的讀與寫都只檢查有沒有登入 {#m06-2}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #8；OWASP：A01 存取控制失效） |
| **在哪裡** | 稽核流程模組：稽核流程裡的討論留言（讀整串、新增一則） |
| **攻擊面位置** | 已登入使用者可打的留言 API，只需要一個流程執行編號。這是同組五條裡唯一「能寫」的 |
| **信任邊界位置** | **使用者 ↔ 稽核流程服務**。路由層直接查流程變數、自己組好留言再寫回，**完全沒經過服務層的守門**，也沒有長度與筆數上限；同模組的「完成任務」「退回任務」都有檢查 |
| **元件端點位置** | 修正前：`GET`／`PUT /api/1.0/flow-engine/process/comments/{id}`（`api/flow_engine/routes/flow_engine_route.py` 的 `WorkflowExecutionCommentRoute`），留言存在流程變數的一個 JSON 陣列裡；新留言經即時通知推播給專案成員 |
| **駭客怎麼打** | ① 任一登入帳號拿到別人專案的流程編號；② 讀走整串稽核討論；③ 再往裡面塞一則留言，例如「請點這個連結補上傳證據」；④ 留言即時推播給該專案全體成員，看起來就像同事發的，可以拿來騙人點連結或交出資料 |
| **得手什麼** | 別人專案的整串稽核討論，以及一個能對該專案全員發假訊息的管道 |
| **修正的做法** | ① 新開 `app/flow_engine/service/workflow_comment_app_service.py` 承接：進場先解流程執行 → `assert_project_participant`；② 留言內容補驗證：不可空白、單則 2000 字、單一流程 500 則上限（留言存在 JSON 陣列裡，沒有資料庫欄位長度可擋，只能在應用層管）；③ 推播的頻道與格式不變，只改用服務層回傳的內容；④ 之後查出整條流程討論前後端都斷了、沒人在用，決策者裁定連留言 API 與即時通知頻道一起拔除（CM-2373） |
| **狀態** | ✅ 已修（CM-2035，BE commit `e2c32258d`）；後續整條拔除（CM-2373，BE `99beceb26`、前端 `2358d96`） |

## M07-1 🟠 舊線「預覽證據檔」填任何雲端硬碟檔案編號就把檔案抓回來 {#m07-1}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #9；OWASP：A01 存取控制失效） |
| **在哪裡** | 證據自動分類模組的**舊線**（以雲端硬碟資料夾為單位的分類）：分類結果頁的「預覽證據檔」 |
| **攻擊面位置** | 任何能登入的帳號，**不必是任何專案的成員**。網址上的雲端硬碟檔案編號由呼叫者自己填 |
| **信任邊界位置** | **使用者 ↔ 授權的 Google 帳號**。系統申請的是最寬的雲端硬碟權限，憑證又是真人帳號授權的；預覽前應該確認「這個檔案屬於這一批分類、而你是這個專案的人」，當時拿到編號就直接用公司那把鑰匙去 Google 抓 |
| **元件端點位置**（拆除前） | 套件 `jedi_evidence_classification/api/routing.py`：`GET /classification-run/{run_folder_id}/file/{file_drive_id}/preview` → `evidence_classification_service` 的檔案預覽方法 → `infra/evidence_drive_ops.py` 以租戶的 Drive 憑證下載 |
| **駭客怎麼打** | ① 任一登入帳號；② 在預覽網址填一個雲端硬碟檔案編號（別人分享過的連結、郵件裡的連結都看得到編號）；③ 系統用授權那個 Google 帳號的權限去抓；④ 那個帳號摸得到的任何檔案——別的專案的證據、人事檔、合約——都被原樣送回來 |
| **得手什麼** | 授權那個 Google 帳號所能觸及的全部檔案（含別人分享給他的） |
| **修正的做法** | 拆除了什麼：舊 Drive 分類線九支網址（觸發、工作清單、狀態讀寫、封存、**檔案預覽**、驗證報表、裁定報表、摘要）連同服務方法整條拆掉，服務檔從 1,247 行剩 70 行、只留正解匯入；契約測試新增「這九支必須不在」的清單，誰掛回來就紅；主專案刪對應測試、前端拿掉舊線入口。拆除前查證 POC／STG 舊線資料 0 筆 |
| **狀態** | 🗑️ 隨舊線退場，1.21.0 出貨（8-L，CM-2222，套件 commit `87fd5439`／BE `ccc5795f6`／前端 `2537ea5`） |

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

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #10；OWASP：A07 身分驗證失效、A02 安全設定錯誤） |
| **在哪裡** | 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；OWASP：A07 身分驗證失效、A02 安全設定錯誤） |
| **在哪裡** | 通知模組檢視時查到：專案根目錄的測試用設定檔 `.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；OWASP：A07 身分驗證失效、A02 安全設定錯誤） |
| **在哪裡** | 公告模組檢視時查到：同一組四把 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；OWASP：A07 身分驗證失效、A02 安全設定錯誤） |
| **在哪裡** | 意見回饋模組檢視時查到：資料庫系統管理帳號（可繞過客戶隔離的那一個）的密碼，散在 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；OWASP：A07 身分驗證失效、A02 安全設定錯誤） |
| **在哪裡** | 通知模組檢視時查到：一支產生資料庫結構文件的腳本，把客戶展示環境（依規定等同正式環境）的完整連線資訊寫在程式碼裡 |
| **攻擊面位置** | **拿得到程式庫的人**，加上連得到展示機的內網位置。它是 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；OWASP：A07 身分驗證失效、A02 安全設定錯誤） |
| **在哪裡** | 公告模組檢視時查到：資料庫系統管理帳號的密碼寫在開發文件裡 |
| **攻擊面位置** | **拿得到程式庫的人**，加上連得到開發環境或出貨基線資料庫的位置 |
| **信任邊界位置** | **版控 ↔ 祕密存放處**，以及**一般應用帳號 ↔ 可繞過隔離的管理帳號**。出貨基線庫是所有安裝檔的來源，這條線一破，寫進去的東西會跟著出貨 |
| **元件端點位置** | 外流位置：`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` 清除） |

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

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #14；OWASP：A01 存取控制失效、A02 安全設定錯誤） |
| **在哪裡** | 檔案上傳下載模組：系統設定裡的「儲存設定」（MinIO／SeaweedFS 物件儲存的連線帳密） |
| **攻擊面位置** | 已登入使用者可打的系統設定讀取 API，不需要管理權限、不需要知道任何編號 |
| **信任邊界位置** | **後端 ↔ 瀏覽器**，同時也是**我們的系統 ↔ 儲存空間**。儲存帳密應該只在後端內部用來連儲存；當時讀設定的 API 把密鑰原文整格回傳。拿到之後可以**完全繞過整個系統直連儲存空間**——程式裡補再多檢查都沒用。落地版一客戶一套，拿到的就是那家的全部檔案 |
| **元件端點位置** | `GET /api/1.0/system/configs/{group}`、`GET /api/1.0/system/config/{uid}`（套件 `jedi_system_core/api/routes/system_config_route.py`）、`GET /api/1.0/system/config/{group}/{key}`（主專案 `api/system_config/routes/system_config_route.py` 的 `SystemConfigGroupRoute`）→ 主專案 `app/system_config/service/guarded_system_config_service.py` |
| **駭客怎麼打** | ① 任一員工登入；② 送一個讀「儲存設定」的請求；③ 回應裡帶著存取帳號與密鑰原文；④ 拿這組帳密用任何物件儲存工具直連，把所有客戶上傳的檔案列出、下載、覆蓋、刪除 |
| **得手什麼** | 可讀寫全部上傳檔案的儲存空間鑰匙 |
| **修正的做法** | 照模組頁「五步、順序不能換」施工：① **先補安全網**：`_merge_storage_secrets` 改成這次沒帶密鑰（缺鍵或空字串）就沿用既有值、絕不覆蓋成空，前端原樣送回的遮罩旗標剝掉不落地；② 前端儲存設定頁密鑰欄改遮罩顯示，沒改就不送；③ **服務層預設不回真值**：新增 `_mask_storage`，讀出儲存設定一律拿掉 `secret_key`／`minio_secret_key`、換成「有沒有設」旗標——不是往遮罩名單加字（名單法已漏過兩次），套件那份名單留作第二層；④ 排查背景工作：上傳服務走同一支設定服務，第③步一上就會拿不到密鑰而靜默連線失敗，故 DI 另開一個可讀真值的實例只注入上傳服務；⑤ 後續補「新建時密鑰必填」（CM-2117）。**沒有另加讀取權限檢查**——遮罩對所有人生效，有權限也拿不到真值 |
| **狀態** | ✅ 已修（CM-2063，BE commit `7c904eb6e`、前端 `756783b`；遮罩名單第一層 CM-2054 套件 `16a3e41c`）。⚠️ 模組頁第五步「把寫死在程式與腳本裡的那組帳密收攏成單一來源」查無對應 commit |

## M22-2 🟠 任何登入帳號打一支查設定的功能，就拿到檔案儲存的帳號密碼明文 {#m22-2}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #14；OWASP：A01 存取控制失效、A02 安全設定錯誤） |
| **在哪裡** | 系統設定模組：同一件事的系統設定那一側——讀設定的三個入口（與 [M02-5](#m02-5) 是同一個洞，從兩個模組各查到一次） |
| **攻擊面位置** | 已登入使用者可打的系統設定讀取 API，**不必是管理員、不必有任何權限、不必知道任何編號**。同一條路還會吐出寄信與員工帳號目錄的位址、埠號、登入帳號 |
| **信任邊界位置** | **後端 ↔ 瀏覽器**。同一支檔案裡寫入端都有能力點檢查，讀取端沒有；三個讀取入口任何一個漏掉都等於沒補 |
| **元件端點位置** | 套件 `jedi_system_core/api/routing.py`：`GET /system/configs/{group}`（`SystemConfigListRoute`）、`GET /system/config/{uid}`（`SystemConfigDetailRoute.get`）；主專案 `GET /system/config/{group}/{key}`（`SystemConfigGroupRoute.get`）；三者都進 `GuardedSystemConfigService` |
| **駭客怎麼打** | ① 任一登入帳號；② 對三個讀取入口任一支要 `STORAGE_CONFIG`；③ 拿到儲存帳密原文；④ 直連儲存空間讀寫所有客戶上傳的檔案，順便記下寄信伺服器與員工目錄的位址與帳號，作為下一步攻擊的目標 |
| **得手什麼** | 可讀寫所有上傳檔案的鑰匙，加上郵件與員工目錄的連線資訊 |
| **修正的做法** | 與 [M02-5](#m02-5) 同一張卡、同一套修法：三個入口都經過的 `GuardedSystemConfigService` 單一分派點 `_mask_secrets` 新增 `_mask_storage`，讀出去一律拿掉密鑰、換成「有沒有設」旗標——在**所有讀取入口共用的那一層**收口，所以三個入口一次補齊；上傳服務另開可讀真值的實例。寄信與員工目錄的密碼本來就在遮罩名單內；位址與帳號由 CM-2401 補上總部層級讀取守門——寄信、帳號目錄、登入政策三組的讀取非總部一律 403，清單查詢濾掉這三組 |
| **狀態** | ✅ 已修（密鑰：CM-2063，BE commit `7c904eb6e`；三組共用設定的讀取：CM-2401，BE `779837919`、FE `afb45a2`）。其他各客戶自己一份的設定讀取入口仍只驗登入 |

## M09-1 🟠 任何一家客戶的管理員都改得動全系統的日誌轉送去向 {#m09-1}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #19；OWASP：A01 存取控制失效、A09 日誌與告警失效） |
| **在哪裡** | 日誌模組：「系統設定 → 日誌轉送設定」頁，填一台伺服器位址，系統就把全系統的活動紀錄持續送過去 |
| **攻擊面位置** | 已登入、持有「改日誌轉送設定」權限的管理員。這個權限被標成客戶層級，每開一家新客戶，那家的管理員角色（不分總部、子公司）就自動拿到，所以門檻是「任何一家客戶的管理員」 |
| **信任邊界位置** | **客戶 ↔ 客戶（租戶之間）**。轉送設定全系統只有一份，送出管道一個程式裡也只有一條，所有客戶的日誌都走它；這條線上應該檢查「你的層級有沒有資格改全系統那一份」，當時只檢查「你有沒有這個權限」 |
| **元件端點位置** | `PUT /api/1.0/log-forwarding`（存設定）、`POST /api/1.0/log-forwarding/test`（測試發送）；套件 `jedi_api_log/forwarding/api/routing.py` 的 `ROUTE_TABLE` → `api/routes/log_forwarding_route.py`：`LogForwardingSettingRoute.put`、`LogForwardingTestRoute.post`；宿主守門在 `core/plugins/api_log.py:_forwarding_guard`，層級判定在 `common/authz/tenant_hq.py:require_tenant_hq` |
| **駭客怎麼打** | ① 任一家客戶的管理員（甚至是子公司的）登入；② 打開日誌轉送設定，填自己控制的一台機器位址、傳送方式選明文，按儲存；③ 系統只問「你有沒有改這個設定的權限」，有就寫進全系統唯一那一列；④ 從此所有客戶的系統活動紀錄持續送往他的機器，當時日誌裡還夾著密碼與通行證原文，而且沒有人會察覺 |
| **得手什麼** | 全部客戶持續不斷的系統活動紀錄（含當時殘留的密碼與通行證） |
| **修正的做法** | ① **寫入多一道「層級」檢查**：`_forwarding_guard` 的寫入那欄（存設定、測試發送）加上 `require_tenant_hq`，順序是「登入 → 有權限 → 層級夠」，沒權限的人拿到的是「沒權限」而不是「層級不夠」，讀取不卡（CM-2054）；② **層級判定收緊**：原本用「租戶樹第二層就算總部」判定，但第二層不只一家，任何一家的管理員仍改得到全站那一份；改成 SaaS 版只有平台管理員能改，落地版只有平台管理員與安裝精靈建立的那個客戶租戶能改（查詢走繞過客戶隔離的獨立連線，因為子公司看不到上層租戶會誤判）；③ 查不到身分、查不到客戶租戶一律拒絕（CM-2202） |
| **狀態** | ✅ 已修（CM-2054，BE commit `e90d8e3cb`＋`a25933de8`；層級判定收緊 CM-2202，commit `6cfcb116e`，1.21.0 出貨）。驗證：DEV 以四種身分實打寫入端點，非精靈建立的客戶與子公司一律 403 |

## M09-2 🟠 不必登入就能在操作日誌匯出檔裡種公式 {#m09-2}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #20；OWASP：A05 注入攻擊） |
| **在哪裡** | 系統日誌模組：管理員在「操作日誌」查詢頁按「匯出」，拿到 Excel 打開 |
| **攻擊面位置** | 系統每一個入口——每次請求都會在任何權限檢查之前被原樣記下來（瀏覽器識別字串、網址、查詢條件、請求內容），所以**不需要帳號**。匯出的 17 個欄位裡有 6 個吃得到外部輸入 |
| **信任邊界位置** | **我們的資料 ↔ 管理員自己的電腦**。「先記錄再檢查權限」的順序是對的，不該改；該守的是匯出那一側——交出去之前把會被試算表當公式的開頭字元標成純文字。當時匯出沒有任何處理 |
| **元件端點位置** | 種入：主專案 `common/middleware/app_mw.py` 的 `before_request()` 寫 api_log；引爆：`jedi_api_log/api_log/api/routing.py` 的 `GET /log/api-logs/export`（`ExportApiLogsRoute`，平台管理員）→ `api_log_service.export_api_log_file()` → `api_log/common/utils/excel_util.py` 的 `clean_invalid_characters()`／`gen_excel_bytes()` |
| **駭客怎麼打** | ① 不需要帳號，對系統任一網址送一個請求；② 把瀏覽器識別字串改成一串以 `=` 開頭的公式；③ 系統照實記進操作日誌；④ 管理員匯出操作日誌、打開 Excel、按掉安全警告；⑤ 公式在管理員電腦上執行，可以是釣魚連結，也可以把整張表送到外部網址 |
| **得手什麼** | 在管理員電腦上執行內容，外洩整份操作日誌 |
| **修正的做法** | ① jedi-common 新建共用函式 `neutralize_formula()`：開頭是 `=` `+` `-` `@` tab CR 的字串前面加一撇單引號，其他原樣，**一個字都不刪**；② 操作日誌匯出的每一格都套它；③ `clean_invalid_characters()` 先清控制字元、再判公式字元——順序不能反，控制字元清掉後才會露出後面的 `=` |
| **狀態** | ✅ 已修（CM-2055，套件 commit `38de2886`）。驗證：實跑匯出、存檔讀回是純文字 |

## M21-1 🟠 意見回饋匯出檔可以被種公式 {#m21-1}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #20；OWASP：A05 注入攻擊；模組頁原評 🟡 中；STRIDE：權限提升、資料外洩）） |
| **在哪裡** | 意見回饋模組：管理員匯出意見回饋成 CSV 或 Excel |
| **攻擊面位置** | 送意見回饋的 API，門檻只有登入。使用者可控的欄位有標題、描述、標籤名、建立者暱稱 |
| **信任邊界位置** | **我們的資料 ↔ 管理員自己的電腦**。同 M09-2，該在匯出那一側中和；當時兩個匯出分支都沒處理 |
| **元件端點位置** | 種入：`jedi_issue/api/routing.py` 的 `POST /feedback`（`FeedbackRoute`）；引爆：`POST /feedback/export/{export_type}`（`FeedbackExportRoute`）→ `jedi_issue/app/feedback/service/feedback_service.py` 的 `export_feedbacks()`（csv 與 excel 兩個分支） |
| **駭客怎麼打** | ① 任一登入帳號送一則意見回饋，標題以 `=` 開頭寫一段公式；② 有匯出權限的管理員匯出成 CSV 或 Excel；③ 用試算表軟體打開；④ 公式執行，把同一份檔案其他欄位的內容送到外部網址，或在使用者點過安全提示後執行外部程式 |
| **得手什麼** | 在管理員電腦上執行內容，外洩整份回饋清單 |
| **修正的做法** | ① 與 M09-2 共用同一支 `neutralize_formula()`，不另寫一份；② csv（`csv.DictWriter` 每列）與 excel（`ws.append` 每格）兩個分支都套，避免只修一個入口 |
| **狀態** | ✅ 已修（CM-2055，套件 commit `38de2886`）。驗證：實跑兩個分支都中和 |

## M15-1 🟠 打字就能誘導 AI 儀表板選中任何一支查詢 {#m15-1}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #21；OWASP：A05 注入攻擊、A06 不安全的設計） |
| **在哪裡** | AI 儀表板模組：使用者打一句需求，AI 從 26 支查詢裡挑一支、再設計圖表 |
| **攻擊面位置** | 生成儀表板的 API，任何最基層的登入帳號都能打。修正前沒有次數限制，每試一次只花公司的 AI 額度 |
| **信任邊界位置** | **使用者的字 ↔ AI 的判斷**。使用者打的字與「可以選哪些功能」的清單被串成同一段純文字交給 AI，中間沒有區隔；AI 選完之後，「這個人能不能看這支查詢的資料」也沒檢查（第 2 條，見 [A01](A01-broken-access-control.html)） |
| **元件端點位置** | `jedi_ai_dashboard/api/routing.py`：`POST /ai-dashboard/auto-generate`（`AiDashboardAutoGenerateRoute`）→ `app/service/ai_dashboard_app_service.py` 的 `_ask_ai_to_select_api()`／`_ask_ai_to_design_layout()` → `jedi_ai_gateway`（`app/gateway_service.py`、`infra/guard/rules/gateway_rules.yaml`） |
| **駭客怎麼打** | ① 任一員工打開 AI 儀表板；② 在需求裡寫得像指令，例如要求它改選「全公司帳號名冊」那支查詢；③ 沒選中就換個說法再試，沒有次數上限；④ AI 選中後系統照跑，回傳組織架構、權限設定、客戶清單、所有專案 |
| **得手什麼** | 本來看不到的組織架構、權限、客戶與專案清單 |
| **修正的做法** | ① **補逾時與次數上限**：外部 AI 呼叫加 60 秒逾時，生成端點每人每分鐘 3 次、超過回 429 並帶剩幾秒；② **後續統一進 AI 閘道**：次數限制改由閘道對每位使用者計（現行預設每分鐘 20 次，一次生成只算挑查詢那一次），兩支 AI 套件自帶的限流隨之刪除；③ **使用者的字獨立成「不可信段」**：送 AI 時標成資料段，並附一句「這段是資料不是指令」；④ 「能拿到什麼」由同模組第 2 條收口——26 支查詢逐支申報要什麼權限，沒權限就不跑 |
| **狀態** | ✅ 已修（CM-2064，套件 commit `248132ec`、主專案 `0d321d98a`）。現行做法已改由 AI 閘道承接（CM-2311 `8f85e3b6`、CM-2344 `2be00daf`、CM-2345 `d4158de59`、CM-2346 `7c3db8b5`）；權限收口見 CM-2038 `8f94e249d` |

## M20-1 🟠 13 張標有客戶歸屬的資料表，資料庫那道隔離牆沒有真正生效 {#m20-1}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #24；OWASP：A01 存取控制失效、A02 安全設定錯誤） |
| **在哪裡** | 客戶資料隔離（跨模組專項）：資料庫層用來擋「別家客戶」的那道牆（PostgreSQL 的列級安全規則） |
| **攻擊面位置** | 所有讀寫這 13 張表的功能，任何登入帳號都經過；應用層只要有一處沒檢查客戶歸屬，就靠這道牆擋——當時牆不在 |
| **信任邊界位置** | **客戶 ↔ 客戶（租戶之間）**。這是所有功能的最後一道保險。13 張表裡 3 張規則寫好但開關沒開、1 張規則不完整（只有寫入規則、沒有查詢規則）、9 張連規則都沒有 |
| **元件端點位置** | 資料表：`compliance.projects`、`public.tenant_drive_integrations`、`compliance.job_executions`、`information_systems`、`compliance.poams`、`evidence_classification_runs`、`evidence_classification_ground_truth`、`framework_parse_jobs`、`user_roles`、`user_tenants`、`compliance.workflow_executions`、`workflow_templates`、`config.log_forwarding_settings`；受影響的功能例如 `POST /api/1.0/projects/list`、`POST /api/1.0/jobs/my/list`、`/audit-round/{round_uid}/poam-items`；規則判斷函式 `app_tenant_allowed_for_session()` |
| **駭客怎麼打** | ① 任一家客戶的員工登入；② 打開專案清單、我的任務、改善計畫等頁面；③ 應用層剛好沒過濾客戶的地方，資料庫也不擋；④ 畫面上出現別家客戶的專案、待辦、任務執行紀錄與稽核資料（實測：指向不存在客戶的鑰匙看到專案 213 筆、任務執行紀錄 11,065 筆） |
| **得手什麼** | 別家客戶的專案、待辦、任務執行紀錄、稽核缺失與改善計畫 |
| **修正的做法** | 2026-09-14 一批（FR-094）分組處理：① **只開開關**：專案補上平台管理員分支後開啟、雲端硬碟連動設定直接開（CM-1768）；② **補規則再開**：使用者角色、使用者客戶對照、使用者部門三張（CM-1769），改善計畫、證據分類執行紀錄與標準答案、任務執行紀錄、框架解析工作五張（CM-1770），每張補查詢／新增／修改／刪除四條標準規則；③ **補查詢規則**：工作流程執行紀錄補上缺的查詢規則，修改刪除改成不綁部門（跨部門協作是常態，綁了會「更新 0 筆且不報錯」）（CM-1771）；④ **流程範本**加範圍欄位與規則（CM-1772）；⑤ 排程類掃全表的工作先接上具名系統身分，避免開牆後安靜掃到 0 筆；⑥ 日誌轉送設定屬平台層設定、寫入只給平台管理員，裁定不當隔離缺口 |
| **狀態** | ✅ 已修，1.21.0 出貨（BE commit `0ca61108c` CM-1768、`197951723` CM-1769、`b5ca22bd1` CM-1770、`b502b0b47` CM-1771、`58950bb8e` CM-1772）。2026-09-20 唯讀重測：同一把鑰匙查專案、待辦、任務執行、流程執行全部 0 筆；剩日誌轉送設定一張屬平台層設定 |

## M02-6 🟠 存放檔案紀錄的資料表沒有客戶歸屬欄位，資料庫擋不住跨客戶 {#m02-6}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #25；OWASP：A01 存取控制失效、A06 不安全的設計） |
| **在哪裡** | 檔案上傳下載模組：記錄每一個上傳檔案的那張資料表 |
| **攻擊面位置** | 所有經過檔案模組的功能（上傳、下載、預覽、換發通行證、刪除），任何登入帳號 |
| **信任邊界位置** | **客戶 ↔ 客戶**。這是「四道防線」的最後一道：就算入口、業務邏輯、取資料三層都漏了，資料庫本身也該擋住別家客戶。當時這張表連「這是哪家客戶的」欄位都沒有、也沒開隔離，只靠「掛它的那張業務表」間接保護——暫存區那種還沒掛到任何業務表的檔案就完全沒保護 |
| **元件端點位置** | 資料表 `public.upload_files`；唯一的寫入點 `jedi_file_upload/infra/repository/upload_file_repo_impl.py` 的 `add_upload_file()`；經過的端點如 `GET /api/1.0/file/download/{uid}`、`GET /api/1.0/file/pdf-preview/{uid}`、`/file/access-token/{uid}` |
| **駭客怎麼打** | ① 任一家客戶的登入帳號；② 拿到別家客戶的檔案編號（網址、分享連結、回應內容都可能帶出）；③ 應用層只要有一處沒查歸屬，資料庫也照給；④ 別家客戶的稽核證據被下載 |
| **得手什麼** | 別家客戶的上傳檔案 |
| **修正的做法** | ① 表上新增客戶歸屬欄位（不可為空）與上傳者欄位；② 回填規則依參照鏈判定：證據、掃描基準版本、掃描執行三張帶客戶的表 → 系統公版資產歸原廠 → 退回建立者所屬客戶 → 剩下 21 筆孤兒歸原廠（「退回建立者」與參照鏈交叉比對 711 筆一致、0 筆衝突）；③ 開隔離並建查詢／新增／修改／刪除四條規則，系統公版資產全客戶可讀；④ 主專案把背景上傳與系統檔上傳包進正確的客戶身分，否則欄位不可為空會讓背景上傳整條掛掉；⑤ 新增守衛測試，以一般帳號實連兩個客戶確認互相看不到 |
| **狀態** | ✅ 已修（2026-09-16，CM-1850：套件 commit `651e33c2`、BE `a48b41cc0`）。DEV 2,706 列全數補上歸屬。⚠️ 總表狀態欄未寫卡號，修正 commit 由本頁回查補上；這次改動原本是為證據暫存區（FR-107）做的，順帶補上了這道牆 |

## M04-1 🟠 登入密碼與通行證原文寫進紀錄檔與資料庫 {#m04-1}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #26；OWASP：A09 日誌與告警失效） |
| **在哪裡** | 共用地基：每一個請求進來時的紀錄程式，會把請求帶的資訊與送出的內容寫進伺服器紀錄檔與操作日誌資料表 |
| **攻擊面位置** | 不是對外的攻擊面，而是「讀得到紀錄的人」——維運人員、能查資料庫的人、外包、離職者、拿到回廠診斷包的人。**不需要攻擊者做任何事**，每一次登入都會發生 |
| **信任邊界位置** | **應用程式 ↔ 紀錄檔／診斷包**。密碼應該在寫出紀錄的那一刻就被遮掉；當時寫資料庫那條有呼叫遮蔽，但印進紀錄檔的那兩行（請求標頭、請求內容）沒用上，回覆內容也是裸寫 |
| **元件端點位置** | 所有端點都經過，最要緊的是 `POST /api/1.0/login`（套件 `jedi_iam/api/routes/login_route.py:LoginRoute`）；紀錄程式在 `common/middleware/app_mw.py:init_app_interceptor` 的 `before_request`／`after_request`，遮蔽函式是 `jedi_common/utils/common_utils.py:mark_password` |
| **駭客怎麼打** | ① 不用打——任何使用者正常登入，帳號密碼就原文寫進 `log/app.log`，請求標頭裡的登入通行證也是；② 拿得到紀錄檔或回廠診斷包的人打開檔案搜尋 password 或 Authorization；③ 直接拿到密碼與可冒用身分的通行證，不必破解任何東西 |
| **得手什麼** | 使用者的登入密碼原文，與可以直接冒用身分的通行證 |
| **修正的做法** | 分三次補齊：① **回覆內容也遮**——原本只遮請求、回覆裸寫，而心跳回應會夾帶解密後的工具密碼；遮罩範圍從 password 擴到 secret／token／credential／authorization／api_key（commit `e2eb1c2c9`）；② **印進紀錄檔的請求內容那行接上同一份 `mark_password`**（commit `f3840d1f8`，CM-1900 順手修）；③ **請求標頭改成白名單**：新增 `_mask_headers()`，名單外的標頭值一律遮成 `***`、名字保留，白名單漏的是「某個診斷標頭看不到值」一眼看得出，黑名單漏的是「未來新增的憑證標頭」沒人會發現（CM-1919，commit `8417bd7cb`）；④ 已寫進去的資料庫兩張紀錄表清成 0 筆、含明文的紀錄檔全數刪除（資料清理，無程式 commit） |
| **狀態** | ✅ 已修（CM-1919 等，BE commit `e2eb1c2c9`／`f3840d1f8`／`8417bd7cb`）。驗證：DEV 帶假通行證與假密碼打 API，`log/app.log` 與操作日誌表 grep 0 命中 |

## M04-2 🟠 沒有登入身分時，客戶資料隔離整個關掉 {#m04-2}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #27；OWASP：A01 存取控制失效、A02 安全設定錯誤） |
| **在哪裡** | 共用基礎模組：每次查資料前告訴資料庫「現在是誰在查」的那一段（所有功能共用） |
| **攻擊面位置** | 所有「還沒有身分」的路徑——登入功能本身、雲端硬碟通知入口這類不帶使用者的端點，以及任何漏掛認證的路由或沒還原身分的背景執行緒 |
| **信任邊界位置** | **未認證請求 ↔ 資料庫隔離**。原本的寫法是「查不到身分就當作最高權限的系統管理員」，另外「身分裡沒有客戶編號」也被當成最高權限。缺值被解讀成特權，任何忘記設值的路徑都會悄悄拿到全部客戶的資料可見性 |
| **元件端點位置** | 套件 `jedi_common/session/database/db.py` 的 `session_scope()`（設定資料庫連線變數 `app.is_super_admin`、`app.allowed_tenant_paths`）；每次都處在無身分狀態的入口：`POST /api/1.0/login`、`POST /api/1.0/webhooks/google-drive/{tenant_id}` |
| **駭客怎麼打** | ① 不需要帳號，打一支處在「無身分」狀態的入口（例如雲端硬碟通知入口）；② 這段程式裡的每一次查詢與寫入都被資料庫當成最高權限；③ 那段程式只要有任何一個小錯誤（例如照請求內容去查資料），影響就從「一家公司」放大成「全部公司」 |
| **得手什麼** | 那段程式碰得到的全部客戶資料 |
| **修正的做法** | 分三刀：① **具名系統身分**：新增 `system_context(name)`，無人登入的排程以具名方式宣告要繞過隔離，每次進入留一行具名警告（CM-1767）；② **拔掉「沒有客戶編號＝最高權限」**：只認兩個明說的訊號（具名系統身分、原廠租戶路徑），簽章通行證改以真實使用者身分組出（CM-1787）；③ **完全沒身分改成什麼都看不到**，並留一行警告指出是哪個呼叫點沒帶身分進來；新增「只看得到單一客戶」的 `tenant_context` 給機器端點用、提權唯讀查詢給登入流程用，代理程式與通知入口從「繞過全部隔離」收成「只看自己那家」（CM-1788）；④ 主專案排程全數接上具名系統身分（BE `3af065133`、`1125ceee1`） |
| **狀態** | ✅ 已修（FR-094.1：套件 commit `2bd93918` CM-1767、`9a0b4893` CM-1787、`fc1cadb9` CM-1788）。⚠️ 總表狀態欄未寫卡號，修正 commit 由本頁回查補上 |

## M13-2 🟠 公司帳號登入：匿名能過、帳號字串能騙查詢、加密連線不認人 {#m13-2}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #28；OWASP：A07 身分驗證失效、A05 注入攻擊、A04 加密機制失效） |
| **在哪裡** | 登入與權限模組：登入頁「用公司帳號登入」（串接客戶自己的員工帳號目錄，AD／OpenLDAP） |
| **攻擊面位置** | 登入頁本身——**不需要任何帳號**，設計上就對所有人開放。加密那一項另需攻擊者站在我們後端與客戶帳號目錄之間的網路路徑上 |
| **信任邊界位置** | **我們後端 ↔ 客戶的帳號目錄伺服器**。後端應該在線的這一側確認兩件事：「對面真的是客戶那台目錄伺服器」（驗憑證）、「這個人輸入的密碼目錄真的認」（用他的密碼再驗一次）。當時兩件都沒做：OpenLDAP 模式一律用匿名身分連線，使用者輸入的密碼從頭到尾沒被比對過；加密連線用的元件預設不驗憑證，程式也沒打開 |
| **元件端點位置** | `POST /api/1.0/login`（`jedi_iam/api/routes/login_route.py` 的 `LoginRoute`）、`POST /api/1.0/ldap/connect-test`（`LdapConnectTestRoute`）→ `jedi_iam/infra/adapter/authenticate_adapter/ldap/ldap_adapter.py` 的 `connect()`、`authenticate()`；設定欄位在 `jedi_iam/app/dto/login_config.py`；主專案透傳在 `api/user_auth_provider/serializers/ldap_config.py` 與 `app/user_auth_provider/service/ldap_service.py` 的 `init_config` |
| **駭客怎麼打** | ① 任何人打開登入頁，選「公司帳號登入」；② 匿名那一招：帳號填某位員工的名字、密碼隨便填，OpenLDAP 模式下系統只查「這個人存不存在」，查到就放行；③ 查詢那一招：帳號欄填 `*` 這類萬用字元，查詢條件被改寫成「隨便一個人」；④ 加密那一招：站在網路中間的人假扮成客戶的目錄伺服器，拿一張自己簽的證明，系統不檢查就把服務帳號與每位使用者輸入的密碼送過來，也可以回一份假的查詢結果冒充任何人 |
| **得手什麼** | 不用密碼就以任意員工身分登入；或在路上收下客戶的服務帳號與員工密碼 |
| **修正的做法** | ① **兩段式驗證**：先用服務帳號（或匿名）查出這個人的完整身分，再用「他的身分＋他輸入的密碼」真正向目錄登入一次，失敗才算密碼錯；② **查詢條件做跳脫**：兩處組查詢字串改用 `escape_filter_chars`，`*` 與括號不再被當成指令；AD 模式先拆網域再跳脫，順序不能反；③ **加密連線驗憑證**：第一版做成主機環境變數放 CA 檔，被首腦退回（同一組設定拆在兩個地方），重做成設定頁兩個欄位——「驗證伺服器憑證」開關與「信任憑證」欄位；勾了驗證走 `CERT_REQUIRED`，貼了 CA 就只信那一份、沒貼就用系統信任庫；④「測試連線」遇到憑證不對時回專屬錯誤碼，畫面能明說是憑證問題 |
| **狀態** | ✅ 已修（CM-1560，套件 commit `9b353ee5`＋重做 `20c67131`，主專案 `f503fba33`，前端 `78903b2`）。驗證：DEV 真實 AD 伺服器手測三態。⚠️ 之後前端 CM-2239（`16141ec`）依決策者裁定把**新建設定的「驗證伺服器憑證」改成預設不勾**——後端欄位預設仍是驗；客戶要自己勾才生效 |

## M20-2 🟠 4 個資料查詢畫面整個繞過隔離機制 {#m20-2}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #36；OWASP：A01 存取控制失效） |
| **在哪裡** | 客戶資料隔離（跨模組專項）：「我的任務」清單、判斷「這個人能做哪些操作」的權限總覽，以及兩份選單清單——四個資料庫視圖（view） |
| **攻擊面位置** | **任何一個登入的人，打開「我的任務」頁面就會撞到**；稽核儀表板上的待辦數量卡片讀的也是同一個視圖 |
| **信任邊界位置** | **客戶 ↔ 客戶**。資料庫視圖預設以「建立者」的身分去查底層資料，而建立者是系統最高權限帳號（可繞過隔離），等於牆對這四個視圖完全不存在 |
| **元件端點位置** | 視圖 `public.vw_user_job_queue`、`public.v_user_capabilities`、`public.v_user_routes`、`public.v_role_routes`；讀取端 `GET /api/1.0/flow-engine/task/queue`（`UserJobQueueRoute`）、`POST /api/1.0/jobs/my/list`（`MyJobListResource`）、儀表板待辦數 `FlowControlDashboardRepoImpl._my_pending_task_count`；視圖定義正本 `scripts/sql/view/vw_user_job_queue.sql` |
| **駭客怎麼打** | ① 任一家客戶的員工登入；② 打開「我的任務」；③ 視圖用最高權限帳號查底層，資料庫不過濾客戶；④ 清單上出現別家客戶的待辦（應該 0 筆、當時看到 39 筆） |
| **得手什麼** | 別家客戶的待辦任務與權限配置 |
| **修正的做法** | ① 「我的任務」與權限總覽兩支視圖加上「以查詢者身分執行」（`security_invoker = true`），並**寫進視圖定義正本**——直接重建視圖不會保留這個設定，只改線上不改正本，下次重建會靜默退回建立者身分、隔離全破；② 兩份選單視圖五個程式庫搜尋零讀者，直接刪除（依賴順序不可反）；③ 突變驗證：拿掉設定後子客戶看到 21 筆，設回來 0 筆 |
| **狀態** | ✅ 已修（CM-1771，BE commit `b502b0b47`，2026-09-16 複查確認） |

## M10-4 🟠 不是這個專案的人，換網址就看到別人專案的稽核判定與改善計畫 {#m10-4}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #57；OWASP：A01 存取控制失效） |
| **在哪裡** | 任務與成員管理模組（從稽核輪次管理那塊延伸查到）：稽核輪次清單與單筆、稽核發現矩陣、風險清單、改善計畫（POA&M）清單與單筆、稽核團隊名單、稽核計畫選單與儀表板。*（模組頁無逐條表，本條依總表描述與修正 commit 撰寫，與 [M12-1](#m12-1) 同一件）* |
| **攻擊面位置** | 已登入使用者可打的稽核輪次 API，同公司任何帳號，只要換網址上的專案或輪次編號。稽核團隊名單那支底下的資料表沒有客戶隔離，**跨客戶也打得到** |
| **信任邊界位置** | **使用者 ↔ 稽核輪次服務**。十支讀取入口開頭都該先確認「你是這個專案的成員」；當時都只驗登入。團隊名單那支寫成「查得到輪次才檢查成員」，別家客戶的輪次被資料庫隔離藏起來時查回空，檢查就整段跳過 |
| **元件端點位置** | 主專案 `api/project/routes/audit_round_route.py`：`/projects/{project_uid}/audit-rounds`、`/audit-round/{round_uid}`、`GET /ap/{ap_uid}/parties`、`GET /audit-round/{round_uid}/ar/findings`、`/ar/risks`、`/poam-items`、`/poam-item/{item_uid}`；`GET /project/{project_uid}/assessment-plans/menu`、`GET /project/{project_uid}/ap/{ap_uid}/dashboard`（`api/flow_control/routes/assessment_plan_route.py`）；服務在套件 `jedi_compliance_audit/app/service/audit_round_app_service.py`、`assessment_result_app_service.py`、`poam_app_service.py` 與主專案 `assessment_plan_app_service.py` |
| **駭客怎麼打** | ① 同公司、但不是某專案成員的員工登入；② 在網址上把專案或輪次編號換成別人的；③ 系統只驗登入；④ 讀到那個專案的稽核判定、風險評等、改善計畫與負責人姓名；⑤ 換成別家客戶的輪次打稽核團隊名單，拿到對方稽核團隊的姓名、Email、電話 |
| **得手什麼** | 別人專案的稽核判定、風險評等、改善計畫與負責人，以及跨客戶的稽核團隊聯絡資料 |
| **修正的做法** | ① 套件六支讀取方法新增 `_require_participant`（限任一參與者、不限角色），主專案路由改傳目前使用者（CM-2037）；② 團隊名單改走 `resolve_ap_and_check_participant`：計畫不存在 404、查不到輪次 404、非成員 403，不再「查得到才檢查」；③ 稽核計畫選單與儀表板新開 `project_read_guard.py`：專案查無 404、非成員 403，並核對網址上的輪次確實屬於網址上的專案；④ 套件七支讀取守門的使用者參數改成**必填**，以後新呼叫端忘了傳會直接報錯而不是安靜放行（CM-2172） |
| **狀態** | ✅ 已修，1.21.0 出貨（CM-2037 BE `cd9223592`＋套件 `f513ae88`；CM-2172 BE `0808b3c8a`＋套件 `7df95248`）。驗證：成員 200、同客戶非成員 403、跨客戶 404 |

## M12-1 🟠 同公司不是這個專案的人，換個網址就看得到稽核判定與改善計畫 {#m12-1}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #57；OWASP：A01 存取控制失效） |
| **在哪裡** | 稽核輪次管理模組：同 [M10-4](#m10-4)，十支讀取入口同一個漏法（這塊是發現的起點） |
| **攻擊面位置** | 已登入使用者可打的稽核輪次 API；計畫編號、輪次編號從別的功能回應裡就拿得到。團隊名單那支跨客戶也打得到 |
| **信任邊界位置** | **使用者 ↔ 稽核輪次服務**，以及**客戶 ↔ 客戶**（團隊名單那張表沒有客戶隔離，應用層是唯一防線） |
| **元件端點位置** | 同 [M10-4](#m10-4)：主專案 `api/project/routes/audit_round_route.py` 八支 GET 與 `api/flow_control/routes/assessment_plan_route.py` 兩支；套件 `jedi_compliance_audit/app/service/` 三支服務 |
| **駭客怎麼打** | ① 拿一個別人專案的輪次編號；② 依序打輪次、稽核發現、風險、改善計畫各支；③ 讀到比輪次清單機密得多的稽核判定內容；④ 團隊名單那支換別家客戶的計畫編號照樣回名單 |
| **得手什麼** | 同 [M10-4](#m10-4) |
| **修正的做法** | 同 [M10-4](#m10-4)：八支先由 CM-2037 補「任一參與者」守門；首腦驗收退回後由 CM-2172 補團隊名單「查不到輪次回找不到」、選單與儀表板的成員檢查與輪次歸屬比對，並把守門參數改成必填 |
| **狀態** | ✅ 已修，1.21.0 出貨（八支 CM-2037，BE `cd9223592`；團隊名單、選單、儀表板 CM-2172，BE `0808b3c8a`＋套件 `7df95248`） |

## M11-1 🟠 知道系統安全計畫編號，就能整份下載成 Excel——而且跨客戶 {#m11-1}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #148；OWASP：A01 存取控制失效） |
| **在哪裡** | 合規文件核心：專案系統安全計畫（SSP）頁的「下載 Excel 樣板（帶現有內容）」 |
| **攻擊面位置** | 已登入、角色帶「看資源庫」權限的帳號，**不必是那個專案的成員**；計畫編號從別的功能回應裡就拿得到 |
| **信任邊界位置** | **使用者 ↔ 合規文件服務**，也是**客戶 ↔ 客戶**：合規文件那批資料表沒有客戶歸屬欄位，資料庫隔離擋不到，應用層是唯一防線。隔壁「匯出」那支早就檢查「你是這份計畫的參與者」，這支沒有 |
| **元件端點位置** | `GET /api/1.0/ssp/{ssp_uid}/excel-template`（主專案 `api/module_frame/routes/ssp_import_template_route.py` 的 `SspScopedImportTemplateResource`）→ `app/module_frame/service/ssp_import_template_app_service.py` 的 `generate_for_ssp()` |
| **駭客怎麼打** | ① 任一帶「看資源庫」權限的帳號，甚至是別家客戶的；② 拿到一份系統安全計畫的編號；③ 打這支下載網址，選帶現有內容；④ 一個請求拿走九張工作表：人員姓名、email、電話、地址，設備與 IP、MAC、作業系統，繼承的授權，每一條控制項的做法 |
| **得手什麼** | 另一家公司的整份稽核底稿 |
| **修正的做法** | ① 服務新增參數 `ssp_permission_checker`，`generate_for_ssp` 第一行 `require_participant(ssp_uid)`；② **檢查服務沒注入時直接 403**——刻意不照隔壁「有注入才檢查」的寫法，那種寫法在 DI 漏接時會靜默放行；③ DI 補上注入 |
| **狀態** | ✅ 已修（FR-114 CM-2188，BE commit `82618a297`，1.21.0 出貨）。驗證：成員 200、非成員 403、別家客戶 404。⚠️ 模組頁寫「從範本下載那支也順手補上程式層檢查」，commit 註明**未做**：查無既有「範本可見性」判斷，依規不自創規則，範本版維持靠資料庫隔離 |

## M24-2 🟠 重建專案雲端資料夾不檢查專案是不是你的，背景還用系統身分載入 {#m24-2}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #200；OWASP：A01 存取控制失效） |
| **在哪裡** | 主系統的雲端硬碟整合：「重建專案資料夾」 |
| **攻擊面位置** | 任何一家客戶的任何登入帳號（只需要有雲端硬碟整合的授權），手上有別家客戶的專案編號即可 |
| **信任邊界位置** | **客戶 ↔ 客戶**，跨在**請求 ↔ 背景工作**這條線上。入口只驗登入，把網址上的專案編號原封不動排進背景工作；背景以系統身分跑，載入專案樹時寫死最高權限、不看公司，於是別家的專案被當成自己的來處理 |
| **元件端點位置** | `POST /api/1.0/integrations/google-drive/projects/{project_uid}/init-folders`（`DriveSyncProjectInitFoldersRoute`）→ `app/cloud_integration/service/drive_sync_admin_service.py` 的 `trigger_init_project_folders()` → 背景 `handlers/init_project_folders_handler.py` → `project_tree_loader.py` 的 `ProjectTreeLoader.load`；證據回寫在 `handlers/import_drive_file_handler.py` |
| **駭客怎麼打** | ① A 公司任一帳號登入；② 拿 B 公司的專案編號送「重建專案資料夾」；③ 系統把 B 的整個專案結構建進 A 的雲端硬碟；④ A 往那些資料夾丟檔案，下一輪同步時被寫成 B 公司任務的證據（已在開發環境實際打過） |
| **得手什麼** | 別家客戶的專案結構，並能把假證據摻進別家客戶的稽核 |
| **修正的做法** | ① **入口補守門**：排工作前以呼叫者身分（受資料庫隔離）查專案，查不到 404；再確認呼叫者是該專案的管理者，否則 403；守門只加在手動入口，建專案等自動路徑的編號是伺服器剛建的、不受影響；② **背景縱深**：專案樹載入多帶「預期的公司」，專案公司與工作公司不同就終止、不重試、不建任何資料夾；③ 證據回寫前反查任務所屬專案、比對公司，不同或查無一律拒絕，檢查元件沒注入時也拒絕；④ DEV 盤點既有 25,409 筆對應全部同公司，第二層不會擋到現有資料 |
| **狀態** | ✅ 已修，1.21.0 出貨（CM-2201，BE commit `73f5f90af`）。驗證：自己管理的專案 200、同公司非成員 403、別家公司 404 |

## M05-6 🟡 還原歷史版本時不檢查指定的版本是不是這份問卷的 {#m05-6}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #38；OWASP：A01 存取控制失效） |
| **在哪裡** | 問卷模組：填答頁的「還原到某個歷史版本」 |
| **攻擊面位置** | 對某份問卷有寫入權限的人（被指派人本人或專案管理者）——自己開一份就有 |
| **信任邊界位置** | **使用者 ↔ 問卷服務**。系統確認了「你能改這一份」，但沒確認「你指定的歷史版本屬於這一份」——歷史版本編號原本只憑編號直撈 |
| **元件端點位置** | `POST /api/1.0/project-survey/history/revert`（套件 `jedi_survey/api/routes/question_answer_history_route.py` 的 `RevertQuestionAnswerHistoryRoute`）→ `app/service/question_answer_history_service.py` 的 `revert_question_answer_from_history()` |
| **駭客怎麼打** | ① 在自己有權限的那份問卷按「還原歷史版本」；② 用第 4 條拿到別部門問卷的歷史版本編號，填進還原請求；③ 系統只檢查你對自己這份有沒有權限；④ 別人的答案被整份複製進你這一份——不只看到，是把內容搬進來 |
| **得手什麼** | 別部門問卷某一版的完整答案 |
| **修正的做法** | ① 取出歷史版本後，比對它記的問卷編號與這次操作的問卷是不是同一份，不同就回「找不到」；② 比對放在推播之前——還原成功會對同一份問卷的其他人推「重新載入」，比對失敗若不先擋會推出一個空事件；③ 權限照舊走「被指派人本人或專案管理者」，沒有放寬 |
| **狀態** | ✅ 已修（FR-114.1-4a／CM-2024，套件 commit `81051e8a`） |

## M19-1 🟡 設備與資訊系統清冊的讀取只檢查登入、不檢查權限 {#m19-1}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #43；OWASP：A01 存取控制失效） |
| **在哪裡** | 設備與資訊系統清冊模組：設備清單、單筆、引用、下拉，以及資訊系統清單、單筆、下拉 |
| **攻擊面位置** | 已登入使用者可打的資產 API。管理員以為收掉了某人（約聘人員、外部顧問）的「查看設備」權限，其實只是側邊選單藏起來，網址貼上就進得去 |
| **信任邊界位置** | **使用者 ↔ 資產服務**。同一支檔案的寫入功能都有能力點檢查，讀取那七支沒有——程式碼裡還寫明這是刻意的取捨。跨客戶仍被資料庫隔離擋住，外洩範圍在同一家客戶內 |
| **元件端點位置** | 套件 `jedi_asset/api/routing.py`：`POST /devices`、`GET /devices/menu`、`GET /device/{uid}`、`GET /device/{uid}/references`（`device_route.py`）；`GET /information-systems/menu`、`POST /information-systems/list`、`GET /information-system/{uid}`（`information_system_route.py`） |
| **駭客怎麼打** | ① 被管理員拿掉查看權限的帳號登入；② 側邊選單看不到入口，直接貼上清冊網址；③ 系統只驗登入；④ 拿到設備的主機名稱、網路位址、作業系統版本，與資訊系統的機密性等級、部署模式、授權邊界、負責人——等於客戶內部網路與安全態勢的地圖 |
| **得手什麼** | 客戶完整的設備清冊與資訊系統清冊 |
| **修正的做法** | ① 七支讀取各補一道 `capability_required`，沿用既有的 `device.read`、`information-system.read` 兩顆權限，沒新造；沒有權限回固定的拒絕代碼 GRC_403022（不是空清單）；② **先補權限資料再上守門**：五處下拉選單都靠這兩支讀取，缺權限時會安靜變空、不報錯，所以另一張卡先替缺這兩顆的既有角色補上（BE migration，隨出貨，CM-2032）；③ 同卡另修「只有修改權限的人送一個停用欄位就能讓系統從選單消失」：修改的請求格式拿掉該欄位，停用只能走刪除 |
| **狀態** | ✅ 已修（FR-114.1-6／CM-2029，套件 commit `0ecdd3a1`；權限資料 CM-2032，BE `fc9fca7d0`） |

## M03-12 🟡 查掃描執行紀錄少了「你是不是這個專案的人」這道檢查 {#m03-12}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #47；OWASP：A01 存取控制失效） |
| **在哪裡** | 弱點檢測整合模組：任務的掃描執行紀錄（歷次掃描的分組、指派、結果摘要） |
| **攻擊面位置** | 已登入使用者可打的檢測 API，只需要任務編號；同一家客戶內任何成員 |
| **信任邊界位置** | **使用者 ↔ 檢測服務**。同一個服務裡另外八個吃任務編號的功能都先查「你是不是這個專案的人」，**只有這一支漏掉** |
| **元件端點位置** | `GET /api/1.0/detection-tools/jobs/{job_uid}/executions`（套件 `jedi_detection/api/routes/detection_tool_route.py` 的 `DetectionToolJobExecutionsRoute`）→ `app/service/detection_orchestration_service.py` 的 `list_executions()` |
| **駭客怎麼打** | ① 同一家客戶的任一成員；② 拿到別人專案的任務編號；③ 查它的掃描執行紀錄；④ 讀到別人專案的掃描歷史 |
| **得手什麼** | 別人專案的掃描歷程與結果摘要 |
| **修正的做法** | ① 先查出任務，再呼叫 `assert_project_participant`（看紀錄是讀取，門檻是任一參與者，不是寫入端用的「被指派人或管理者」）；② 任務不存在時拋「找不到任務」而不是回空清單——空清單分不出「沒資料」與「沒這個任務」，會變成探測管道 |
| **狀態** | ✅ 已修（套件 commit `5a9baf2b`，卡號 CM-2042）。⚠️ 總表狀態欄寫 CM-2040，但 CM-2040 的 commit 修的是資源庫範本寫入；這條實際由 CM-2042 那顆 commit 修好（commit 訊息點名 #47） |

## M05-3 🟡 列出任務問卷不限範圍，送空白請求就回整個客戶的全部 {#m05-3}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #48；OWASP：A01 存取控制失效） |
| **在哪裡** | 問卷模組：「列出任務問卷」 |
| **攻擊面位置** | 已登入使用者可打的問卷 API，任何登入帳號，不需要知道任何編號 |
| **信任邊界位置** | **使用者 ↔ 問卷服務**。查詢條件可以全空，而底層「條件全空就不過濾」；這支同時是第 2 條的入場券（給出所有問卷編號） |
| **元件端點位置** | `POST /api/1.0/task-surveys`（套件 `jedi_survey/api/routes/task_survey_route.py` 的 `TaskSurveysRoute`）→ `app/service/task_survey_service.py` 的 `get_task_surveys()` |
| **駭客怎麼打** | ① 任一登入帳號；② 送一個空白查詢；③ 拿到全公司問卷的編號、所屬專案、狀態、受檢設備與部門；④ 拿編號去打第 2 條讀答案 |
| **得手什麼** | 全公司問卷清單，以及讀取別人答案所需的編號 |
| **修正的做法** | ① 任務編號（或任務識別碼）改成二擇一必填；② 取到第一筆後做歸屬檢查：由任務反查專案，確認呼叫者是參與者；③ 單筆讀取 `get_task_survey_by_uid` 同補守門，原本兩支重複實作的讀取收斂成一支（避免補一支漏一支）；④ 後續 CM-2114 改用宿主注入的唯一歸屬判斷 |
| **狀態** | ✅ 已修（CM-2034，套件 commit `83fc34e5`；歸屬判斷 CM-2114 `034eef0e`） |

## M05-4 🟡 列出填答歷史不限範圍 {#m05-4}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #49；OWASP：A01 存取控制失效） |
| **在哪裡** | 問卷模組：填答歷史清單與歷史版本下拉 |
| **攻擊面位置** | 已登入使用者可打的問卷 API，任何登入帳號 |
| **信任邊界位置** | **使用者 ↔ 問卷服務**。查詢條件可以全空就不過濾；這支同時提供第 1 條與第 6 條需要的歷史版本編號 |
| **元件端點位置** | `POST /api/1.0/project-survey/histories`（`QuestionAnswerHistoriesRoute`）、`GET /api/1.0/project-survey/histories/menu/{uid}`（`QuestionAnswerHistoryMenuRoute`），套件 `jedi_survey/api/routes/question_answer_history_route.py` → `app/service/question_answer_history_service.py` |
| **駭客怎麼打** | ① 任一登入帳號；② 送一個空白查詢到填答歷史清單；③ 看出全公司跨所有專案裡誰在什麼時候改了哪份問卷；④ 同時拿到歷史版本編號，可以接著讀明細（第 1 條）或還原進自己的問卷（第 6 條） |
| **得手什麼** | 全公司的填答動態，以及讀取或搬運別人答案的編號 |
| **修正的做法** | ① 問卷識別碼或問卷編號二擇一必填；② 反查任務問卷後補 `assert_task_survey_reader`，並把問卷編號**強制寫回查詢條件**；③ 下拉那支同補守門；④ 後續 CM-2114 改用宿主注入的唯一歸屬判斷 |
| **狀態** | ✅ 已修（CM-2034，套件 commit `83fc34e5`；歸屬判斷 CM-2114 `034eef0e`） |

## M05-5 🟡 問卷討論列表送空查詢就把全公司討論撈回來 {#m05-5}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #50；OWASP：A01 存取控制失效） |
| **在哪裡** | 問卷模組：問卷的討論串列表 |
| **攻擊面位置** | 已登入使用者可打的問卷 API，任何登入帳號 |
| **信任邊界位置** | **使用者 ↔ 問卷服務**。兩個查詢條件都可以不填、不填就不過濾；也沒有檢查呼叫者是不是該專案的人 |
| **元件端點位置** | `POST /api/1.0/survey-discussions`（套件 `jedi_survey/api/routes/survey_discussion_route.py` 的 `SurveyDiscussionsRoute`）→ `app/service/survey_discussion_service.py` 的 `get_survey_discussions()` |
| **駭客怎麼打** | ① 任一登入帳號；② 送一個空查詢到討論列表；③ 拿回全公司的討論串，含你完全沒參與的專案；④ 讀到稽核過程的缺失細節與審查意見 |
| **得手什麼** | 全公司問卷討論串（缺失細節與審查意見） |
| **修正的做法** | ① 問卷識別碼與任務問卷識別碼**兩者皆必填**；② 由任務問卷反查任務後補讀取守門；③ 主專案替這支服務接上反查歸屬所需的依賴（BE `4a5650c5d`）；④ 後續 CM-2114 改用宿主注入的唯一歸屬判斷 |
| **狀態** | ✅ 已修（CM-2034，套件 commit `83fc34e5`、BE `4a5650c5d`；歸屬判斷 CM-2114） |

## M05-7 🟡 多人同時填問卷的「房間」想進哪間就進哪間 {#m05-7}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #51；OWASP：A01 存取控制失效） |
| **在哪裡** | 問卷模組：多人同時填同一份問卷的即時協作（誰在編輯哪一題、即時同步答案） |
| **攻擊面位置** | 即時通訊頻道 `/socket/fill-survey`，任何登入帳號；房號就是任務問卷編號 |
| **信任邊界位置** | **使用者 ↔ 即時協作頻道**。進房前應該確認是該問卷所屬專案的人；當時只驗登入，而且其他事件（更新、編輯、結束編輯）不必先進房就能指定房號 |
| **元件端點位置** | 套件 `jedi_survey/app/handler/fill_survey_socketio_handler.py`：`on_join`、`on_leave`、`on_editing`、`on_update`、`on_edit`、`on_endedit` 六個事件；主專案 `config/socketio_namespaces.py` 註冊 `/socket/fill-survey` |
| **駭客怎麼打** | ① 任一登入帳號連上填問卷頻道；② 用第 3 條拿到的問卷編號當房號加入；③ 旁聽別專案正在編輯中的問卷；④ 觸發讀取當下答案快照，拿到還沒存檔的作答 |
| **得手什麼** | 別專案正在填寫中的問卷內容 |
| **修正的做法** | ① 新增 `_assert_room_member`（以房號反查任務問卷、檢查專案參與者）；② **六個事件全部呼叫**——這些事件不必先加入就能指定房號，只守加入等於沒守；③ 讀答案快照那條路用同一道檢查；④ 後續 CM-2114 改用宿主注入的唯一歸屬判斷 |
| **狀態** | ✅ 已修（CM-2034，套件 commit `83fc34e5`；歸屬判斷 CM-2114 `034eef0e`） |

## M06-5 🟡 知道一個稽核輪次編號就能讀走別人的階段歷程 {#m06-5}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #52；OWASP：A01 存取控制失效） |
| **在哪裡** | 稽核流程模組：稽核輪次的階段歷程（每次推進、退回的紀錄） |
| **攻擊面位置** | 已登入使用者可打的稽核流程 API，只需要一個輪次編號 |
| **信任邊界位置** | **使用者 ↔ 稽核輪次服務**。網址上明明帶了專案編號，**程式從頭到尾沒拿它比對過**——等於「假裝有檢查」 |
| **元件端點位置** | `GET /api/1.0/project/{project_uid}/audit-round/{round_uid}/stage/transitions`（主專案 `api/flow_engine/routes/stage_rollback_route.py` 的 `RoundStageTransitionsResource`）→ 套件 `jedi_compliance_audit/app/service/audit_round_app_service.py` 的 `list_stage_transitions()` |
| **駭客怎麼打** | ① 任一登入帳號；② 網址專案隨便填、輪次填別人的；③ 讀到每次推進或退回的操作者帳號、暱稱、從哪一階段到哪一階段；④ 以及退回理由全文——稽核人員親筆寫的「哪裡有問題」 |
| **得手什麼** | 別人稽核輪次的流程軌跡與退回理由 |
| **修正的做法** | ① 套件 `list_stage_transitions` 用起網址上的專案編號：比對這一輪是否真的屬於該專案，不符回 404；② 帶上目前使用者，限該專案任一參與者；③ 歸屬不符回 404 而非 403——回 403 等於確認「這個輪次存在、只是不屬於你」，可被拿來列舉編號；④ 後續 CM-2172 把使用者參數改成必填 |
| **狀態** | ✅ 已修（CM-2035，套件 commit `3e82b625`、BE `e2c32258d`；參數必填 CM-2172 `7df95248`） |

## M06-6 🟡 「查目前在哪個階段」用的專案編號和輪次編號兩者不核對 {#m06-6}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #53；OWASP：A01 存取控制失效） |
| **在哪裡** | 稽核流程模組：稽核輪次的目前階段、推進、退回 |
| **攻擊面位置** | 已登入使用者可打的稽核流程 API；拿自己有權限的專案編號配上別人的輪次編號 |
| **信任邊界位置** | **使用者 ↔ 稽核輪次服務**。角色用網址的專案編號查、資料用網址的輪次編號撈，兩者從不核對；角色不符也只是把「能不能推進」標成否，**其他資料照樣整包回傳**。推進與退回寫法相同，當時只是剛好被下游套件的另一道檢查擋住 |
| **元件端點位置** | 主專案 `api/flow_engine/__init__.py`：`GET /project/{project_uid}/audit-round/{round_uid}/stage/info`（`StageInfoResource`）、`POST …/stage/advance`、`POST …/stage/rollback` → `app/flow_engine/service/stage_advance_service.py`、`stage_rollback_service.py`；共同入口 `app/flow_engine/service/stage_project_guard.py` |
| **駭客怎麼打** | ① 在自己有角色的專案 A 之下；② 網址專案填 A、輪次填別的專案 B 的；③ 角色檢查用 A 查、過關；④ 回傳的是 B 的階段資訊與流程脈絡 |
| **得手什麼** | 別人稽核輪次的階段與流程資訊（推進／退回當時靠下游檢查擋住） |
| **修正的做法** | ① 新增 `resolve_project_id_for_round` 當三處共同入口：解析網址專案 → 確認這一輪確實屬於它 → 回傳專案給角色查詢用；② 查詢、推進、退回三個入口全改走它，不再依賴下游套件擋；③ 陷阱：查目前階段有一條「沒有當前階段就提前回傳」的路，檢查若放在角色那步會被整段跳過，所以放在解析流程之後、提前回傳之前 |
| **狀態** | ✅ 已修（CM-2035，BE commit `e2c32258d`） |

## M10-1 🟡 對專案成員查詢送空白查詢，一次拿到全公司成員名冊 {#m10-1}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #54；OWASP：A01 存取控制失效） |
| **在哪裡** | 任務與成員管理模組：專案成員清單與下拉。*（模組頁無逐條表，本條依總表描述與修正 commit 撰寫）* |
| **攻擊面位置** | 已登入使用者可打的成員 API，同一家公司裡任何能登入的人，不需要知道任何編號 |
| **信任邊界位置** | **使用者 ↔ 成員服務**。專案編號可以不填、不填就不過濾（[M10-9](#m10-9) 的底層規則）；也不檢查呼叫者是不是該專案的人 |
| **元件端點位置** | 套件 `jedi_task_platform/participant/api/routing.py`：`POST /project-participants`（`ProjectParticipantsRoute`）、`GET /project-participants/menu` → `participant/app/service/project_participant_service.py` 的 `get_project_participants()`、`get_project_participant_menus()` |
| **駭客怎麼打** | ① 任一登入帳號；② 對成員清單送一個連專案編號都不填的空白查詢；③ 底層不過濾；④ 一次拿到公司內全部專案的成員名冊（誰在哪個專案、什麼角色） |
| **得手什麼** | 全公司專案成員名冊與角色配置 |
| **修正的做法** | ① 專案編號改**必填**；② 補 `assert_project_participant` 歸屬檢查；③ 守門介面新增 `assert_participant`，主專案兩處實作一起補——兩處共用同一個模組級單例，只補一邊會被後掛載那個覆蓋，全站呼叫都會報錯（BE `05870c4cc`） |
| **狀態** | ✅ 已修（CM-2036，套件 commit `3ed750cd`、BE `05870c4cc`）。驗證：非參與者 403、缺編號 400 |

## M10-2 🟡 換一個編號，就讀到別人專案群組層與控制項層的人員配置 {#m10-2}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #55；OWASP：A01 存取控制失效） |
| **在哪裡** | 任務與成員管理模組：群組與控制項層的成員（誰負責哪個控制群組、哪個控制項）。*（模組頁無逐條表，本條依總表描述與修正 commit 撰寫）* |
| **攻擊面位置** | 已登入使用者可打的控制項成員 API，同公司任何帳號 |
| **信任邊界位置** | **使用者 ↔ 成員服務**。專案編號可不填、不檢查歸屬，而且「含繼承」那支會連帶把整個專案層的成員一起回傳 |
| **元件端點位置** | 修正前：套件 `jedi_task_platform/participant` 的 `/project-control-participants`、`/project-control-participants/menu` → `participant/app/service/project_control_participant_service.py` 的 `get_control_participant_menu()`、`get_control_participants_with_inherits()` |
| **駭客怎麼打** | ① 同公司、但不是某專案成員的人；② 在網址上換成別人專案的編號；③ 讀到那個專案群組層與控制項層的完整人員配置 |
| **得手什麼** | 別人專案的控制項負責人配置 |
| **修正的做法** | ① 兩支讀取的專案編號改必填、補歸屬檢查，順帶把回傳範圍收斂到控制項層、不再連帶洩漏整個專案的成員（CM-2036）；② 之後查出寫入端早已失效、讀取端前端也沒在用，決策者裁定整組退役：群組與控制項成員四支網址連同只被它們呼叫的服務方法一起拿掉，資料表保留（CM-2072） |
| **狀態** | ✅ 已修（CM-2036，套件 commit `3ed750cd`）；網址後續退役（CM-2072，套件 `f1fa23c1`） |

## M10-3 🟡 不是專案的人直接開專案摘要報告，讀到稽核結論與歷史版本 {#m10-3}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #56；OWASP：A01 存取控制失效） |
| **在哪裡** | 任務與成員管理模組（延伸到專案摘要報告）：專案摘要報告與其歷史版本。*（模組頁無逐條表，本條依總表描述與修正 commit 撰寫）* |
| **攻擊面位置** | 已登入使用者可打的摘要報告 API，同公司任何帳號，只要有報告或專案編號 |
| **信任邊界位置** | **使用者 ↔ 摘要報告服務**。讀報告、讀歷史都只驗登入；而歷史版本常保留後來被刪掉的稽核發現 |
| **元件端點位置** | 主專案 `api/project_summary_report/__init__.py`：`POST /project-summary-reports`、`GET /project-summary-report/{uid}`、`POST /project-summary-report/histories`、`GET /project-summary-report/history/{uid}`、`GET /project-summary-report/export/{uid}` → `app/project_summary_report/service/project_summary_report_service.py`、`project_summary_report_history_service.py` |
| **駭客怎麼打** | ① 同公司、不是某專案成員的人；② 直接打開那個專案的摘要報告；③ 讀到稽核結論全文；④ 再翻歷史版本，找到後來被刪掉的稽核發現 |
| **得手什麼** | 別人專案的稽核結論與被刪改前的版本 |
| **修正的做法** | ① 報告服務四支、歷史服務兩支讀取方法補 `_require_participant`／`_require_participant_by_report_uid`；② 列表方法改成只回目前使用者有參與的專案；③ 後續 CM-2211 另補「還原歷史版本」要比對歷史確實屬於這份報告，不符 404（同公司的人拿別份報告的歷史就能蓋掉本報告） |
| **狀態** | ✅ 已修（CM-2037，BE commit `cd9223592`；還原比對 CM-2211 `fe928859f`） |

## M10-5 🟡 對任務指派清單送空白查詢，一次拿到全公司一萬多筆指派紀錄 {#m10-5}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #58；OWASP：A01 存取控制失效） |
| **在哪裡** | 任務與成員管理模組：任務指派清單。*（模組頁無逐條表，本條依總表描述與修正 commit 撰寫）* |
| **攻擊面位置** | 已登入使用者可打的指派 API，同公司任何帳號，不需要任何編號 |
| **信任邊界位置** | **使用者 ↔ 成員服務**。查詢條件全空就不過濾，也不檢查歸屬 |
| **元件端點位置** | 套件 `jedi_task_platform/participant/api/routing.py`：`POST /task-assignees`（`TaskAssigneesRoute`）→ `participant/app/service/task_assignee_service.py` 的 `get_task_assignees()` |
| **駭客怎麼打** | ① 任一登入帳號；② 對任務指派清單送一個空白查詢；③ 拿回公司內全部一萬多筆任務指派紀錄（誰負責哪個任務） |
| **得手什麼** | 全公司任務分工表 |
| **修正的做法** | ① 專案編號與任務識別碼**二擇一必填**；② 只給任務識別碼時反查任務所屬專案再做歸屬比對；③ 補成員檢查 |
| **狀態** | ✅ 已修（CM-2036，套件 commit `3ed750cd`） |

## M10-9 🟡 底層「查詢條件全沒填就不過濾」，空白查詢等於回整張表 {#m10-9}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #59；OWASP：A01 存取控制失效、A06 不安全的設計） |
| **在哪裡** | 共用的取資料底層（所有套件的資料存取都繼承它）。*（模組頁無逐條表，本條依總表描述與修正 commit 撰寫）* |
| **攻擊面位置** | 所有把請求內容直接展開成查詢條件的清單 API；任何登入帳號送空白查詢 |
| **信任邊界位置** | **使用者給的查詢條件 ↔ 資料存取層**。底層把「沒有條件」解讀成「全部都要」；只要某支 API 沒自己把關，空白查詢就回整張表 |
| **元件端點位置** | 套件 `jedi_common/session/database/repository/base_repository_impl.py` 的 `_get_pager_list_query()`（`if not filter_dict: return query`）；受影響的入口如 `POST /project-participants`、`POST /task-assignees`、`POST /task-surveys`、`POST /project-survey/histories`、`POST /survey-discussions` |
| **駭客怎麼打** | ① 任一登入帳號；② 找一支清單 API，送 `{}`；③ 底層看到條件是空的，直接不加任何過濾；④ 這張表的所有資料一次回來（跨客戶由資料庫隔離擋，同客戶內全部外洩） |
| **得手什麼** | 那張表在同客戶內的全部資料 |
| **修正的做法** | **裁定逐支補、底層不動**：有洞的入口各自把查詢鍵改成必填並補歸屬檢查——成員與指派三支（CM-2036）、問卷六組（CM-2034）。底層 `_get_pager_list_query` 維持原樣，因為全專案有大量合法的「列出全部」呼叫依賴它。反悔條件寫在模組頁：新功能第四次撞到同一件事，就改底層 |
| **狀態** | ✅ 已修（逐支止血：CM-2036 套件 `3ed750cd`、CM-2034 套件 `83fc34e5`）；底層規則仍在 |

## M15-2 🟡 AI 儀表板呼叫的 26 支查詢完全不檢查「你能不能看這筆資料」 {#m15-2}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #60；OWASP：A01 存取控制失效） |
| **在哪裡** | AI 儀表板模組：AI 選中查詢之後，系統代替使用者去呼叫那支查詢 |
| **攻擊面位置** | 生成儀表板的 API，任何登入帳號；配合 [M15-1](#m15-1) 能誘導 AI 選中任一支 |
| **信任邊界位置** | **AI 儀表板 ↔ 各業務服務**。儀表板不走正常的網址入口、直接叫服務層，所以網址入口的權限檢查根本沒被碰到——不是失效，是這條路不經過 |
| **元件端點位置** | `POST /api/1.0/ai-dashboard/auto-generate`（套件 `jedi_ai_dashboard/api/routing.py`）→ `app/service/data_api_service.py` 的 `call_api()`；26 支查詢的登記在主專案 `di_containers/dashboard_apis/*.py`；權限判定 `common.authz.viewer_has_capability` |
| **駭客怎麼打** | ① 一般員工打開 AI 儀表板；② 要求「列出所有使用者與他們的角色」；③ AI 選中帳號查詢；④ 系統直接叫服務層，不問權限；⑤ 拿到整份組織架構、誰的權限最大、公司有哪些客戶——同樣的資料走正常畫面是要權限的 |
| **得手什麼** | 組織架構、權限配置、客戶清單 |
| **修正的做法** | ① 套件在登記簿契約加 `required_capabilities` 欄位，`call_api` 執行前檢查；**沒填＝拒絕**，忘了填的新查詢不會悄悄變成無守門端點；真要公開必須顯式寫 `PUBLIC_CAPABILITY`；② 給 AI 看的查詢目錄只列這個人有權呼叫的；③ 主專案 26 支逐一申報，對照側邊選單既有的那套權限；④ 宿主沒接權限判定元件時也一律拒絕；⑤ 後續 CM-2232 加守衛測試，檢查 26 支申報的參數真的收得下 |
| **狀態** | ✅ 已修（CM-2038，套件 commit `07126b3d`、BE `8f94e249d`） |

## M15-3 🟡 AI 儀表板查專案清單時把呼叫者當成管理員 {#m15-3}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #61；OWASP：A01 存取控制失效） |
| **在哪裡** | AI 儀表板模組：專案清單那支查詢 |
| **攻擊面位置** | 生成儀表板的 API，任何登入帳號 |
| **信任邊界位置** | **AI 儀表板 ↔ 專案服務**。這支查詢寫死「視同管理員」去繞過「只看自己參與的專案」——當時是刻意對齊舊版儀表板「看本客戶全部專案」，不是寫錯，但等於給所有人開了後門 |
| **元件端點位置** | `POST /api/1.0/ai-dashboard/auto-generate` → 主專案 `app/flow_control/service/project_service.py` 的 `get_projects_for_dashboard()`（原本 `is_admin=True`）；登記在 `di_containers/dashboard_apis/project.py` |
| **駭客怎麼打** | ① 任一員工打開 AI 儀表板；② 要求「列出所有稽核專案」；③ 系統以管理員視角查；④ 回來的是該客戶底下全部專案——哪家客戶正在被查什麼 |
| **得手什麼** | 本客戶全部稽核專案清單 |
| **修正的做法** | ① `get_projects_for_dashboard` 改成 `is_admin=False`，與規劃頁列表同一條規則：只回呼叫者參與的專案；② 收掉路由檔裡已過期的註解（它還寫著「AI 儀表板走管理員視角不受影響」）；③ 副作用已被決策者接受：專案管理者之後也只看得到自己參與的專案，要全局視圖應另開管理者專用端點 |
| **狀態** | ✅ 已修（CM-2038，BE commit `8f94e249d`） |

## M17-2 🟡 AI 儀表板這條路繞過守門直接讀公告 {#m17-2}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #62；OWASP：A01 存取控制失效） |
| **在哪裡** | 公告模組：AI 儀表板代替使用者呼叫公告清單 |
| **攻擊面位置** | 生成儀表板的 API，任何登入帳號 |
| **信任邊界位置** | **AI 儀表板 ↔ 公告服務**，以及**我們的系統 ↔ 外部 AI**。公告服務靠一個「要不要依部門過濾」的旗標決定可見範圍，這條路沒人帶這個旗標，於是整份全回；前三筆還會原文送到外部 AI 服務。檢視時這條路會當場出錯——是格式錯誤把守門缺失遮住了，不是守門 |
| **元件端點位置** | `POST /api/1.0/ai-dashboard/auto-generate` → 套件 `jedi_ai_dashboard/app/service/data_api_service.py` 的 `call_api()` → 套件 `jedi_bulletin/app/service/bulletin_service.py` 的清單方法；申報在主專案 `di_containers/dashboard_apis/bulletin.py` |
| **駭客怎麼打** | ① 任一登入帳號打開 AI 儀表板；② 說「列出所有公告」；③ 拿到整個客戶底下的全部公告——含別部門的、停用的草稿、還沒到發布時間的；④ 前三筆內容同時被送到外部 AI |
| **得手什麼** | 全客戶的公告（含草稿與未發布的），並讓內容流到外部 AI |
| **修正的做法** | ① 公告查詢申報需要「看公告」權限；② 新增 `fixed_params`：申報處硬綁 `auth=True`、最後套用、覆寫呼叫端——靠 AI 或呼叫端帶等於沒帶，它們不知道有這個參數；③ 修掉格式錯誤（注入的使用者物件轉成一般字典），這條路恢復暢通後守門才真正生效；④ 公告本身的可見範圍規則另由 [M17-3](#m17-3)／[M17-4](#m17-4) 收口 |
| **狀態** | ✅ 已修（CM-2038，套件 commit `07126b3d`、BE `8f94e249d`） |

## M20-3 🟡 查任務詳細資料收了專案編號卻從頭到尾沒用 {#m20-3}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #63；OWASP：A01 存取控制失效） |
| **在哪裡** | 客戶資料隔離（跨模組專項）：規劃頁點開任務、修改任務、刪除任務 |
| **攻擊面位置** | 已登入使用者可打的任務 API，任何登入的人，只要在任務列表上看過一筆別人專案的任務編號 |
| **信任邊界位置** | **使用者 ↔ 任務服務**。查詢完全沒有守門；修改、刪除只驗「你在你填的專案是不是管理者」，沒核對「這筆任務屬不屬於那個專案」。網址上的專案填真的、全零、亂打都查得到同一筆 |
| **元件端點位置** | `GET`／`PUT`／`DELETE /api/1.0/project/{project_uid}/job/{job_uid}`（主專案 `api/flow_control/routes/job_route.py` 的 `ProjectJobDetailResource`）→ `app/flow_control/service/job_service.py`；歸屬判斷 `infra/readmodel/tasks/job_project_ownership_query.py` 的 `JobProjectOwnershipQuery` |
| **駭客怎麼打** | ① 任一登入帳號；② 網址專案隨便填、任務填別人的；③ 讀到完整任務內容；④ 若自己是某專案的管理者，拿那個專案編號配上別人的任務編號，連改、刪都過得了 |
| **得手什麼** | 別人專案的任務內容，並可修改或刪除 |
| **修正的做法** | ① 新增 `_assert_job_belongs_to_project`：反查任務真正屬於哪個專案，與網址不符或查無一律回 404（不回 403，避免洩漏編號存不存在）；② 讀取補成員檢查，刪除的專案編號改必填；③ 任務歸屬原本讀指派表，但建任務時沒有指派列，99.7% 的任務會被誤擋，CM-2113 改成全系統唯一的「讀輪次鏈」判斷，三個入口共用；④ 「強制開始」順手接同一判斷 |
| **狀態** | ✅ 已修（CM-2039，BE commit `8e33568d2`＋套件 `39771c11`；CM-2113 BE `f52a53f1e`） |

## M07-2 🟡 舊線查結果、查報表不檢查專案成員，查不到還退回直接讀雲端硬碟 {#m07-2}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #66；OWASP：A01 存取控制失效） |
| **在哪裡** | 證據自動分類模組的**舊線**：分類結果、驗證報表、裁定報表、摘要 |
| **攻擊面位置** | 任何登入帳號，手上有一個雲端硬碟資料夾編號 |
| **信任邊界位置** | **使用者 ↔ 授權的 Google 帳號**。查結果前應該確認是專案成員；更麻煩的是查不到紀錄時會退回直接去雲端硬碟讀，而且用的是**呼叫者自己公司的鑰匙**——程式裡寫的安全理由在「完整硬碟權限」下不成立 |
| **元件端點位置**（拆除前） | 套件 `jedi_evidence_classification/api/routing.py`：`GET /classification-run/{run_folder_id}/state`、`/report/validation`、`/report/adjudication`、`GET /project/{project_uid}/classify-evidence/summary` → `evidence_classification_service` |
| **駭客怎麼打** | ① 任一登入帳號；② 用第 3 條拿到的資料夾編號查分類結果與報表；③ 系統不查成員；④ 讀到別專案的分類判定，並藉由回傳內容換到檔案編號，接著打第 1 條拿檔案本身 |
| **得手什麼** | 別專案的證據分類結果與通往檔案的編號 |
| **修正的做法** | 拆除了什麼：與 [M07-1](#m07-1) 同批，舊 Drive 分類線九支網址連同服務方法整條拆除，契約測試確保不會被掛回來 |
| **狀態** | 🗑️ 隨舊線退場，1.21.0 出貨（8-L，CM-2222，套件 commit `87fd5439`／BE `ccc5795f6`／前端 `2537ea5`） |

## M07-3 🟡 舊線工作清單與單筆狀態不檢查專案成員，進度資料不分客戶 {#m07-3}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #66；OWASP：A01 存取控制失效） |
| **在哪裡** | 證據自動分類模組的**舊線**：分類工作清單與單筆狀態 |
| **攻擊面位置** | 任何登入帳號 |
| **信任邊界位置** | **使用者 ↔ 舊線分類服務**，以及**客戶 ↔ 客戶**：進度資料放在程式記憶體裡，在資料庫隔離管不到的地方。這是第 1、2 條的入場券——從這裡拿到資料夾編號，第 2 條換到檔案編號，第 1 條換到檔案本身 |
| **元件端點位置**（拆除前） | 套件 `jedi_evidence_classification/api/routing.py`：`GET /project/{project_uid}/classify-evidence/jobs`、`GET /project/{project_uid}/classify-evidence/jobs/{job_uid}` → `evidence_classification_service` 的記憶體進度表 |
| **駭客怎麼打** | ① 任一登入帳號；② 查舊線工作清單；③ 拿到別專案（甚至別家客戶）分類工作的資料夾編號；④ 一路串到第 2 條、第 1 條拿檔案 |
| **得手什麼** | 別專案的分類工作與資料夾編號 |
| **修正的做法** | 拆除了什麼：與 [M07-1](#m07-1) 同批整條拆除，記憶體進度表與背景工作一併刪除 |
| **狀態** | 🗑️ 隨舊線退場，1.21.0 出貨（8-L，CM-2222，套件 commit `87fd5439`／BE `ccc5795f6`／前端 `2537ea5`） |

## M07-4 🟡 舊版證據分類的雲端硬碟搜尋條件用字串拼接 {#m07-4}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #67；OWASP：A05 注入攻擊） |
| **在哪裡** | 證據自動分類模組的**舊線**：以雲端硬碟資料夾編號為單位觸發分類、列出證據檔 |
| **攻擊面位置** | 舊線觸發分類的 API，需要專案管理員身分；請求內容可以帶一個「證據資料夾編號」覆寫值 |
| **信任邊界位置** | **使用者給的值 ↔ Google 雲端硬碟的搜尋語法**。資料夾編號與名稱被直接拼進搜尋條件（`'<編號>' in parents`），應該先跳脫或驗格式；當時沒有處理，名稱那處也只跳脫單引號 |
| **元件端點位置**（拆除前） | `jedi_evidence_classification/api/routing.py`：`POST /project/{project_uid}/ap/{ap_uid}/classify-evidence`（body 可帶 `evidence_folder_id`）→ `evidence_classification_service.trigger_classify()` → `infra/evidence_drive_ops.py` 的 `list_children()`／`find_child_by_name()` |
| **駭客怎麼打** | ① 專案管理員觸發舊線分類；② 在資料夾編號欄填一段帶引號的特製字串；③ 搜尋條件被改寫，例如從「這個資料夾底下」改成「這個硬碟裡的所有檔案」；④ 系統把撈到的檔案當證據拿去分類。**這條沒有實際打過**，Google 那端會不會照吃不確定 |
| **得手什麼** | 授權那個 Google 帳號摸得到的檔案清單 |
| **修正的做法** | 拆除了什麼：舊 Drive 分類線九支網址（觸發、工作清單、狀態讀寫、封存、檔案預覽、驗證報表、裁定報表、摘要）與其服務方法整條拆掉，服務檔從 1,247 行剩 70 行，只留正解匯入；契約測試加「這九支必須不在」的清單，誰掛回來就紅；主專案刪對應測試、前端拿掉舊線入口。拆除前查證 POC／STG 舊線資料 0 筆 |
| **狀態** | 🗑️ 隨舊線退場，1.21.0 出貨（8-L，CM-2222，套件 commit `87fd5439`／主專案 `ccc5795f6`／前端 `2537ea5`）。`evidence_drive_ops.py` 類別檔仍在（宿主 DI 仍會建它），但已沒有任何網址呼叫到拼字串的那兩支方法 |

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

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #70；OWASP：A04 加密機制失效、A02 安全設定錯誤） |
| **在哪裡** | 弱點檢測整合模組：按下「開始掃描」、「立即重新指派」或「重跑」時，系統替每台目標主機開一張派工單 |
| **攻擊面位置** | 資料庫本身——需要拿到**資料庫備份或任一個唯讀帳號**。這一面不是對外開放的，但備份、維運帳號、診斷匯出都可能流出去 |
| **信任邊界位置** | **程式記憶體內 ↔ 資料庫落地**。客戶主機帳密在資料庫裡本來是加密存的，只在派工那一刻解開。解開後的明文應該只留在記憶體裡用完就丟；當時被順手塞進派工單的參數欄位寫回資料庫，跨過了「明文不落地」這條線 |
| **元件端點位置** | `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；OWASP：A07 身分驗證失效、A02 安全設定錯誤） |
| **在哪裡** | 通知模組檢視時查到：三支資料調整／測試腳本寫死系統管理員帳號密碼，其中兩支寫成「環境變數沒設就用這個」 |
| **攻擊面位置** | **拿得到程式庫的人**；另一面是**執行腳本的人自己**——變數忘了設，腳本會靜默用真密碼跑下去，操作者不會被告知 |
| **信任邊界位置** | **版控 ↔ 祕密存放處**。「沒設就用預設值」本來是方便，但預設值填的是真密碼，等於把祕密當成程式的一部分出貨 |
| **元件端點位置** | `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；OWASP：A07 身分驗證失效、A02 安全設定錯誤） |
| **在哪裡** | 公告模組檢視時查到：一支資料庫調整腳本寫「環境變數沒設就用這個密碼」，那個密碼是真的管理員密碼 |
| **攻擊面位置** | **拿得到程式庫的人**；以及忘了設變數就執行的維運人員 |
| **信任邊界位置** | **版控 ↔ 祕密存放處**。同 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；OWASP：A07 身分驗證失效、A04 加密機制失效、A02 安全設定錯誤） |
| **在哪裡** | 意見回饋模組檢視時順手做的密碼掃描：開發過程留下的對話紀錄裡，夾著 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** |

## M23-6 🟡 打包腳本寫死套件倉庫帳密並進了版本控制 {#m23-6}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #75；OWASP：A03 軟體供應鏈失效、A07 身分驗證失效） |
| **在哪裡** | 意見回饋模組檢視時往外查到的：打包前端、做出客戶要安裝的前端映像檔的那支腳本，以及前端程式碼倉庫的三支 CI 流程設定檔——四個檔都寫著同一組內部套件倉庫帳密，跨兩個程式碼倉庫 |
| **攻擊面位置** | 主專案與前端兩個程式碼倉庫，任何讀得到程式碼的人都構成攻擊面，不需要系統帳號。帳密不是明文，是一行指令就能還原的編碼——**這不是加密，只是換個樣子寫**。解出來是管理員層級的帳號，不是限定唯讀的專用帳號 |
| **信任邊界位置** | **「讀得到程式碼的人」↔「打包流程與套件倉庫」**。帳密應該只在打包機本機（不進版控的認證檔）或 CI 平台的遮罩變數裡，建置時臨時掛進去、用完就消失；當時直接寫成腳本的預設值與 CI 設定檔的變數值，還以建置參數傳進去——建置參數會留在建置紀錄、快取與映像檔歷史裡，連映像檔本身都帶著它 |
| **元件端點位置** | 主專案 `scripts/build/build_fe_image.sh`（`NEXUS_AUTH` 預設值，自 `8c503ae5a` 起入版控，以 `--build-arg NEXUS_AUTH` 傳入）；前端 repo `Dockerfile`（`ARG NEXUS_AUTH` ＋ `npm config set`）；前端 repo `.gitlab-ci.yml`、`.gitlab-ci-on-premises.yml`、`.gitlab-ci-terraform.yml` 的 `variables` 段。被拿來抓套件的倉庫：`…/repository/npm-proxy/` |
| **駭客怎麼打** | ① 任何能讀這兩個程式碼倉庫的人，打開打包腳本或 CI 設定檔；② 把那串編碼一行還原，得到套件倉庫的管理員帳密；③ 在內網登入套件倉庫，往前端會抓的套件來源放一個夾帶惡意程式的版本；④ 下次打包前端映像檔時，`npm install` 從同一個倉庫把它抓進來，跟著前端映像檔送到每個客戶的瀏覽器前；⑤ 另一條路：拿到任何一顆已出貨的前端映像檔，翻它的建置歷史一樣看得到這組帳密 |
| **得手什麼** | 往客戶安裝的前端程式裡塞東西的位置，以及公司套件倉庫的管理員權限 |
| **修正的做法** | ① **前端打包改用建置當下臨時掛入的祕密檔**（CM-2230）：`build_fe_image.sh` 拿掉寫死的預設值，改成 `--secret id=npmrc,src=${FE_NPMRC:-~/.npmrc}`，找不到認證檔就警告並改走公開套件來源，另加 `--public-npm` 選項；② 前端 `Dockerfile` 拿掉 `ARG NEXUS_AUTH`，改 `RUN --mount=type=secret,id=npmrc,target=/root/.npmrc,required=false`，倉庫網址改用 `npm install --registry` 傳（不能再 `npm config set`，因為那個位置此時是唯讀掛入的祕密檔）；③ **CI 設定檔刪值**（CM-2252）：三支 CI 檔刪掉明文，改讀 GitLab 專案的遮罩變數，建置時先寫成暫存檔再用同樣的 `--secret` 掛入；④ 驗證：有／無祕密檔各完整建置一次都成功，兩顆映像檔的建置歷史與建置紀錄搜尋認證字樣皆 0 筆；三支 CI 檔搜尋認證字樣 0 命中 |
| **狀態** | ✅ 已修，1.21.0 出貨（CM-2230：主專案 commit `237ffbf86`、前端 commit `7e8f545`；CM-2252：前端 commit `48cc2bd`）。帳密已輪換，歷史 commit 仍含舊值、不改寫歷史 |

## M03-6 🟡 「測試連線」要連到哪台主機由呼叫者自己指定 {#m03-6}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #79；OWASP：A01 存取控制失效） |
| **在哪裡** | 弱點檢測整合模組：「測試連線」按鈕（與 [M03-2](#m03-2) 同一個入口上的另一個問題：那條是「誰可以按」，這條是「按下去打到哪」） |
| **攻擊面位置** | 測試連線 API；目標主機清單由請求帶入。修好 M03-2 之後需要工具設定的修改權限 |
| **信任邊界位置** | **雲端後端 ↔ 客戶內網（經代理程式）**。客戶內網裡的代理程式會照請求去連指定主機；連得上／被拒／逾時的差異原樣回到畫面，等於一台內網探測器 |
| **元件端點位置** | `POST /api/1.0/detection-tools/configs/{uid}/test-connection` → 套件 `jedi_detection/app/service/detection_tool_service.py` 的測試連線流程；上限設定在主專案 `config/config.py` 的 `DETECTION_TEST_CONNECTION_MAX_HOSTS` |
| **駭客怎麼打** | ① 有權限按測試連線的人；② 在目標清單放一整段內網位址；③ 代理程式逐台去連；④ 從每台「連得上／被拒／逾時」的差異畫出客戶內網有哪些機器開著哪些服務 |
| **得手什麼** | 客戶內網的主機存活與服務分布圖 |
| **修正的做法** | **裁定不設目標白名單**（掃描目標是客戶自己的主機、帳號也是客戶給的，哪台算合法目標由客戶管），改做三件：① 單次可帶主機數上限（預設 32），擋「一個請求帶一萬台」；② 回應收斂成單純成功／失敗，逐台診斷改寫進日誌；③ 稽核日誌記下誰按的、哪家客戶、哪份設定、哪個工具、哪台代理程式、目標清單與逐台結果 |
| **狀態** | ✅ 已修（CM-2058，套件 commit `f8e3281a`、BE `ebc57b96f`） |

## M04-3 🟡 密碼裡有雙引號就只遮一半 {#m04-3}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #82；OWASP：A09 日誌與告警失效） |
| **在哪裡** | 共用地基：全站唯一那道密碼遮蔽，在要寫進紀錄的文字裡找出密碼那一段換成星號 |
| **攻擊面位置** | 不是對外攻擊面，而是「讀得到操作日誌的人」。**密碼越強越容易中招**——強密碼常帶雙引號，受害最深的是系統整合用的密碼與金鑰密語 |
| **信任邊界位置** | **應用程式 ↔ 紀錄（操作日誌資料表）**。遮蔽在寫出那一端，方向是對的；錯在比對式子假設密碼裡不會出現雙引號，遇到被跳脫的 `\"` 就以為密碼結束了 |
| **元件端點位置** | `jedi_common/utils/common_utils.py:mark_password`；呼叫點在 `common/middleware/app_mw.py`（請求／回覆寫進操作日誌）與套件 `jedi-common` 的 `handler.py`、`jedi-log` 的 `masking.py`；最常觸發的入口是 `POST /api/1.0/login` 與各種「存整合設定」端點 |
| **駭客怎麼打** | ① 管理員設定 LDAP 或 SSH 整合，密碼是 `ab"cd1234` 這種；② 系統寫操作日誌時，JSON 裡那段是 `ab\"cd1234`，比對式子在跳脫引號處就收尾；③ 只有 `ab` 被遮，`cd1234` 原文留在資料庫九十天；④ 能查操作日誌的人拼回大半截密碼 |
| **得手什麼** | 系統整合密碼的後半截原文 |
| **修正的做法** | ① 比對值的正則從「不含雙引號的一串」改成「不含雙引號、但允許反斜線跳脫任何字元的一串」，欄位名同步改；② **刻意不改成「先把整筆解析成 JSON 再遞迴遮」**：紀錄常有半寫壞的內容，解析失敗時整筆會原文吐出去，正好在最需要遮蔽時失效；③ 兩處呼叫點實跑確認巢狀結構的通行證也遮得到；④ 補 unit test，正則改回舊版 4 條轉紅 |
| **狀態** | ✅ 已修（CM-2065，套件 jedi-common commit `a9562dd0`，1.21.0 出貨） |

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

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #84；OWASP：A02 安全設定錯誤、A10 例外狀況處理不當） |
| **在哪裡** | 共用基礎模組：服務啟動時讀「現在是什麼環境」（`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 出貨） |

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

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #92；OWASP：A04 加密機制失效、A02 安全設定錯誤） |
| **在哪裡** | 系統日誌模組：管理員在「日誌轉送」設定頁選傳送方式，把系統日誌送到客戶自己的監控系統 |
| **攻擊面位置** | 我們後端到客戶監控系統之間的**網路路徑**。攻擊者需要能看封包（同機房網段、被入侵的網路設備），不需要我們系統的帳號 |
| **信任邊界位置** | **我們後端 ↔ 客戶的監控系統**。日誌裡有帳號、操作、稽核事件，跨出後端這條線時應該能加密。缺口不在「沒加密」（明文轉送是業界常態），而在**沒給選擇**——兩種格式、兩種傳送方式，四種組合全是明文 |
| **元件端點位置** | `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`）。驗證：自簽憑證起真的加密收件端實跑四種情形 |

## M15-4 🟡 AI 儀表板把查到的資料整包送回，夾帶密碼鹽值與超級管理員旗標 {#m15-4}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #95；OWASP：A06 不安全的設計、A01 存取控制失效） |
| **在哪裡** | AI 儀表板模組：查詢結果送回畫面，以及送給外部 AI 的樣本資料 |
| **攻擊面位置** | 生成儀表板的 API，任何能選中帳號類查詢的登入帳號；回應內容在瀏覽器開發工具裡就看得到 |
| **信任邊界位置** | **後端 ↔ 瀏覽器／外部 AI**。畫面只顯示三欄，但後端把整筆帳號資料（含密碼加密用的鹽值、「是不是超級管理員」）都序列化送出；送給外部 AI 的樣本同樣沒遮。根源在共用的序列化工具（[M04-12](#m04-12)） |
| **元件端點位置** | `POST /api/1.0/ai-dashboard/auto-generate` → 套件 `jedi_common/utils/serialization_util.py` 的 `to_serializable()`；帳號查詢在套件 `jedi_iam/app/service/user_service.py` |
| **駭客怎麼打** | ① 任一員工在 AI 儀表板查帳號清單；② 打開瀏覽器開發工具看回應；③ 每個帳號的密碼鹽值與超管旗標都在裡面；④ 據此鎖定最高權限的帳號當下一個目標 |
| **得手什麼** | 每個帳號的密碼加密材料與超管身分 |
| **修正的做法** | ① 共用序列化工具新增「不送名單」：鹽值、密碼、超管旗標、各類通行證欄位一律不輸出；物件與字典兩個分支都擋（有 `to_dict()` 的物件會先轉字典，只擋一邊等於留繞過路徑）；② 長期方向是讓資料本身帶標記，不在本卡範圍；③ 日誌那一側（CM-2269）：身分套件查詢類服務的日誌原本整包印出帳號資料（含密碼雜湊、鹽值、超管旗標），實查十處全改成只記筆數 |
| **狀態** | ✅ 已修（CM-2064，套件 commit `248132ec`；日誌側 CM-2269，套件 `f679363d`） |

## M16-2 🟡 寄信失敗把整組郵件設定含密碼寫進紀錄 {#m16-2}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #96；OWASP：A09 日誌與告警失效） |
| **在哪裡** | 通知模組：系統寄信（通知信、驗證碼、測試信）失敗時的錯誤紀錄 |
| **攻擊面位置** | 不是對外攻擊面，而是「看得到系統紀錄的人」——維運人員、能查資料庫的人、外部紀錄收集服務的管理員，遠多於被授權改郵件設定的人。**寄信失敗一次就發生**（密碼填錯、網路抖動都算） |
| **信任邊界位置** | **應用程式 ↔ 紀錄檔**。失敗訊息應該只記錯誤本身；當時把整個設定物件格式化進訊息，物件的字串形式含密碼，而共用遮蔽只認「欄位名與值都用雙引號包起來」的格式，蓋不到這種寫法 |
| **元件端點位置** | 套件 `jedi_notification/infra/smtp_mail/smtp_mail_adapter.py` 的寄信方法兩個 `except` 分支；觸發入口如 `POST /api/1.0/mail/test`（套件 `jedi_notification/api/routing.py`）與所有系統通知寄信；同一件事在回廠診斷包那端的出口是 `infra/support/diag_masking.py` |
| **駭客怎麼打** | ① 不用打——郵件伺服器暫時連不上，或管理員打錯一次密碼；② 系統把「寄信失敗」連同整組設定（伺服器、帳號、**密碼**）寫進紀錄；③ 任何看得到紀錄的人、或拿到回廠診斷包的人，搜尋就得到郵件帳密；④ 拿這組帳密可以冒用公司郵件帳號寄信 |
| **得手什麼** | 公司郵件伺服器的帳號密碼 |
| **修正的做法** | ① 兩個失敗分支改成只記伺服器位址、連接埠與錯誤類別，錯誤堆疊保留（郵件伺服器的錯誤內容是對方回應，不含我們送出的密碼）；② 全套件搜過所有把設定物件或密碼格式化進紀錄的地方，只有這兩處；③ 補 3 條測試確認三種失敗紀錄都不含假密碼；④ 診斷包那端暴露的同種寫法，併進診斷包遮罩一次補齊（CM-2212，commit `c8534bfcb`／`8f5755f1d`） |
| **狀態** | ✅ 已修（CM-2111，套件 jedi-notification commit `e12287e2`；診斷包端 CM-2212，1.21.0 出貨） |

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

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #97；OWASP：A04 加密機制失效、A07 身分驗證失效） |
| **在哪裡** | 寄信與通知模組：系統寄出的每一封信（一次性驗證碼、新帳號初始密碼、各種通知），以及設定頁的「寄測試信」 |
| **攻擊面位置** | 我們後端到郵件伺服器之間的**網路路徑**。郵件服務在外部雲端時比較容易站上去；不需要我們系統的帳號 |
| **信任邊界位置** | **我們後端 ↔ 郵件伺服器**。設定頁選了加密，客戶合理相信連線安全，後端應該在送出帳密前確認對方憑證。當時沒有任何程式把驗證關掉——**是程式語言內建的加密升級呼叫預設本來就不驗對方身分，我們也沒主動打開**。後果一樣，但稽核時答案不同 |
| **元件端點位置** | `POST /api/1.0/mail/test`（`jedi-notification` 的 `SendMailTestRoute`）及所有寄信流程 → 套件 `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`；前端 `src/views/smtp-config/SmtpConfigForm.vue` |
| **駭客怎麼打** | ① 攻擊者站在我們與郵件伺服器之間；② 假扮成郵件伺服器，拿一張隨便自簽的證明；③ 系統照單全收、不報任何錯，下一行就把郵件帳號密碼送過來；④ 接著每一封經過的信都落到他手上——包含登入用的一次性驗證碼與新帳號的初始密碼信，等於帳號被接管的材料整批送出 |
| **得手什麼** | 郵件帳密，以及每一封信的內容（含一次性驗證碼、初始密碼） |
| **修正的做法** | ① 寄信設定新增兩個欄位：「驗證伺服器憑證」（`tls_verify`）與「信任憑證」（`tls_ca_cert`）；② 勾了驗證就用 `build_ssl_context()` 建一個要求憑證且比對主機名稱的加密環境再升級，貼了 CA 就只信那一份；③ **升級前的舊設定沒有這個值，一律當作不驗**——否則升級當天所有接自簽伺服器的客戶會突然寄不出信；④ 驗證失敗回「寄送失敗」並記一行看得懂的中文訊息，不再把交握失敗吞掉；⑤ 同一張卡把「寄信失敗時把整份設定含密碼寫進紀錄」一併修掉 |
| **狀態** | ✅ 已修（CM-2111，套件 commit `e12287e2`，主專案 `e3c726896`，前端 `b391966`）。⚠️ 之後前端 CM-2239（`16141ec`）依決策者裁定把**新建設定改成預設不勾**、拿掉未勾時的風險提示——報告原寫的「新建預設驗證、設定頁頂部提示建議開啟」已不是現況；現在要客戶自己勾才會驗 |

## M23-4 🟡 連 GitHub 時把憑證驗證關掉 {#m23-4}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #100；OWASP：A04 加密機制失效、A07 身分驗證失效） |
| **在哪裡** | 意見回饋模組：使用者送出意見後，系統帶著公司的 GitHub 通行證去 GitHub 開問題單、上傳附件 |
| **攻擊面位置** | 我們後端到 GitHub 之間的**網路路徑**。攻擊者要站在路上；不需要我們系統的帳號 |
| **信任邊界位置** | **我們後端 ↔ GitHub**。帶著公司通行證出門前，應該確認對面真的是 GitHub。當時兩處建立連線的程式**明寫了關閉驗證**（這是「寫死不驗」，與寄信那條「沒主動打開」不同）。這個模組的通行證先前已外流過一次 |
| **元件端點位置** | `POST /api/1.0/feedback`、`PUT /api/1.0/feedback/{uid}`（`jedi_issue/api/routes/feedback_route.py`）同步開單時 → `jedi_issue/infra/github.py` 的 `get_github_client()`、`jedi_issue/infra/issue/adapter/github/github_issue_adapter.py` 建構子 |
| **駭客怎麼打** | ① 攻擊者站在我們後端與 GitHub 之間（例如劫持網址解析）；② 使用者照常送出一筆意見回饋；③ 系統去連「GitHub」，攻擊者回一張自簽證明，系統不驗；④ 公司的 GitHub 通行證連同問題單內容一起送到攻擊者手上 |
| **得手什麼** | 公司的 GitHub 通行證（可讀寫對應的程式庫與問題單） |
| **修正的做法** | ① 拿掉兩處 `Github(..., verify=False)` 的關閉開關，恢復套件預設的憑證驗證；② 同層的 GitLab 那一半查過本來就有驗，不需改；③ 同一張卡順手修了附件刪除誤用未過濾清單的問題（#135） |
| **狀態** | ✅ 已修（CM-2066，套件 commit `4f54c9b4`） |

## M03-7 🟡 資料庫的客戶隔離規則方向寫反 {#m03-7}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #104；OWASP：A01 存取控制失效） |
| **在哪裡** | 弱點檢測整合模組（延伸到全基線）：資料庫裡決定「哪個客戶看得到哪幾筆」的規則，同樣錯法在出貨基線共 11 條 |
| **攻擊面位置** | 所有讀寫這些表的功能，任何子單位（子公司）帳號 |
| **信任邊界位置** | **母單位 ↔ 子單位（租戶之間）**。規則把子單位的路徑 `/1/102/152/` 拆成 `[1, 102, 152]` 逐一比，於是子單位看得到（還能改能刪）母單位與平台的資料，母單位反而看不到底下子單位的。合規文件匯入那兩張是第三種更寬的寫法：用純文字包含比對，還拿單位編號去跟使用者編號比 |
| **元件端點位置** | 弱點檢測五張（`detection_execution_groups`、`detection_executions`、`jedta`、`jedt`、`tdtc`）、遠端代理程式 `agent_tasks`、授權三張；合規文件匯入兩張 `oscal.ap_docx_parse_jobs`、`oscal.ar_xlsx_parse_jobs`。受影響功能如 `GET /api/1.0/detection-tools/configs`、`POST …/test-connection`、`GET /api/1.0/license/status` |
| **駭客怎麼打** | ① 子公司的成員登入；② 打開檢測工具設定，看到母公司登記的工具；③ 疊上 [M03-2](#m03-2)，對母公司那份按測試連線，把母公司的維運帳密送到自己的主機；④ 也能改、刪母公司的掃描紀錄 |
| **得手什麼** | 母公司的檢測設定、維運帳密與掃描紀錄 |
| **修正的做法** | ① **九張表統一改前綴比對**（只看自己與底下）：已裝機另開主線 migration 重建（套件檔以 DO 區塊包，既有庫會跳過，只改套件無效），套件檔同步改給新裝機；② 新增守衛測試，讀資料庫規則斷言全庫不再有拆陣列寫法；③ 合規文件匯入那兩張改成標準四條規則（CM-2195）；④ 授權那三張的「讀」不能照改——改了子單位讀不到上層授權、全面 403——後續另加只管讀的「祖先可讀」規則，改刪仍只吃前綴（CM-2369） |
| **狀態** | ✅ 已修，1.21.0 出貨（CM-2271，BE `6c74ff0e3`／套件 `6e0d0d4b`；匯入兩張 CM-2195 BE `56dd4cdc8`；授權讀取 CM-2369）。⚠️ 總表與模組頁說匯入兩張是 09-14 由 CM-1770 重建，但 CM-1770 的 commit（`b5ca22bd1`）沒動這兩張；實際改成標準規則的是 09-25 的 CM-2195 |

## M18-8 🟡 子單位帳號刪得掉母公司的停權紀錄與授權異動紀錄 {#m18-8}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #104；OWASP：A01 存取控制失效） |
| **在哪裡** | 授權管理模組：授權檔、授權異動紀錄、停權紀錄這三張資料表的資料庫門禁規則（決定哪個客戶碰得到哪幾筆） |
| **攻擊面位置** | 任何以**子單位帳號**身分寫到這三張表的路徑。資料庫門禁是最後一道防線——程式層守門只要漏一支，這一道就是唯一的擋板。需要被停權客戶手上有任一子單位帳號 |
| **信任邊界位置** | **子單位 ↔ 母公司（租戶 ↔ 租戶）**。子單位「讀」母公司的授權是設計需要（授權只發給最頂層、底下共用）；但當時同一條規則把「讀得到」直接等同「改得到、刪得到」，資料庫這一側沒有另外守寫入 |
| **元件端點位置** | 資料表 `config.tenant_licenses`、`config.tenant_license_events`、`config.tenant_license_suspensions` 的門禁規則（RLS policy）；套件 `jedi-license-runtime/migrations/002-license-rls.sql`、`003-tenant-license-suspensions.sql`；已裝機修正 `scripts/sql/2026-09-28-fr114-rls-tenant-prefix-unify.sql`、`scripts/sql/2026-10-01-cm2369-license-tables-ancestor-read.sql`；讀這三張的功能如 `GET /license/status`（`jedi_license_runtime/api/routing.py`） |
| **駭客怎麼打** | ① 被停權的客戶用手上任一個子單位帳號登入；② 經任何一條以子單位身分寫到停權紀錄表的路徑送出刪除；③ 資料庫規則把母公司那列算成「這個子單位碰得到的」，同一條規則連刪除一起放行；④ 停權紀錄表的設計是「有一列就代表停權中」，刪掉那列整棵樹當場復權；⑤ 再順手改掉授權到期日與模組清單、刪掉追查用的異動紀錄，把痕跡一併抹掉（模組頁未點名具體寫入入口，此處依資料庫規則推論） |
| **得手什麼** | 被停權客戶自行復權並改寫授權內容，且刪得掉事後追查用的異動時間線 |
| **修正的做法** | ① **改刪收緊**（CM-2271，BE `6c74ff0e3`＋套件 `6e0d0d4b`）：這三張連同弱點檢測、遠端代理共九張表，門禁規則由「把路徑拆成編號陣列逐一比」改成前綴比對，只碰得到自己與底下；已裝機另開主線 migration 重建（套件檔以 DO 區塊包，既有庫會跳過）；新增守衛測試，斷言全庫不再有拆陣列寫法；② **讀取開回給子單位**（CM-2369，BE `3574033a6`＋套件 `ba14e128`）：第①步讓子單位讀不到上層的照、被判成沒買全面 403；三張表各加一條只管「讀」的規則（`tenant_licenses_ancestor_read` 等三條，重用既有的祖先判斷函式），改刪仍只吃前綴規則；③ 真庫測試 12 條：子讀上層＝1、子改刪上層＝0、兄弟互看＝0 |
| **狀態** | ✅ 已修（改刪：CM-2271，1.21.0；讀取：CM-2369，1.21.1）。驗證：子單位改不到也刪不掉母公司的授權與停權紀錄，且讀得到上層授權、功能正常 |

## M20-5 🟡 一整類業務資料表連客戶歸屬欄位都沒有，牆沒有東西可依據 {#m20-5}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #105；OWASP：A01 存取控制失效） |
| **在哪裡** | 客戶資料隔離（跨模組專項）：合規文件那一整塊（系統安全計畫、稽核發現等）的資料表 |
| **攻擊面位置** | **只在多家客戶共用的 SaaS 版成立**。落地版一家客戶一套資料庫，「看到別家」不成立 |
| **信任邊界位置** | **客戶 ↔ 客戶**。資料庫隔離要靠「這筆是哪家客戶的」欄位判斷，這批表連欄位都沒有，牆沒有依據；應用層是唯一防線，漏一處就跨客戶 |
| **元件端點位置** | 盤點結果：102 張沒有牆、其中 101 張連欄位都沒有；53 張要補、49 張確認不用補，要設計的接點 4 個（模組頁附清單）。主要在 `oscal.*` 結構（系統安全計畫、評估計畫、評估結果、改善計畫及其子表），經 `api/oscal/__init__.py` 與 `api/project/__init__.py` 下的各支 API 讀寫 |
| **駭客怎麼打** | ① SaaS 版上，任一家客戶的帳號；② 找到一支應用層剛好沒檢查專案歸屬的合規文件 API；③ 資料庫不擋；④ 讀到別家客戶的系統安全計畫與稽核發現（實測：指向不存在客戶的鑰匙看到系統安全計畫 639 筆、稽核發現 525 筆，專案 0 筆） |
| **得手什麼** | （SaaS 版）別家客戶的系統安全計畫與稽核發現 |
| **修正的做法** | **尚未施工**，已定案的方向：補「父表看得到、子表才看得到」的跟隨規則，**不加欄位、不回填資料**（範本是 FR-094 專案底下 13 張表的做法：子表規則用 EXISTS 繞父表，PostgreSQL 的規則子查詢一樣受父表隔離約束）。依決策者裁定排在程式面守門修完並測過一版之後 |
| **狀態** | 🕓 SaaS 前必做（清單已盤好：53 張、4 個接點） |

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

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #106；OWASP：A09 日誌與告警失效、A02 安全設定錯誤） |
| **在哪裡** | 日誌模組：操作日誌與系統日誌兩張表按月分開存放，靠一支維護程式每月建新月份、清掉過期月份（操作日誌留 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`）。驗證：開發環境實查本月與未來兩個月分區已建、備用表清空歸零 |

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

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #107；OWASP：A01 存取控制失效、A07 身分驗證失效） |
| **在哪裡** | 通知模組：郵件設定頁的「寄測試信」按鈕 |
| **攻擊面位置** | **持有「改寄信設定」權限的人**——這個權限刻意下放到各個下層客戶的管理員（共 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`） |

## M11-12 🟡 可以把別人的程序書掛到自己的控制項，再從回應讀到檔案編號去下載 {#m11-12}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #153；OWASP：A01 存取控制失效） |
| **在哪裡** | 合規文件核心：系統安全計畫裡「把程序書掛到控制項／查核項目」 |
| **攻擊面位置** | 管理自己一個專案的人（自己開一個專案就有），手上有別人專案的文件編號 |
| **信任邊界位置** | **專案 ↔ 專案**。把文件編號換成內部編號時應該只認「這份計畫自己的文件庫」；當時只用編號在全系統查，跨專案都換得到。跨公司被檔案表隔離擋住，同公司跨專案沒擋 |
| **元件端點位置** | 主專案 `api/oscal/__init__.py`：`POST /ssp/{ssp_uid}/control-implementation/{control_identifier}/document-mappings`、`…/objective/{statement_identifier}/document-mappings` → `app/oscal/service/ssp_document_pool_service.py`；套件 `jedi_compliance_audit/infra/repository/ssp_document_pool_query.py` 的 `resolve_doc_uids_to_ids()` |
| **駭客怎麼打** | ① 在自己管理的專案；② 把別人專案的程序書編號掛到自己的控制項；③ 系統照單掛上，回應附上檔案編號；④ 拿檔案編號去下載，A 專案的機密程序書被 B 專案的人拿走 |
| **得手什麼** | 同公司其他專案的程序書 |
| **修正的做法** | ① 套件 `resolve_doc_uids_to_ids(ssp_id, doc_uids)` 的計畫編號改**必填**，查詢加上「屬於這份計畫的文件庫」條件，池外編號不會回傳；② 主專案六處呼叫全部傳入計畫編號；③ 掛載時送上來的編號只要有一個不在池內就整批回 404，不靜默丟掉（否則前端會以為掛成功），檢查放在建立任何資料之前 |
| **狀態** | ✅ 已修（FR-114 CM-2173，套件 commit `5170e507`／BE `c4b6bdb16`，1.21.0 出貨）。驗證：掛別的專案文件 404、掛自己的 200 |

## M11-22 🟡 框架 PDF 解析失敗時把原始錯誤訊息整段回給前端 {#m11-22}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #155；OWASP：A10 例外狀況處理不當） |
| **在哪裡** | 合規文件核心：平台管理員上傳法規框架 PDF，系統自動解析出控制項 |
| **攻擊面位置** | 要先是**平台管理員**（框架是全域資源，匯入只限最上層），所以評中。設計上就開放上傳任意檔案給這個角色 |
| **信任邊界位置** | **伺服器內部 ↔ 使用者畫面**。程式出錯的原始訊息只該留在伺服器日誌；當時解析失敗時把「白話訊息：原始錯誤」整串存進解析單，查詢時原樣回給前端。而同一段也包著「寫資料庫」，寫庫失敗時連資料庫語句都會帶出去 |
| **元件端點位置** | 主專案 `api/oscal/__init__.py`：`POST /oscal-framework-parse-jobs/parse`（`FrameworkParseJobParseRoute`）→ `app/oscal/service/framework_parse_job_service.py` 的 `parse()`（`require_platform_admin()`）→ 失敗時 `write_error()`；讀回：`GET /oscal-framework-parse-jobs/{uid}` |
| **駭客怎麼打** | ① 拿到平台管理員帳號的人，打開框架匯入；② 上傳一份故意弄壞的 PDF；③ 解析失敗，系統把程式的原始錯誤（伺服器路徑、用的是哪一版解析套件）整段存起來；④ 打開解析結果就看到這些內部細節，不必自己猜伺服器怎麼裝的；⑤ 若讓寫資料庫那一步也失敗，連資料庫語句都會顯示出來 |
| **得手什麼** | 伺服器路徑、第三方套件與版本、資料庫語句片段——下一步攻擊的地圖 |
| **修正的做法** | ① 解析單與回給前端的只放固定錯誤碼 `GRC_FRAMEWORK_PARSE_FAILED` 與白話訊息，原始錯誤只用 `logger.exception` 進伺服器日誌；② **寫資料庫那段在同一個保護範圍內**，寫庫失敗也只存白話訊息；③ 已經存在、可能帶路徑的舊解析單不另外修資料（解析單 24 小時就過期） |
| **狀態** | ✅ 已修（FR-114 CM-2183，commit `c33f7e828`，1.21.0 出貨）。驗證：DEV 上傳亂碼 PDF，畫面只見「框架 PDF 解析失敗」，日誌留完整原始錯誤 |

## M11-14 🟡 沒被授權看合規範本的員工，直接打網址就讀走全公司範本 {#m11-14}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #157；OWASP：A01 存取控制失效） |
| **在哪裡** | 合規文件核心：合規範本（資源庫）的清單下拉與單一範本完整內容 |
| **攻擊面位置** | 已登入使用者可打的範本 API；側邊選單看不到入口的人照樣打得到 |
| **信任邊界位置** | **使用者 ↔ 範本服務**。同一支檔案的新增、修改、刪除、複製都有權限檢查，只有讀取兩支漏掉——不是設計。原廠公版與母公司分享的範本跨公司也讀得到 |
| **元件端點位置** | 主專案 `api/module_frame/__init__.py`：`GET /module-frame/{uid}`（`ModuleFrameRoute`）、`GET /module-frames/menu`（`ModuleFramesMenuRoute`），路由在 `api/module_frame/routes/module_frame_route.py` |
| **駭客怎麼打** | ① 沒被授權看範本的員工；② 直接打範本下拉網址拿到清單；③ 再逐一打單一範本；④ 讀走全公司範本的完整內容 |
| **得手什麼** | 全公司合規範本內容 |
| **修正的做法** | ① 單一範本掛 `require_capability("module-frame.read")`；② **範本下拉依裁定放寬為「看範本或能建專案」任一即過**——建立專案畫面要選範本，能建專案的人一定要能選；③ 先寫 migration 替「有建專案或改範本權限、卻缺看範本」的既有角色補上權限，否則上守門後那些人永遠被拒、畫面沒有錯誤訊息 |
| **狀態** | ✅ 已修（FR-114 CM-2186，BE commit `aafaa070d`，1.21.0 出貨） |

## M11-15 🟡 範本的稽核流程圖任何登入帳號都讀得到 {#m11-15}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #158；OWASP：A01 存取控制失效） |
| **在哪裡** | 合規文件核心：範本裡的稽核流程圖（一次稽核走哪些步驟、每步誰負責、順序） |
| **攻擊面位置** | 已登入使用者可打的範本 API |
| **信任邊界位置** | **使用者 ↔ 範本服務**。同一支檔案的新增、修改、刪除都要權限，讀取沒有 |
| **元件端點位置** | `GET /api/1.0/module-frame/item/{uid}`（主專案 `api/module_frame/routes/module_frame_item_route.py` 的 `ModuleFrameItemRoute.get`）→ `module_frame_item_service.get_module_frame_item()`（內含流程範本） |
| **駭客怎麼打** | ① 任一登入帳號；② 拿一個範本項目編號打讀取；③ 讀走稽核流程的步驟、負責角色與順序 |
| **得手什麼** | 公司的稽核流程設計 |
| **修正的做法** | ① 讀取補 `require_capability("module-frame.read")`；② 同卡 migration 先替缺這顆權限的既有角色補上（DEV 補了 2 個角色），避免上守門後合法使用者永遠被拒 |
| **狀態** | ✅ 已修（FR-114 CM-2186，BE commit `aafaa070d`，1.21.0 出貨） |

## M11-16 🟡 範本掛了哪些程序書任何人都列得出來，附檔案編號就能下載 {#m11-16}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #161；OWASP：A01 存取控制失效） |
| **在哪裡** | 合規文件核心：範本的參考程序書清單 |
| **攻擊面位置** | 已登入使用者可打的範本 API；清單附檔案編號，下載那支當時只認編號 |
| **信任邊界位置** | **使用者 ↔ 範本服務**，以及**使用者 ↔ 檔案服務**。讀到清單約等於拿到程序書本身 |
| **元件端點位置** | `GET /api/1.0/module-frame/{uid}/reference-documents`（主專案 `api/module_frame/routes/module_frame_reference_document_route.py` 的 `ModuleFrameReferenceDocumentListResource`）；下載 `GET /api/1.0/file/download/{uid}`（套件 jedi-file-upload） |
| **駭客怎麼打** | ① 任一登入帳號；② 列出某範本掛的程序書；③ 拿到每份的檔案編號；④ 打下載網址，整份程序書下來 |
| **得手什麼** | 範本附掛的程序書全文 |
| **修正的做法** | ① 清單補「必須有看範本的權限」（CM-2186）；② 下載一端由檔案歸屬檢查收口：主專案替「系統公版資產」與資源庫範本的佐證登記歸屬答案，**跨公司一律拒絕**（CM-2033）；③ 同公司內可下載範本程序書屬刻意設計——範本是公司共用資源 |
| **狀態** | ✅ 已修，1.21.0 出貨（清單 CM-2186 `aafaa070d`；下載 CM-2033 `a334f5385`） |

## M11-17 🟡 範本的「稽核目標預設內容」任何登入帳號都讀得到 {#m11-17}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #162；OWASP：A01 存取控制失效） |
| **在哪裡** | 合規文件核心：範本裡每條稽核目標的預設完成狀態、實作說明、備註 |
| **攻擊面位置** | 已登入使用者可打的範本 API |
| **信任邊界位置** | **使用者 ↔ 範本服務**。同一支檔案的修改、刪除都要權限；隔壁做同一件事的檔案四個入口全掛齊——是漏掉 |
| **元件端點位置** | 主專案 `api/module_frame/routes/module_frame_control_objective_default_route.py`：`GET /module-frame/{uid}/objective-defaults`（`ModuleFrameObjectiveDefaultListResource`）、`GET /module-frame/{uid}/objective-defaults/{objective_default_uid}`（`ModuleFrameObjectiveDefaultResource`） |
| **駭客怎麼打** | ① 任一登入帳號；② 打清單與單筆；③ 讀走每條稽核目標的預設實作說明與備註 |
| **得手什麼** | 範本的稽核目標預設內容 |
| **修正的做法** | 兩支讀取補 `require_capability("module-frame.read")`，同卡 migration 先補既有角色權限 |
| **狀態** | ✅ 已修（FR-114 CM-2186，BE commit `aafaa070d`，1.21.0 出貨） |

## M11-4 🟡 範本裡的人員、設備、系統元件、繼承授權與系統特性拿編號就讀得到，而且跨公司 {#m11-4}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #163；OWASP：A01 存取控制失效） |
| **在哪裡** | 合規文件核心：範本的人員名單、設備清單、系統元件、繼承授權、系統特性五個分頁 |
| **攻擊面位置** | 任何登入帳號，拿一個範本編號；原廠公版與母公司分享下來的範本，別家公司也讀得到 |
| **信任邊界位置** | **使用者 ↔ 範本服務**。同一支檔案的新增、修改、刪除都有權限檢查，只有讀取漏掉 |
| **元件端點位置** | 主專案 `api/module_frame/__init__.py`：`GET /module-frame/{uid}/parties`、`/inventory`、`/components`、`/leveraged`、`/system-characteristic`（`module_frame_party_route.py`、`module_frame_inventory_route.py`、`module_frame_components_route.py`、`module_frame_leveraged_route.py`、`module_frame_system_characteristic_route.py` 的清單類別） |
| **駭客怎麼打** | ① 任一登入帳號，甚至別家公司的；② 拿到一個範本編號（公版範本的編號誰都看得到）；③ 依序打五個分頁；④ 拿到人員姓名、email、電話、地址，設備資產編號、IP、主機名稱，系統開放的通訊協定與連接埠，向哪些外部供應商繼承了哪些控制——現成的踩點資料 |
| **得手什麼** | 範本內的人員聯絡資料與資產、網路踩點資料 |
| **修正的做法** | 五個讀取入口各補一行 `require_capability("module-frame.read")`，一張卡一起補；同卡 migration 先替缺權限的既有角色補上 |
| **狀態** | ✅ 已修（FR-114 CM-2186，BE commit `aafaa070d`，1.21.0 出貨） |

## M06-17 🟡 規劃頁的任務清單不問你是不是這個專案的人 {#m06-17}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #169；OWASP：A01 存取控制失效） |
| **在哪裡** | 稽核流程模組：規劃頁列出某查核項目底下的任務 |
| **攻擊面位置** | 同一家客戶裡任何有專案模組的帳號；查核項目可以用公開的標準條號一條條試 |
| **信任邊界位置** | **使用者 ↔ 任務服務**。清單不驗成員，專案編號可以不帶；列出的任務編號正好是改、刪、匯入別人任務的材料 |
| **元件端點位置** | `POST /api/1.0/project/{project_uid}/ap/{ap_uid}/control-group/{group_uid}/control/{control_uid}/assessment-object/{ao_uid}/jobs/list`（主專案 `api/flow_control/routes/job_route.py` 的 `ProjectJobListResource`）→ `app/flow_control/service/job_service.py` 的 `list_jobs()` |
| **駭客怎麼打** | ① 同客戶任一帳號；② 帶一個別專案的編號，把查核項目換成標準條號逐條查；③ 列出那個專案每個查核項目底下的任務——指派人、部門、設備、問卷、檢測工具設定（帳密有遮）；④ 拿任務編號去改、刪、匯入 |
| **得手什麼** | 別專案的任務配置，以及修改它們所需的編號 |
| **修正的做法** | ① `list_jobs` 開頭 `_require_participant`：解析專案（沒帶或查無 404）→ 任一成員；② 成員檢查元件沒注入時一律 403（不放行） |
| **狀態** | ✅ 已修，1.21.0 出貨（CM-2191，BE commit `33aab8cba`） |

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

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #172；OWASP：A01 存取控制失效、A07 身分驗證失效） |
| **在哪裡** | 證據自動分類模組：「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 已註明：沒真的跑一次分類容器，以組出的環境變數判定 |

## M07-14 🟡 證據分類逾時，原廠 AI 金鑰明文寫進日誌 {#m07-14}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #173；OWASP：A09 日誌與告警失效） |
| **在哪裡** | 證據自動分類模組：系統啟動一個容器跑 AI 分類，跑超過 30 分鐘會被判逾時 |
| **攻擊面位置** | **不需要任何人攻擊**，大批證據或 AI 服務卡住就會發生。外洩出口是日誌轉送與回廠診斷包，拿到其中之一的人就拿到金鑰 |
| **信任邊界位置** | **後端 ↔ 分類容器，與後端 ↔ 紀錄檔／診斷包**。金鑰應該只傳進容器、不出現在任何會被記錄的地方；當時金鑰寫在啟動容器的指令上，逾時的錯誤物件帶著整條指令，被原樣印進日誌，診斷包的遮罩又認不得「名稱=值」這種寫法 |
| **元件端點位置** | 觸發入口 `POST /api/1.0/evidence-batches/{batch_uid}/classify`（套件 `jedi_evidence_classification/api/routing.py`）；容器啟動在 `jedi_evidence_classification/infra/classifier_container_runner.py:ClassifierContainerRunner.run`；診斷包遮罩在主專案 `infra/support/diag_masking.py:mask_log_lines` |
| **駭客怎麼打** | ① 不用打——使用者送一大批證據去分類，或 AI 服務那頭卡住；② 30 分鐘後逾時，錯誤訊息連同 `docker run -e ANTHROPIC_API_KEY=明文金鑰…` 整條指令寫進後端日誌；③ 這筆日誌經日誌轉送送出、或被打進回廠診斷包；④ 拿到的人直接用原廠 AI 金鑰，費用算在原廠帳上 |
| **得手什麼** | 原廠 AI 服務金鑰原文 |
| **修正的做法** | ① **金鑰不再寫在指令上**：改寫成一個只有擁有者能讀的暫存檔，指令只帶 `--env-file 檔案路徑`，檔案放在不會掛進容器、不會上傳雲端的目錄，跑完或出錯都刪掉（CM-2193，套件 commit `fdf8c3eb`）；② 逾時與找不到程式兩個錯誤改成不串帶原始錯誤（`from None`），日誌不再出現整條指令；③ 主系統診斷包遮罩補認「名稱=值」寫法，擋住升級前已留在客戶機上的舊日誌（CM-2193，BE commit `78ec5a816`）；④ 其餘寫法（帶引號的值、連線字串帳密、`export`、PEM 憑證等）補齊，資料庫紀錄也改走同一套遮罩，打包前再統一過一道（CM-2212） |
| **狀態** | ✅ 已修，1.21.0 出貨（CM-2193，套件 `fdf8c3eb`＋BE `78ec5a816`；CM-2212，BE `c8534bfcb`／`8f5755f1d`）。驗證：真 docker 逾時實跑，日誌不含金鑰也不含指令 |

## M24-3 🟡 使用者上傳的檔案整個資料夾開在 /static/ 下，不用登入就能下載 {#m24-3}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #203；OWASP：A02 安全設定錯誤、A01 存取控制失效） |
| **在哪裡** | 主系統：網站框架預設替 `static/` 資料夾註冊的公開路徑；這個資料夾放的是問卷答案附件、意見回饋附件、合規文件匯出檔 |
| **攻擊面位置** | 直接連到後端服務埠的任何人，**不用登入**。今天出貨的網站前門只放行兩條路徑、外面打不到；但只要有人為了排錯開了後端的埠、或換一台代理設定，整包就對外公開 |
| **信任邊界位置** | **網路 ↔ 後端檔案目錄**。產品裡的下載本來全部走要登入的功能；框架預設附送的靜態路徑繞過了這套檢查，知道路徑就能抓 |
| **元件端點位置** | 修正前：`GET /static/{path}`（Flask 在 `core/app_factory.py` 建立應用時預設註冊）；寫入端如套件 jedi-survey 用 `current_app.static_folder` 寫答案備份 |
| **駭客怎麼打** | ① 找到一個能直連後端埠的位置（排錯時開出來的埠、設定錯的代理）；② 猜或從別處得知檔案路徑（例如 `static/file/answer/upload/{帳號}/…`）；③ 直接下載，不需要帳號 |
| **得手什麼** | 問卷答案附件、意見回饋附件、合規文件匯出檔 |
| **修正的做法** | ① 建立應用時 `static_folder=None`，**不註冊公開路徑**；建立後再把 `app.static_folder` 指回原資料夾，讓套件寫答案備份、設定讀路徑都照舊；② 卡上原寫 `static_url_path=None`，實測在 Flask 3.1 會退回預設的 `/static`、擋不住，故改用上述做法；③ 先搜過全部程式碼與前端，沒有任何地方組 `/static/…` 網址，其他下載都是帶絕對路徑的檔案串流 |
| **狀態** | ✅ 已修，1.21.0 出貨（8-I，CM-2219，BE commit `782ebb8b3`）。驗證：放探針檔，舊程式 200、新程式 404；意見回饋附件下載、問卷答案匯入照常 |

## M24-5 🟡 即時通訊逐筆封包紀錄夾帶通行證進診斷包 {#m24-5}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #204；OWASP：A09 日誌與告警失效） |
| **在哪裡** | 主系統：即時通訊服務（推播畫面更新用）與系統診斷包 |
| **攻擊面位置** | 不是對外攻擊面，而是「拿到診斷包或伺服器紀錄的人」。診斷包是要送出客戶機房給原廠的，出去的那一刻就不在客戶掌控裡 |
| **信任邊界位置** | **客戶機房 ↔ 原廠**。即時通訊服務不該逐筆記封包（裡面有登入通行證），診斷包收紀錄時也該遮；當時兩道都沒有 |
| **元件端點位置** | 即時通訊設定在 `core/app_factory.py` 的 `socketio.init_app(logger=…, engineio_logger=…)`；診斷包入口 `POST /api/1.0/support/diagnostic-bundle`（`api/support/routes/diagnostic_bundle_route.py:DiagnosticBundleRoute`）與指令版 `python -m app.support.diag_cli`；收容器紀錄的是 `infra/support/collector/host_collector.py`、`staged_collector.py`，打包在 `infra/support/diag_packer.py:DiagPacker.pack` |
| **駭客怎麼打** | ① 不用打——使用者正常使用，每一筆即時通訊封包連同登入通行證被寫進容器紀錄；② 客戶遇到問題，管理員產出診斷包寄給原廠；③ 診斷包裡的容器紀錄沒遮，經手診斷包的任何人搜尋就拿到通行證；④ 通行證還在效期內就能直接冒用該使用者登入 |
| **得手什麼** | 使用者的登入通行證 |
| **修正的做法** | ① 即時通訊的逐封包紀錄**只在除錯模式開**，正式環境不記；② 主機版與預蒐版兩支收容器紀錄的程式，打包前都過遮罩；③ **收口關卡放在 `DiagPacker.pack()`**：指令版、畫面版、降級產包三條路都經過它，任何收集器漏遮這裡還攔得到；④ 遮罩規則一次補齊常見漏網寫法（單獨出現的 Bearer 通行證、帶引號的值、連線字串帳密等），首腦驗收再補七種 |
| **狀態** | ✅ 已修，1.21.0 出貨（8-B，CM-2212，BE commit `c8534bfcb`／`8f5755f1d`）。驗證：實產一包、各種假密碼寫法整包 grep 零命中 |

## M24-8 🟡 所有雲端硬碟資料夾設成「知道連結的任何人都能編輯」 {#m24-8}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #205；OWASP：A01 存取控制失效、A02 安全設定錯誤） |
| **在哪裡** | 主系統的雲端硬碟整合：系統替專案、輪次、任務建立的每一個雲端資料夾（連最上層根資料夾）的分享設定 |
| **攻擊面位置** | 拿得到資料夾連結的人。根資料夾連結**公司裡任何一個登入帳號**都查得到；連結一旦被轉傳出去，外面的人也能用 |
| **信任邊界位置** | **Google 雲端硬碟 ↔ 任何持有連結的人**。系統建資料夾時直接開「任何人都能編輯」，等於把權限判斷從產品移交給「誰手上有連結」；被改過的檔案下一輪同步時還會被當成正常證據匯入 |
| **元件端點位置** | `infra/cloud_integration/google_drive/google_drive_api_client.py` 的 `share_anyone_writer()`（`type: anyone, role: writer`），呼叫點在 `app/cloud_integration/service/handlers/init_project_folders_handler.py`、`create_folder_handler.py`、`archive_drive_file_handler.py`、`drive_sync_orchestration_service.py`；觸發入口如 `POST /api/1.0/integrations/google-drive/projects/{project_uid}/init-folders` |
| **駭客怎麼打** | ① 公司裡任一登入帳號取得根資料夾連結；② 用瀏覽器打開，不需要任何專案權限；③ 看、改、刪全公司的證據檔；④ 被改過的檔案下一輪同步時被匯入成正式證據 |
| **得手什麼** | 全公司雲端硬碟上的稽核證據（可讀可改可刪） |
| **修正的做法** | **裁定不修**（決策者 10-01）：雲端硬碟的分享權限，未來改由權限管理控管，或交由客戶在自己的 Google 設定處理，產品不另外管。報告原建議（根資料夾連結不回給沒權限的人、分享改成只限公司網域或專案成員）留作日後回頭檢視的依據 |
| **狀態** | 🚫 裁定不修（決策者 2026-10-01）；`share_anyone_writer` 仍在四處呼叫 |

## M10-20 🟡 摘要報告匯出 PDF 時放行 SVG 插圖，伺服器會替人去連內網 {#m10-20}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #208；OWASP：A01 存取控制失效） |
| **在哪裡** | 任務與成員管理模組（延伸到專案摘要報告）：摘要報告匯出成 PDF。*（模組頁無逐條表，本條依總表描述與修正 commit 撰寫）* |
| **攻擊面位置** | 能編輯摘要報告的專案成員 |
| **信任邊界位置** | **伺服器 ↔ 內網**。伺服器端把報告的 HTML 渲染成 PDF 時會去抓圖片；先前已有插圖來源白名單，但白名單放行了 SVG 內嵌圖，SVG 裡可以再引用外部網址，於是伺服器替使用者對內網發出請求 |
| **元件端點位置** | `GET /api/1.0/project-summary-report/export/{uid}`（主專案 `api/project_summary_report/routes/project_summary_report_route.py` 的 `ProjectSummaryReportExportReportRoute`），以 WeasyPrint `write_pdf` 渲染 |
| **駭客怎麼打** | ① 能編輯報告的人在內容裡放一張 SVG 內嵌圖，SVG 裡引用 `http://內網位址/…`；② 按匯出 PDF；③ 伺服器渲染時去連那個內網位址；④ 用回應時間或渲染結果探測內網服務，或讓伺服器替他打內部管理介面 |
| **得手什麼** | 以伺服器身分對內網發請求的能力，與內網服務的存活資訊 |
| **修正的做法** | 兩道都做：① 插圖白名單排除 `data:image/svg+xml`；② `write_pdf` 帶自訂的 `url_fetcher`，非 `data:` 的來源一律拋錯，渲染器完全不對外發請求（樣板本身沒有外部資源，已查過，不影響排版） |
| **狀態** | ✅ 已修，1.21.0 出貨（8-C，CM-2213，BE commit `a5804a990`／`bde8f9d16`）。驗證：本機起監聽，匯出含惡意 SVG 的報告收到 0 次請求（對照組不帶 `url_fetcher` 會被抓兩次） |

## M24-6 🟡 診斷包設定快照遇到多行值只遮第一行 {#m24-6}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #211；OWASP：A09 日誌與告警失效） |
| **在哪裡** | 主系統：系統診斷包會附一份設定快照，把設定值裡的密碼遮掉 |
| **攻擊面位置** | 拿到診斷包的人。今天出貨的資料庫密碼剛好是單行，所以還沒踩到，但換成多行格式（例如憑證）就會 |
| **信任邊界位置** | **客戶機房 ↔ 原廠**。遮蔽應該以「整個設定值」為單位；當時以「一行」為單位，先把設定組成多行文字再逐行比對，第二行以後已經沒有鍵名可以判斷 |
| **元件端點位置** | `infra/support/diag_masking.py`：`mask_env_mapping`（畫面版設定快照）、`mask_env_text`（設定檔版）；由 `infra/support/collector/container_collector.py` 等收集器呼叫，入口同 `POST /api/1.0/support/diagnostic-bundle` |
| **駭客怎麼打** | ① 客戶把某個設定值改成多行格式，例如貼了一段私鑰或多行憑證；② 產診斷包時，只有第一行被遮成星號；③ 第二行以後原文進了診斷包，跟著送出客戶機房；④ 經手診斷包的人拼回整段私鑰 |
| **得手什麼** | 多行格式的密碼或私鑰 |
| **修正的做法** | ① 畫面版改用 `mask_env_mapping()`：**先依鍵名判定是不是機密，再組成「名稱=值」**，整個值一起遮；② 設定檔版 `mask_env_text()` 遇到機密鍵的跨行引號值整段吞掉；③ 非機密鍵的值（例如帶帳密的資料庫連線字串）也過一次日誌遮罩規則並列進清單；④ 看到 PEM 憑證開頭就整段遮、不看鍵名 |
| **狀態** | ✅ 已修，1.21.0 出貨（8-B，CM-2212，BE commit `c8534bfcb`／`8f5755f1d`） |

## M17-3 ⚪ 用網址直接開一則公告，完全不做任何檢查 {#m17-3}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #111；OWASP：A01 存取控制失效） |
| **在哪裡** | 公告模組：打開單則公告 |
| **攻擊面位置** | 已登入使用者可打的公告 API；只要有公告編號（第 4 條的列表就會給） |
| **信任邊界位置** | **使用者 ↔ 公告服務**。列表有依部門過濾，單筆讀取沒有；擋著的只有「編號猜不到」。調部門之後，舊部門的公告用舊網址還是打得開，還沒發布的也能提前看 |
| **元件端點位置** | `GET /api/1.0/bulletin/{uid}`（套件 `jedi_bulletin/api/routes/bulletin_route.py` 的 `BulletinDetailRoute.get`）→ `app/service/bulletin_service.py` 的 `get_bulletin()` |
| **駭客怎麼打** | ① 任一登入帳號；② 從第 4 條或舊書籤拿到公告編號；③ 直接打開；④ 看到不屬於自己部門、或尚未發布的公告 |
| **得手什麼** | 別部門與未發布的公告內容 |
| **修正的做法** | 與第 4 條合併成同一套設計：① 公告新增「發送範圍」欄位（全公司／指定部門），舊公告一律標全公司，行為與修改前相同；② 單筆讀取帶上使用者脈絡，比對發送範圍，看不到回 404（公告存不存在本身就是資訊）；③ 判斷規則與清單的資料庫條件必須一致，否則會「列表看得到、點進去 404」；④ 同卡補修改、刪除必須是作者本人 |
| **狀態** | ✅ 已修（FR-114.1-5a／CM-2026，套件 commit `8ca1c279`） |

## M17-4 ⚪ 公告列表碰到沒有部門的帳號就整個不過濾 {#m17-4}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #111；OWASP：A01 存取控制失效） |
| **在哪裡** | 公告模組：公告列表 |
| **攻擊面位置** | 已登入、沒有被分配部門的帳號；而且「要不要過濾」這個開關是呼叫端自己在請求裡傳的 |
| **信任邊界位置** | **使用者 ↔ 公告服務**。服務寫成「有部門才過濾」，沒部門就跳過整個過濾；過濾開關由請求帶，不是伺服器判定 |
| **元件端點位置** | `POST /api/1.0/bulletins`（套件 `jedi_bulletin/api/routes/bulletin_route.py` 的 `BulletinListRoute`）→ `app/service/bulletin_service.py`、`infra/repository/bulletin_repo_impl.py` |
| **駭客怎麼打** | ① 一個沒有被分配部門的帳號（或被調離部門的人）；② 打公告列表；③ 拿到整個客戶的全部公告，含草稿、含過期；④ 連同它們的編號，餵給第 3 條 |
| **得手什麼** | 全客戶的公告與編號 |
| **修正的做法** | ① 服務在「依部門過濾」模式下**一律**設定可見部門清單，沒部門時是空清單，意思是「只看得到全公司那類」；② 資料庫條件改成「發送範圍＝全公司，或＝指定部門且命中」；③ 發送範圍查詢刻意不開放成搜尋條件——底層對字串條件走模糊比對並歸進 OR 群組，加進去會讓搜尋框變成恆真 |
| **狀態** | ✅ 已修（FR-114.1-5a／CM-2026，套件 commit `8ca1c279`） |

## M03-8 ⚪ 權限矩陣取消勾選後，後端八支讀取照樣回全部 {#m03-8}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #113；OWASP：A01 存取控制失效） |
| **在哪裡** | 弱點檢測整合模組：「掃描基準」功能的八支讀取（清單、下拉、單筆、版本歷史、主檔使用狀況、版本使用狀況、規則清單、抽取狀態） |
| **攻擊面位置** | 已登入、但被租戶管理員在權限矩陣取消勾選的帳號；直接對 API 送請求 |
| **信任邊界位置** | **使用者 ↔ 檢測服務**。前端依權限藏了選單，後端八支讀取一支都沒掛權限——管理員被騙了，那一格勾選其實不生效。程式裡還有一句「讀取端刻意不掛」的說明 |
| **元件端點位置** | 套件 `jedi_detection/api/routes/detection_profile_route.py`：`POST /detection-tool-profiles/list`、`GET /detection-tool-profiles/menu`、`GET /detection-tool-profiles/{uid}`、`…/{uid}/versions`、`…/{uid}/referencing-usage`、`GET /detection-tool-profile-versions/{uid}/referencing-usage`、`…/{uid}/controls`、`…/{uid}/extraction` |
| **駭客怎麼打** | ① 被拿掉權限的帳號；② 側邊選單看不到入口，直接送 API；③ 掃描基準的完整內容照樣回來 |
| **得手什麼** | 管理員以為已收回的掃描基準內容 |
| **修正的做法** | ① 八支 GET 全部掛新的 `_profile_read`（值取自既有的 `detection-profile.read`，該權限點早已存在，不需建新的）；② 寫法沿用同檔的延遲求值形狀——路由類別在載入期定義、那時讀不到設定，簡化成立即求值會在掛載前炸；③ 改掉兩處「讀取不設權限」的過期說明，免得下一個人當成設計拿掉；④ 只加在「能不能進這個功能」那層，「看得到哪些」的三路 OR（公版／自有／上游分享）完全不動；⑤ 首腦另裁：替缺這顆權限的 5 個既有角色補資料，隨出貨（BE `3a651140e`） |
| **狀態** | ✅ 已修（CM-2042，套件 commit `5a9baf2b`；權限資料 BE `3a651140e`） |

## M04-12 ⚪ 共用的「資料轉成回覆內容」工具資料上有什麼就吐什麼 {#m04-12}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #114；OWASP：A01 存取控制失效、A06 不安全的設計） |
| **在哪裡** | 共用基礎模組：把資料物件轉成回覆內容的共用工具（所有套件都用） |
| **攻擊面位置** | 所有用這支工具組回覆的 API；呼叫的人只要忘了自己先篩選，畫面就會拿到不該看的內部欄位 |
| **信任邊界位置** | **後端 ↔ 瀏覽器**。工具不分欄位、整包轉出；已經真的出過事——AI 儀表板回覆夾帶密碼鹽值，走的正是這支（[M15-4](#m15-4)） |
| **元件端點位置** | 套件 `jedi_common/utils/serialization_util.py` 的 `to_serializable()`；已知受害入口 `POST /api/1.0/ai-dashboard/auto-generate` |
| **駭客怎麼打** | ① 找一支用這個工具、又沒自己篩欄位的查詢；② 正常呼叫；③ 回應裡帶出物件上的全部屬性，包含密碼雜湊、鹽值、超管旗標、通行證 |
| **得手什麼** | 物件上不該外送的內部欄位 |
| **修正的做法** | ① 工具新增 `_SENSITIVE_SKIP_KEYS`：鹽值、密碼、超管旗標、通行證類欄位一律不輸出；② 物件分支與字典分支都擋——有 `to_dict()` 的物件會先轉字典再走字典分支，只擋一邊等於留下繞過路徑；③ 已知取捨：名單制漏登記會靜默外洩，長期方向是資料本身帶標記，不在本卡範圍 |
| **狀態** | ✅ 已修（套件 commit `248132ec`，卡號 CM-2064）。⚠️ 總表寫 CM-2038，但 CM-2038 那顆 commit 自己註明此條「實查已由 CM-2064 完成，本棒未再動該檔」 |

## M06-11 ⚪ 流程範本的列表與單筆讀取沒有掛權限檢查 {#m06-11}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #115；OWASP：A01 存取控制失效） |
| **在哪裡** | 稽核流程模組：流程範本（稽核流程的階段設計、角色指派、判斷條件）的清單與單筆 |
| **攻擊面位置** | 同一家客戶內任何登入帳號，直接打 API；前端選單擋住了沒權限的人，功能本身是敞開的 |
| **信任邊界位置** | **使用者 ↔ 流程範本服務**。同檔的新增、修改、刪除都有權限檢查，讀取兩支沒有；跨客戶由資料庫隔離擋住（已實際查證） |
| **元件端點位置** | 主專案 `api/flow_engine/routes/flow_template_route.py`：`GET /flow-engine/flow-templates`（`FlowTemplatesRoute.get`）、`GET /flow-engine/flow-templates/{uid}`（`FlowTemplateRoute.get`） |
| **駭客怎麼打** | ① 同客戶內沒被授權看範本的帳號；② 直接打範本列表與單筆；③ 讀走全部流程範本與系統內建範本的完整設計 |
| **得手什麼** | 客戶與系統內建的稽核流程設計 |
| **修正的做法** | ① 列表與單筆補 `_flow_template_read`；② **不是單純只收「看範本」權限**：建立專案第一步要選流程範本、會打這支列表，只收那一顆會讓有建案權限但沒範本權限的人卡住，故用「看範本或能建專案」任一即過 |
| **狀態** | ✅ 已修（CM-2035，BE commit `e2c32258d`） |

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

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #116；OWASP：A02 安全設定錯誤） |
| **在哪裡** | 共用基礎套件：一支呼叫外部 AI 幫程式產生註解的開發輔助腳本，檔頭自己寫明「不是產品程式碼」 |
| **攻擊面位置** | 拿到客戶機器上安裝內容的人——它會跟著出貨套件一起裝到客戶機器上 |
| **信任邊界位置** | **開發環境 ↔ 出貨產物**。開發工具不該跨進出貨範圍；它帶進了一個只有它在用的外部 AI 依賴，也讓出貨內容多了一段沒人維護的程式 |
| **元件端點位置** | 套件 `jedi-common/jedi_common/utils/gen_comment.py`（無 HTTP 端點）；`jedi-common/pyproject.toml` 的 `[ai]` 選配依賴 |
| **駭客怎麼打** | ① 拿到客戶機器上的安裝目錄；② 找到這支開發工具，了解內部開發流程與用到的外部服務；③ 若哪天有人把它接進產品流程，它會把原始碼送給外部 AI |
| **得手什麼** | 開發流程資訊（實際風險低） |
| **修正的做法** | 拆除了什麼：確認五個程式庫全部零引用後整支刪除，連帶移除只有它用得到的 `[ai]` 選配依賴與 README 對它的說明；同卡另刪一個全系統沒有任何規則讀取的死開關（`app.can_manage_orgs`），免得被誤以為已有部門層級檢查 |
| **狀態** | 🗑️ 裁定刪除，已刪除（FR-114.3-3／CM-2045，套件 commit `599ab7c5`，1.21.0 出貨） |

## M06-14 ⚪ 三支接收檔案路徑、但現在完全沒人呼叫的程式 {#m06-14}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #118；OWASP：A01 存取控制失效） |
| **在哪裡** | 稽核流程套件：「存流程圖到檔案」「匯出 XML」「從 XML 匯入」三支，都直接拿傳入的路徑開檔讀寫 |
| **攻擊面位置** | 目前沒有——全系統零呼叫。現在無害的唯一理由是沒人用它 |
| **信任邊界位置** | **呼叫端給的路徑 ↔ 主機檔案系統**。這三支不檢查路徑，只要哪天有人把使用者輸入接到參數上，就變成任意檔案讀寫 |
| **元件端點位置** | 套件 `jedi-flow-engine/jedi_flow_engine/common/utils/bpmn_generator.py`：`save_bpmn_to_file()`、`export_xml()`、`import_xml()`（無 HTTP 端點） |
| **駭客怎麼打** | ①（假設日後有人把它接到 API）攻擊者在參數裡填 `../../etc/…` 這類路徑；② 程式照路徑開檔；③ 讀到伺服器上的任意檔案，或把內容寫到任意位置 |
| **得手什麼** | （接上之後）伺服器上任意檔案的讀寫 |
| **修正的做法** | 拆除了什麼：三支整支刪除（全程式庫零呼叫，FR-088 已查證），連帶移除只給匯入用的 import；有人在用的「匯出／匯入成字串」兩支保留 |
| **狀態** | 🗑️ 裁定刪除，已刪除（隨 CM-2059，套件 commit `4a416ece`，1.21.0 出貨）。⚠️ 總表與模組頁沒寫這半的卡號，刪除 commit 由本頁回查補上 |

## M06-16 ⚪ 套件裡兩組「接好但沒人用」的備用服務，完全沒有權限檢查 {#m06-16}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #118；OWASP：A01 存取控制失效） |
| **在哪裡** | 稽核流程模組：主系統組裝好、放在那裡備用的「任務新增刪改」服務（套件提供、主系統自己重寫了一份有檢查的版本） |
| **攻擊面位置** | 目前沒有——整個程式庫沒有任何地方呼叫它；只要哪天有人接上去，就憑空多出一組沒有檢查的入口 |
| **信任邊界位置** | **未來的呼叫端 ↔ 任務資料**。套件本身不做權限檢查是設計（那是宿主的事），但宿主把它註冊成可注入的服務，等於留了一個沒守門的零件在架上 |
| **元件端點位置** | 主專案 `di_containers/flow_engine/workflow_excution_containers.py` 的 `job_execution_service` 註冊（無 HTTP 端點） |
| **駭客怎麼打** | ①（假設日後有人誤接）新功能直接注入這支服務來改任務；② 它不檢查專案成員與客戶歸屬；③ 任何能打到新功能的人都能改刪別人的任務 |
| **得手什麼** | （誤接之後）改刪任意任務 |
| **修正的做法** | 拆除了什麼：拆掉沒人注入的 `job_execution_service` 註冊與其 import（全庫搜尋只剩容器自己）；它用到的領域服務仍被主系統那份在用，保留 |
| **狀態** | 🗑️ 已拆除，1.21.0 出貨（8-E，CM-2215，BE commit `822216b6c`） |

## M21-2 ⚪ 任何登入帳號都拿得到全站使用者名冊（含電子郵件） {#m21-2}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #121；OWASP：A01 存取控制失效） |
| **在哪裡** | 意見回饋模組（重新檢視）：一支查成員名冊的入口——查證後發現沒有任何地方在用 |
| **攻擊面位置** | 任何登入帳號，送一個空白查詢 |
| **信任邊界位置** | **客戶 ↔ 客戶**。那張成員表連「這是哪家客戶的」欄位都沒有、也沒有隔離規則；入口只驗登入，空查詢回整張表。真正的問題是它是一個沒人看管、隨時可能被接上的入口 |
| **元件端點位置** | 修正前：`GET /api/1.0/issue/get_members`（套件 `jedi_issue/api/routes/issue_member_route.py` 的 `IssueMemberRoute`）→ `member_service.get_members()` → `.query(Members).all()` |
| **駭客怎麼打** | ① 任一登入帳號；② 打這支入口、不帶條件；③ 拿回整張全站成員名冊與電子郵件 |
| **得手什麼** | 全站成員名冊與電子郵件（三個環境當時都是 0 筆） |
| **修正的做法** | 拆除了什麼：刪除對外查詢入口（路由、服務、領域服務、資料存取的 `get_members` 一路到底，連同抽象介面宣告，否則實作類別無法建立），以及外掛契約與組裝裡指向它的接線；**資料表與同步程式保留**——意見回饋指派負責人功能內部還在用。主專案同步改註解，說明是刪除不是搬遷 |
| **狀態** | 🗑️ 裁定刪除，已刪除（FR-114.3-4／CM-2046，套件 commit `04e0e6c1`、BE `0fb8d93c0`，1.21.0 出貨）。驗證：入口回 404，意見回饋建立到刪除全流程正常 |

## M02-9 ⚪ 本機儲存刪檔時漏清轉檔產生的 PDF {#m02-9}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #123；OWASP：A06 不安全的設計） |
| **在哪裡** | 檔案上傳下載模組：本機儲存模式下刪除檔案 |
| **攻擊面位置** | 不是對外的攻擊面；是「以為刪了、其實還在」——拿到主機檔案系統的人（維運、備份、主機被入侵）看得到 |
| **信任邊界位置** | **使用者的刪除意圖 ↔ 主機上的實體檔案**。預覽時轉出的快取 PDF 是衍生資料，刪主檔時應一併刪；物件儲存那條有做，本機儲存那條漏了 |
| **元件端點位置** | 刪除入口 `DELETE /api/1.0/file/upload/{uid}`、`/file/uploads`（套件 `jedi_file_upload/api/routing.py`）→ `infra/adapter/local/local_file_adapter.py` 的 `delete_file()`、`delete_files_by_uids()`；衍生檔清理 `infra/adapter/derived_files.py` |
| **駭客怎麼打** | ① 使用者刪掉一份敏感檔案；② 本機儲存只刪了主檔；③ 先前預覽時轉出的 PDF 還留在硬碟上；④ 能讀主機檔案或備份的人照樣拿到內容 |
| **得手什麼** | 使用者以為已刪除的檔案內容（PDF 版） |
| **修正的做法** | ① 把物件儲存那條的衍生檔清理抽成共用函式 `delete_derived_files`（吃資料存取元件與一個刪實體的函式）；② 本機與物件儲存兩種模式共用；③ 維持寬容行為：刪衍生檔失敗只記日誌、不擋主檔刪除 |
| **狀態** | ✅ 已修（CM-2062，套件 commit `a061438f`） |

## M02-11 ⚪ 連自己裝的檔案儲存空間時不加密 {#m02-11}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #124；OWASP：A04 加密機制失效） |
| **在哪裡** | 檔案上傳下載模組：後端連到同一套安裝裡的物件儲存（MinIO／SeaweedFS）存取客戶上傳的檔案。**與 Google 雲端硬碟無關**——那條走 Google 官方工具，底層固定加密 |
| **攻擊面位置** | 後端與儲存容器之間的網路。這是同一台主機上容器之間的內部通訊，**要攔得先進到這台主機** |
| **信任邊界位置** | **後端容器 ↔ 儲存容器**。兩者在同一套安裝、同一台主機的內部網路裡，邊界在主機外圍而不在這條線上。查 DEV 七筆儲存設定**全部明確填成不加密**——是現況設定，不是忘了填，所以只改預設值對既有環境無效 |
| **元件端點位置** | `app/upload_file/service/managed_file_upload_service.py` 的 `_build_config_dto()`（`secure=v.get('secure', False)`）→ 套件 `jedi_file_upload/infra/adapter/minio/minio_adapter.py`（`secure=config.secure`）、`seaweedfs/seaweedfs_adapter.py`；所有經過的上傳下載端點（如 `POST /api/1.0/file/upload`、`GET /api/1.0/file/download/{uid}`） |
| **駭客怎麼打** | ① 攻擊者先取得客戶那台主機的存取權；② 在容器網路上側錄後端與儲存容器的流量；③ 讀到檔案內容與儲存帳密；④ 但到了這一步，直接讀主機上的設定檔與儲存資料夾更快——攔線沒有額外收穫 |
| **得手什麼** | 客戶上傳的檔案內容與儲存帳密（前提是已經進到主機） |
| **修正的做法** | **裁定不修**（決策者 2026-09-23 裁、首腦比照 #101 內建容器間通訊不修）：同一套安裝內容器之間的內部通訊，邊界在主機。報告當初建議的「出貨預設開加密、既有環境另評估切換時機」留作日後回頭檢視的依據 |
| **狀態** | 🚫 裁定不修（CM-2063 驗證卡內裁定；比照 #101） |

## M05-11 ⚪ 問卷資料夾列表可以叫出系統刻意隱藏的資料夾 {#m05-11}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #126；OWASP：A01 存取控制失效） |
| **在哪裡** | 問卷模組：問卷資料夾列表 |
| **攻擊面位置** | 任何登入帳號，連權限點都沒掛 |
| **信任邊界位置** | **使用者給的查詢條件 ↔ 資料夾服務**。路由把請求內容整包展開成查詢條件，「已刪除」與「排除系統資料夾」這兩個內部開關也能從請求傳進來；同一支檔案的下拉選單就是寫對的範例——條件由程式寫死 |
| **元件端點位置** | `POST /api/1.0/survey-folders`（套件 `jedi_survey/api/routes/survey_folder_route.py` 的 `SurveyFoldersRoute`）→ `app/service/survey_folder.py` |
| **駭客怎麼打** | ① 任一登入帳號；② 對資料夾列表送 `is_delete=1` 或 `exclude_system=false`；③ 看到已刪除的資料夾，以及流程自動產生的內部快照資料夾（`__` 開頭） |
| **得手什麼** | 已刪除資料夾與系統快照資料夾的清單 |
| **修正的做法** | ① 列表的查詢條件改由後端寫死：只查未刪除、排除系統資料夾，照同檔下拉那支的寫法；前端本來就只送「未刪除」，行為不變；② 同卡另修「修改資料夾時多塞一個欄位就能把資料夾軟刪、繞過刪除守門」：修改改用只收名稱與描述的請求格式，未知欄位丟棄 |
| **狀態** | ✅ 已修（CM-2060，套件 commit `54ac36f7`） |

## M05-12 ⚪ 問卷資料夾列表把資料庫原始錯誤整句吐回畫面 {#m05-12}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #127；OWASP：A10 例外狀況處理不當） |
| **在哪裡** | 問卷模組：問卷資料夾列表 |
| **攻擊面位置** | 已登入使用者可打的資料夾列表 API，任何能看問卷的帳號都構成攻擊面 |
| **信任邊界位置** | **伺服器內部 ↔ 使用者畫面**。產品有一套統一錯誤處理（回固定錯誤碼、原文進日誌）；這支自己包了一層「不管出什麼錯都接住、把錯誤內容原樣回傳」，繞過了統一處理 |
| **元件端點位置** | `jedi_survey/api/routing.py`：`POST /survey-folders`（`SurveyFoldersRoute.post`，`jedi_survey/api/routes/survey_folder_route.py`） |
| **駭客怎麼打** | ① 任一能看問卷的帳號，打開問卷資料夾列表；② 改請求內容，在查詢條件裡塞一個型別不對的值（例如該填數字的地方填文字）；③ 資料庫報錯；④ 這支把錯誤原文整句回到畫面，資料表名稱、欄位名稱、查詢片段全看得到 |
| **得手什麼** | 問卷那塊的資料表結構，方便拼下一步攻擊 |
| **修正的做法** | ① 拿掉 `try/except Exception` 後「回傳原始錯誤訊息」那段，讓錯誤照原路交給統一錯誤處理（固定錯誤碼＋完整日誌）；② 同一支的查詢條件改由後端寫死（只查未刪除、預設排除系統自動產生的資料夾），不再接任何請求參數，這個錯誤面本身也收掉了 |
| **狀態** | ✅ 已修（CM-2060，套件 commit `54ac36f7`，1.21.0 出貨） |

## M07-6 ⚪ 按「刪除整批」之後，判定結果與分類紀錄仍永久留在主機上 {#m07-6}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #129；OWASP：A06 不安全的設計） |
| **在哪裡** | 證據自動分類模組：分類工作目錄（每次分類的輸入檔、判定結果、容器紀錄） |
| **攻擊面位置** | 要有主機的登入權限才看得到這些殘檔，所以不是越權存取；問題是「使用者以為刪乾淨了」，而合規客戶常有資料保留期限要求 |
| **信任邊界位置** | **使用者的刪除意圖 ↔ 主機上的工作目錄**。分類跑完只清了上傳檔子目錄，判定結果與紀錄留在工作目錄；刪整批只刪資料庫列，工作目錄原封不動，磁碟也無限成長 |
| **元件端點位置** | `DELETE /api/1.0/evidence-batches/{batch_uid}`（套件 `jedi_evidence_classification/api/routes/evidence_batch_route.py` 的 `EvidenceBatchDetailRoute.delete`）→ `app/service/evidence_batch_service.py` 的 `delete_batch()`；分類 `POST /evidence-batches/{batch_uid}/classify`；清理函式 `infra/job_dir_cleanup.py` |
| **駭客怎麼打** | ① 使用者上傳一批證據跑分類，之後按「刪除整批」；② 畫面上消失；③ 主機的工作目錄裡判定結果與紀錄都還在；④ 拿到主機或備份的人翻得到 |
| **得手什麼** | 使用者以為已刪除的分類結果與證據相關紀錄 |
| **修正的做法** | ① 每次分類跑完（不論成功失敗），結果與紀錄先存進資料庫，再把**整個工作目錄**刪掉；② 刪整批時再把這一批留下的所有工作目錄清一次；③ 清理只刪位於工作目錄底下、本身不是捷徑的目錄，不會跟著捷徑刪到別處；④ 清理失敗只記日誌、不回滾刪除；⑤ 同卡另補開發機容器的記憶體、CPU、程序數上限與逾時真的停掉容器 |
| **狀態** | ✅ 已修，1.21.0 出貨（CM-2227，套件 commit `ecddb72c`／`85370a17`） |

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

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #130；OWASP：A02 安全設定錯誤、A04 加密機制失效） |
| **在哪裡** | 證據自動分類模組：後端啟動分類程式時，把 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 逾時，錯誤紀錄不含金鑰 |

## M08-6 ⚪ 回報竄改時原封不動送出紀錄檔最後 50 行 {#m08-6}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #133；OWASP：A09 日誌與告警失效） |
| **在哪裡** | 防竄改模組：偵測到產品檔案被竄改時，會把主機紀錄檔最後 50 行當作鑑識證據，寫進標記檔與資料庫並回報原廠 |
| **攻擊面位置** | 不是對外攻擊面；拿到竄改回報內容的人就能看到那 50 行。評低是因為回報目前送的是本機位址，不會離開那台機器 |
| **信任邊界位置** | **客戶主機 ↔ 原廠（回報管道）**。那 50 行是刻意設計的證據（框出竄改時間窗），應該在收錄前就過遮蔽；當時原文收錄 |
| **元件端點位置** | 套件 `jedi_integrity/infra/forensic.py:_log_tail`（取最後 50 行）→ `collect_trigger_context` → `infra/lc_report.py:report_tamper_best_effort`（回報）；主專案在 `main.py:_mask_integrity_log_lines` 把遮蔽器接進 `build_integrity_context` |
| **駭客怎麼打** | ① 無需攻擊者——產品偵測到檔案被改；② 系統讀紀錄檔最後 50 行，裡面剛好有一行印著密碼或通行證；③ 這 50 行原文寫進竄改紀錄並回報；④ 能看到竄改紀錄或回報的人拿到憑證 |
| **得手什麼** | 夾在紀錄檔尾段的密碼或通行證 |
| **修正的做法** | ① 套件新增選填介面 `LogLineMasker`，`_log_tail` 先過宿主給的遮蔽器才收錄；② **未接遮蔽器或遮蔽失敗一律不收原文**（寧缺證據，不外洩憑證）；③ 主專案把診斷包那套現成的 `mask_log_lines` 接進去，不重寫（BE commit `0dda326b8`）；④ 不採「只送行數與時間」，因為那會丟掉鑑識價值 |
| **狀態** | ✅ 已修（CM-2053，套件 commit `953697c9`＋BE commit `0dda326b8`）。驗證：手測紀錄裡的 password、Authorization、DB_PASSWORD 被遮，無遮蔽器時不收原文 |

## M22-3 ⚪ 儲存設定的密鑰沒在遮蓋名單裡，明文回給瀏覽器 {#m22-3}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #134；OWASP：A04 加密機制失效、A01 存取控制失效） |
| **在哪裡** | 系統設定模組：客戶管理員打開「儲存設定」頁時，後端把設定整份回給瀏覽器 |
| **攻擊面位置** | 有正當權限的客戶管理員的瀏覽器——任何看得到他瀏覽器流量、存檔、或前端錯誤紀錄的人。不需要自己有帳號 |
| **信任邊界位置** | **後端 ↔ 瀏覽器**。密碼類欄位跨出後端前應該遮掉；系統本來就有遮蓋機制，但靠兩份名單決定遮什麼（哪幾類設定、哪些欄位名），**兩份都漏了檔案儲存**。寄信與員工帳號目錄剛好有登記所以沒事——這是「名單制漏登記＝密碼直接外洩」的實例 |
| **元件端點位置** | `GET /api/1.0/system/configs/{group}`、`GET /api/1.0/system/config/{uid}`（`jedi_system_core/api/routing.py`）→ 遮蓋名單 `jedi_system_core/plugin/contract.py` 的 `DEFAULT_SECRET_MASKED_GROUPS`／`DEFAULT_SECRET_VALUE_KEYS`；主專案 `app/system_config/service/guarded_system_config_service.py` 的 `_mask_secrets`／`_merge_storage_secrets` |
| **駭客怎麼打** | ① 客戶管理員照常打開儲存設定頁；② 回應內容裡帶著儲存服務的密鑰原文；③ 攻擊者只要能看到這次回應——瀏覽器開發工具、被存下來的頁面、前端錯誤回報、公司的網路側錄——就拿到密鑰；④ 用它直接讀寫客戶的檔案儲存 |
| **得手什麼** | 客戶檔案儲存服務的密鑰 |
| **修正的做法** | ① **套件遮蓋名單補上**：設定類別加 `STORAGE_CONFIG`，欄位名加 `secret_key`、`minio_secret_key`（兩種鍵名都有人用）；帳號欄 `access_key` 刻意不遮，否則使用者看不出自己填的帳號對不對；② **寫入側補「沒帶就沿用原值」**：前端拿不到密鑰後，只改個桶名按儲存就會把密鑰抹成空，故主專案 `_merge_storage_secrets` 從既有列補回（CM-2054）；③ 後續 CM-2063 再往前一步改成**預設不回真值**：服務層讀出儲存設定一律拿掉密鑰、換成「有沒有設」旗標，名單變成第二層；上傳服務另開一個可讀真值的注入點 |
| **狀態** | ✅ 已修（CM-2054，套件 commit `16a3e41c`、主專案 `e90d8e3cb`；加強 CM-2063 主專案 `7c904eb6e`、前端 `756783b`） |

## M04-4 ⚪ 兩張運作紀錄表沒有客戶隔離 {#m04-4}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #138；OWASP：A01 存取控制失效；模組頁原評 🟡 中）） |
| **在哪裡** | 共用基礎模組：記錄每個請求的操作日誌表，與記錄程式運作的系統日誌表 |
| **攻擊面位置** | 能直接連進資料庫的人（維運、資料庫帳號持有者）；產品畫面上看日誌另有權限把關 |
| **信任邊界位置** | **客戶 ↔ 客戶（在資料庫層）**。兩張表連可以拿來隔離的客戶欄位都沒有；登入失敗、系統啟動、背景排程這類紀錄天生分不出是哪家客戶的 |
| **元件端點位置** | 資料表 `public.api_logs`、`public.system_logs`；寫入在主專案 `common/middleware/app_mw.py` 與套件 `jedi_common/logger/db_log/db_handler.py`；畫面查詢 `GET /api/1.0/log/api-logs`（套件 jedi-log） |
| **駭客怎麼打** | ① 取得一個能直接連資料庫的帳號；② 查這兩張表；③ 看到全部客戶的運作紀錄（誰在什麼時候打了什麼） |
| **得手什麼** | 全部客戶的系統運作紀錄 |
| **修正的做法** | **裁定不修**：兩個理由——這類紀錄天生分不出客戶（登入失敗時還不知道是誰）；而落地版一家客戶一套資料庫，「跨客戶」不成立。改成明確定位為維運資料、限縮誰能讀 |
| **狀態** | 🚫 裁定不修（模組頁 M04 第 4、5 條裁定） |

## M04-5 ⚪ 完整的程式出錯內容被寫進那張沒有隔離的紀錄表 {#m04-5}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #138；OWASP：A01 存取控制失效；模組頁原評 🟡 中）） |
| **在哪裡** | 共用基礎模組：系統日誌表裡的程式出錯內容（完整錯誤堆疊） |
| **攻擊面位置** | 能直接連進資料庫的人；看得到這張表的只有維運人員 |
| **信任邊界位置** | **應用程式 ↔ 維運紀錄**。完整出錯內容可能帶出內部路徑與資料片段，但這正是維運除錯要用的 |
| **元件端點位置** | 資料表 `public.system_logs`；寫入 `jedi_common/logger/db_log/db_handler.py` 的 `DBLogHandler.emit` |
| **駭客怎麼打** | ① 取得資料庫帳號；② 查系統日誌表的錯誤欄位；③ 從錯誤堆疊讀到程式路徑、套件版本與出錯當下的資料片段 |
| **得手什麼** | 系統內部結構與出錯當下的資料片段 |
| **修正的做法** | **裁定維持原樣**：與第 4 條同一套理由——看得到這張表的只有維運人員，完整出錯內容正是他們除錯要用的。（後續 CM-1920 已把寫進資料庫的範圍收到只放行稽核事件與錯誤等級） |
| **狀態** | 🚫 裁定不修（模組頁 M04 第 4、5 條裁定） |

## M04-6 ⚪ 連暫存資料庫時寫死不確認對方身分 {#m04-6}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #139；OWASP：A04 加密機制失效、A07 身分驗證失效；模組頁原評 🟡 中；STRIDE：資料外洩、冒充身分）） |
| **在哪裡** | 共用基礎模組：主系統連「暫存資料庫」（快取伺服器，放 AI 聊天紀錄、問卷共編名單等）的連線工具 |
| **攻擊面位置** | 後端與快取伺服器之間的網路。以目前的部署形態，兩者在同一台主機的容器內部網路，**要攔得先進到這台主機**；而且只有部署方把加密打開時這行才會被走到（預設不加密） |
| **信任邊界位置** | **後端容器 ↔ 快取容器**。邊界在主機外圍。進到主機之後直接讀設定檔就有快取密碼，攔線沒有意義——這是裁定不修的理由 |
| **元件端點位置** | 掃描當時：主專案 `common/util/redis_client_util.py` 第 32 行寫死 `ssl_cert_reqs=False`。現況：該檔已在 CM-2043 刪除，問卷（`infra/survey/adapters.py`）與 AI 助手（`core/plugins/ai_bot.py`）改用套件 `jedi_iam/common/utils/redis_client_util.py` 的 `RedisClient`；另一條主連線在 `config/config.py` 的 `REDIS_URL`（交給 flask_redis） |
| **駭客怎麼打** | ① 攻擊者先進到客戶那台主機；② 部署方若開了快取加密，攻擊者在容器網路上假扮成快取伺服器，拿一張自簽證明；③ 系統不驗，把快取帳密與聊天紀錄、共編名單送過來；④ 但他早就能直接讀主機上的設定檔 |
| **得手什麼** | 快取伺服器帳密與其中的聊天紀錄、共編名單（前提是已經進到主機） |
| **修正的做法** | **裁定不修**（2026-09-21 M04 內化修訂）：連線跑在容器內部網路、沒有對外。**但實查程式現況與裁定時不同**：隔天 CM-2043（`d61d5342d`，2026-09-22）為了另一條同形狀的問題（M13-10）把這支寫死不驗的複製品整支刪掉，兩個呼叫點改用套件那支「開了加密就強制驗憑證與主機名稱」的版本——**掃描點名的那一行已不存在**。殘留的一處：`config/config.py` 給 flask_redis 的 `REDIS_URL` 寫死 `redis://`，不理會加密設定（同檔給即時通訊用的那條有依設定切換），屬於「不加密」而非「加密不驗」，在同一裁定範圍內 |
| **狀態** | 🚫 裁定不修（M04 內化修訂 `2d9d642b7`）；實際上掃描點名的那行已隨 CM-2043（`d61d5342d`）刪除 |

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

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #140；OWASP：A04 加密機制失效、A02 安全設定錯誤） |
| **在哪裡** | 共用基礎模組：所有套件共用的日誌工具，一被載入就自動啟動遙測（追蹤與指標），往本機 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；OWASP：A02 安全設定錯誤） |
| **在哪裡** | 防竄改檢查模組：機器被鎖定後，系統原本的埠改由一個鎖定畫面接手，顯示報修用的資訊 |
| **攻擊面位置** | 任何打得開這個系統網址的人，**包含還沒登入的人** |
| **信任邊界位置** | **網路 ↔ 鎖定中的機器**。鎖定畫面在服務停擺時接手，沒有登入可言；顯示的是機器指紋與鎖定事件編號 |
| **元件端點位置** | 套件 `jedi-integrity/jedi_integrity/infra/lockdown.py`：鎖定殼接手原服務埠，`_render_html()`（瀏覽器）／`_render_json()`（其他）回傳 `machine_fingerprint`、`tamper_event_id`、偵測時間 |
| **駭客怎麼打** | ① 機器被鎖定時，任何人打開系統網址；② 看到機器指紋與鎖定事件編號；③ 拿著這兩個值去申請解鎖檔——但還得通過原廠的人工窗口確認身分 |
| **得手什麼** | 兩個報修識別碼（單獨拿到無法解鎖） |
| **修正的做法** | **裁定不修**（決策者）：這兩個值本來就是報修識別碼，就是要讓現場的人抄下來報修；要換到解鎖檔必須過原廠人工窗口。鎖定畫面輸出的值一律做 HTML 跳脫 |
| **狀態** | 🚫 裁定不修 |

## M17-5 ⚪ 公告與部門的關聯表沒有設定客戶資料隔離 {#m17-5}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #143；OWASP：A01 存取控制失效） |
| **在哪裡** | 公告模組：「這則公告要發給哪些部門」那張關聯表 |
| **攻擊面位置** | 任何能讀到這張表的查詢路徑；但要查它得先有公告編號，而公告主表擋得住 |
| **信任邊界位置** | **客戶 ↔ 客戶（在資料庫層）**。關聯表只存公告編號與部門編號兩個欄位、沒有任何內容，不受客戶隔離保護 |
| **元件端點位置** | 資料表 `bulletin_org_units`（套件 `jedi_bulletin/infra/models/bulletin.py`）；經 `POST /api/1.0/bulletins`、`GET /api/1.0/bulletin/{uid}` 讀取 |
| **駭客怎麼打** | ① 攻擊者需要先拿到別家客戶的公告編號；② 但公告主表有隔離，查不到就拿不到編號；③ 就算查到關聯，也只是兩個數字 |
| **得手什麼** | 公告與部門的對應編號（無內容） |
| **修正的做法** | **裁定不補**：多做一層會變成兩套規則各自維護、反而更糟；靠公告主表擋，並在模組頁留一句提醒給下一個人 |
| **狀態** | 🚫 裁定不修 |

## M11-28 ⚪ 範本的「設備／資訊系統對照」清單任何登入帳號都讀得到 {#m11-28}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #178；OWASP：A01 存取控制失效） |
| **在哪裡** | 合規文件核心：範本裡連到真實資產主檔的設備與資訊系統對照 |
| **攻擊面位置** | 任何登入帳號，拿一個範本編號 |
| **信任邊界位置** | **使用者 ↔ 範本服務**。比 [M11-4](#m11-4) 多一層：帶上資產主檔裡真實紀錄的名稱與內部編號 |
| **元件端點位置** | `GET /api/1.0/module-frame/{uid}/ssp-resources`（主專案 `api/module_frame/routes/module_frame_ssp_resources_route.py` 的 `ModuleFrameSspResourcesResource`），回傳 `{devices, info_systems}` |
| **駭客怎麼打** | ① 任一登入帳號；② 打某範本的設備／資訊系統對照；③ 拿到真實設備與資訊系統的名稱與內部編號，配合資產 API 繼續查 |
| **得手什麼** | 真實資產主檔的名稱與內部編號 |
| **修正的做法** | 補 `require_capability("module-frame.read")`，與 [M11-4](#m11-4) 同一張卡 |
| **狀態** | ✅ 已修（FR-114 CM-2186，BE commit `aafaa070d`，1.21.0 出貨） |

## M11-31 ⚪ 從框架版本下載空白範本，附了全公司 email、設備與 IP {#m11-31}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #179；OWASP：A01 存取控制失效） |
| **在哪裡** | 合規文件核心：系統安全計畫 Excel 範本下載（範本頁、專案頁、框架版本空白表三支共用同一批下拉資料） |
| **攻擊面位置** | 只被授權「看範本」的人 |
| **信任邊界位置** | **使用者 ↔ 範本產生器**。範本的隱藏工作表帶著全客戶人員（暱稱＋帳號＋email）與設備（名稱＋IP）清單，供下拉選單用；資料範圍沒有依呼叫者權限縮減。不跨客戶 |
| **元件端點位置** | 主專案 `api/module_frame/routes/ssp_import_template_route.py`：`GET /ssp-import-template`（`SspImportTemplateByFrameworkVersionResource`）、`GET /module-frame/{uid}/ssp-import-template`、`GET /ssp/{ssp_uid}/excel-template` → `app/module_frame/service/ssp_import_template_app_service.py`；縮減在 `app/module_frame/service/template_lookup_scope.py` |
| **駭客怎麼打** | ① 只能看範本的帳號；② 下載框架版本的空白範本；③ 打開隱藏工作表；④ 拿到全公司的使用者 email、全部設備名稱與 IP、全部資訊系統清單 |
| **得手什麼** | 全公司的人員 email 與資產 IP 清冊 |
| **修正的做法** | 決策者選方案 A：① 下拉的顯示文字保留（填範本必要），只依權限拿掉自動帶入的敏感欄位——沒有「看使用者」權限不給人員 email、沒有「看設備」權限不給設備 IP；② 新檔 `scope_bundle_lookups_to_viewer()` 就地縮減下拉資料，三支下載各加一行呼叫（原服務檔已超過體積上限，只加呼叫不加邏輯）；③ 設備標籤以名稱精確對回，不用正規式剝括號（設備名本身可能以括號結尾） |
| **狀態** | ✅ 已修，1.21.0 出貨（CM-2188，BE commit `7d62545c3`） |

## M06-20 ⚪ 匯出任務 Excel 時，檢查的專案和實際匯出的專案可以是兩個 {#m06-20}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #182；OWASP：A01 存取控制失效） |
| **在哪裡** | 稽核流程模組：規劃頁「匯出任務 Excel」 |
| **攻擊面位置** | 專案 A 的一般成員，把網址裡的計畫編號換成專案 B 的 |
| **信任邊界位置** | **使用者 ↔ 任務匯出服務**。系統確認了「你是 A 的成員」，但沒確認「網址上的計畫屬於 A」；守門服務沒注入時還會整段跳過 |
| **元件端點位置** | `GET /api/1.0/project/{project_uid}/ap/{ap_uid}/jobs/export`（套件 `jedi_task_platform/task/api/routes/job_import_route.py` 的 `JobExportRoute`）→ 主專案 `app/flow_control/service/job_import_service.py`；解析鏈 `infra/readmodel/tasks/job_export_query.py` |
| **駭客怎麼打** | ① 專案 A 的成員；② 匯出網址的專案填 A、計畫填 B 的；③ 成員檢查用 A 過關；④ 拿到 B 的整張任務表（任務編號、指派人帳號、部門、設備） |
| **得手什麼** | 同客戶內別專案的任務規劃表 |
| **修正的做法** | ① 新增 `_assert_plan_in_project`，匯出、驗證、重新驗證、確認四個入口都呼叫：專案查無 404、成員檢查、計畫解出的專案必須等於網址專案（計畫底下沒任務也照核）、計畫下每張任務批次核對歸屬；② 不符一律回「查無」不回「拒絕」；③ 守門服務或歸屬查詢漏注入時一律拒絕（原本會整段跳過） |
| **狀態** | ✅ 已修，1.21.0 出貨（CM-2191，BE commit `33aab8cba`） |

## M06-21 ⚪ 匯入任務的「驗證」「重新驗證」兩步完全不檢查身分 {#m06-21}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #183；OWASP：A01 存取控制失效） |
| **在哪裡** | 稽核流程模組：規劃頁匯入任務 Excel 的驗證與重新驗證 |
| **攻擊面位置** | 同一家客戶任何登入帳號 |
| **信任邊界位置** | **使用者 ↔ 任務匯入服務**。驗證兩步零守門；回傳的錯誤訊息可以拿來試探「某任務屬不屬於某計畫」「某帳號、部門、設備名稱存不存在」。不會改資料 |
| **元件端點位置** | 套件 `jedi_task_platform/task/api/routing.py`：`POST /project/{project_uid}/ap/{ap_uid}/jobs/import`（`JobImportRoute`，上傳並驗證）、`POST …/jobs/import/validate`（`JobRevalidateRoute`）→ 主專案 `app/flow_control/service/job_import_service.py` |
| **駭客怎麼打** | ① 同客戶任一帳號；② 上傳一份自己編的任務 Excel，裡面放想查的任務編號、帳號、部門、設備名稱；③ 看驗證錯誤訊息哪些「找不到」、哪些「不屬於」；④ 逐一試出別專案的任務歸屬與公司內的帳號、部門、設備名單 |
| **得手什麼** | 任務歸屬與帳號、部門、設備名稱是否存在的資訊 |
| **修正的做法** | 與 [M06-20](#m06-20) 同一顆 commit：兩步開頭補「計畫屬於網址專案＋你是成員」，不符回 404 |
| **狀態** | ✅ 已修，1.21.0 出貨（CM-2191，BE commit `33aab8cba`） |

## M06-22 ⚪ 「查卡住狀態」誰都能查別的專案 {#m06-22}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #184；OWASP：A01 存取控制失效） |
| **在哪裡** | 稽核流程模組：任務卡住時查「在等哪幾條分支」 |
| **攻擊面位置** | 同一家客戶任何登入帳號，拿一個任務編號、網址專案隨便填 |
| **信任邊界位置** | **使用者 ↔ 任務服務**。說明文字寫「任一專案參與者可看」，程式沒做；任務歸屬檢查與成員檢查都缺 |
| **元件端點位置** | `GET /api/1.0/project/{project_uid}/job/{job_uid}/blocked-status`（主專案 `api/flow_control/routes/job_force_start_route.py` 的 `JobBlockedStatusResource`）→ `app/flow_engine/service/job_force_start_service.py` 的 `inspect()` |
| **駭客怎麼打** | ① 同客戶任一帳號；② 打別人任務的卡住狀態；③ 讀到任務名稱、流程節點、狀態，以及**每條分支的任務編號**——正好是強制開始別人任務的材料 |
| **得手什麼** | 別專案任務的流程狀態與分支任務編號 |
| **修正的做法** | ① 查到任務後比對任務真正所屬專案與網址專案，不符或查無回 404（改走 CM-2113 的唯一歸屬判斷）；② `inspect` 開頭新增 `_require_participant`，非成員 403 |
| **狀態** | ✅ 已修，1.21.0 出貨（歸屬 CM-2113 BE `f52a53f1e`；成員檢查 CM-2148 BE `a3fe63b03`） |

## M12-4 ⚪ 同一家客戶任何帳號都看得到任何專案的「任務設定樹」 {#m12-4}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #187；OWASP：A01 存取控制失效） |
| **在哪裡** | 稽核輪次管理模組：任務設定樹（控制群組、控制項、評估目標，以及每個目標底下的任務數與被指派人姓名） |
| **攻擊面位置** | 同客戶任何登入帳號；網址上的專案編號收了沒用，後面那個編號程式一次猜三種（專案、輪次、稽核計畫），手上有任何一種都能打。前端已經沒有畫面在用 |
| **信任邊界位置** | **使用者 ↔ 稽核輪次服務**。只驗登入，不驗專案成員 |
| **元件端點位置** | 修正前：`GET /api/1.0/project/{project_uid}/ap/{ap_uid}/task-setup/tree`（套件 `jedi_compliance_audit/api/routes/task_setup_route.py`）→ `app/service/task_setup_service.py` |
| **駭客怎麼打** | ① 同客戶任一帳號；② 拿任一種編號打任務設定樹；③ 看到別專案的控制項結構、任務數與被指派人姓名 |
| **得手什麼** | 別專案的稽核範圍與人力配置 |
| **修正的做法** | 拆除了什麼：決策者裁「拆網址不補守門」——前端早已零呼叫，唯一在打的 e2e 已改走專案詳情。套件拿掉路由與凍結清單、刪除只被這條鏈用的路由、序列化、服務、領域服務、介面、實體；資料存取實作從 397 行剩 72 行（只留匯出還在用的兩個工具）；外掛契約少兩欄，主專案組裝同步拿掉（不拿掉宿主起不來） |
| **狀態** | 🗑️ 已拆除，1.21.0 出貨（CM-2176，套件 commit `742ef49e`、BE `553381efe`、前端 `fc478d0`）。驗證：網址回 404，任務匯出 Excel 照常 |

## M12-5 ⚪ 非成員知道專案編號就問得出「目前的系統安全計畫」編號與狀態 {#m12-5}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #188；OWASP：A01 存取控制失效） |
| **在哪裡** | 稽核輪次管理模組：查專案「目前的系統安全計畫」 |
| **攻擊面位置** | 同客戶任何登入帳號；前端沒有任何地方在用 |
| **信任邊界位置** | **使用者 ↔ 稽核輪次服務**。只驗登入；傷害小——只洩漏一個隨機編號和狀態，拿到編號後其他相關網址各自有檢查 |
| **元件端點位置** | 修正前：`GET /api/1.0/project/{uid}/current-ssp-uid`（套件 `jedi_compliance_audit/api/routes/project_current_ssp_route.py`）→ `app/service/project_current_ssp_service.py` |
| **駭客怎麼打** | ① 同客戶任一帳號；② 拿別人專案的編號打這支；③ 拿到那個專案目前系統安全計畫的編號與狀態，作為打其他 API 的入場券 |
| **得手什麼** | 別專案系統安全計畫的編號與狀態 |
| **修正的做法** | 拆除了什麼：與 [M12-4](#m12-4) 同批拆網址，連同路由、服務與外掛契約欄位；前端拿掉沒人呼叫的常數與方法 |
| **狀態** | 🗑️ 已拆除，1.21.0 出貨（CM-2176，套件 commit `742ef49e`、BE `553381efe`、前端 `fc478d0`） |

## M11-32 ⚪ Word 打不開時錯誤訊息帶出伺服器暫存檔路徑 {#m11-32}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #194；OWASP：A10 例外狀況處理不當） |
| **在哪裡** | 合規文件核心：在專案裡匯入系統安全計畫（SSP）的 Word 檔 |
| **攻擊面位置** | 已登入、能匯入 SSP 的使用者。設計上就開放上傳任意檔案 |
| **信任邊界位置** | **伺服器內部 ↔ 使用者畫面**，與 M11-22 同一個病：解析 Word 的程式把原始錯誤字串直接當成給使用者看的訊息 |
| **元件端點位置** | 主專案 `api/oscal/__init__.py`：`POST /ssp-docx-imports/parse`（`SspDocxImportParseRoute`）→ `app/oscal/service/ssp_docx_import_app_service.py` → `domain/oscal/parser/docx_parser_core.py` 的 `ParseException`；讀回：`GET /ssp-docx-import/{parse_uid}`。同形狀的還有 Excel 匯入 `app/oscal/service/ssp_excel_import_app_service.py` |
| **駭客怎麼打** | ① 能匯入 SSP 的使用者；② 上傳一個副檔名是 .docx、內容卻不是 Word 的檔案；③ 解析失敗，畫面顯示類似「找不到 /var/folders/…/tmpxxx.docx」的訊息；④ 伺服器暫存目錄的完整路徑就外洩了 |
| **得手什麼** | 伺服器暫存目錄的路徑（洩漏範圍只有這個） |
| **修正的做法** | 與 M11-22 同一張卡：① `ParseException` 的訊息只放錯誤碼對應的白話訊息，不再夾帶原始錯誤，原始錯誤用 `raise … from e` 掛在後面，日誌照樣看得到完整鏈；② 新增 `public_parse_error()`：業務上的錯誤（找不到控制項編號、框架不符）照原樣給使用者，其他一律回 `GRC_DOCX_PARSE_FAILED`；③ Excel 匯入的「非預期錯誤」分支同樣改回 `GRC_EXCEL_INVALID_FILE` 白話訊息 |
| **狀態** | ✅ 已修（FR-114 CM-2183，commit `c33f7e828`，1.21.0 出貨）。驗證：DEV 上傳非 zip 的 .docx，畫面只見「docx 結構損壞，解析失敗」 |

## M11-30 ⚪ 建資源庫時不看框架版本發佈了沒，草稿也能被複製 {#m11-30}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #195；OWASP：A01 存取控制失效） |
| **在哪裡** | 合規文件核心：用框架版本建立合規資源庫（手動新增、Excel 建新庫、Word 建新庫三條路） |
| **攻擊面位置** | 有「建資源庫」權限的帳號，手上有一個草稿版本的編號 |
| **信任邊界位置** | **平台管理員的草稿 ↔ 客戶的資源庫**。平台端還在編修的條文，不該被客戶複製出去；建庫前沒有檢查版本的發佈狀態。不跨客戶、不改公版 |
| **元件端點位置** | `POST /api/1.0/oscal/resource-libraries`（主專案 `api/oscal/routes/resource_library_route.py` 的 `ResourceLibraryCreateRoute`）、`POST /ssp-excel-imports/parse`、`POST /ssp-docx-imports/parse` → `app/oscal/service/resource_library_app_service.py` 的 `create_resource_library()` |
| **駭客怎麼打** | ① 有建資源庫權限的帳號；② 拿一個平台管理員還沒公開的草稿版本編號；③ 用它建資源庫、開專案；④ 拿到未公開的控制項內容，而且之後原廠改草稿，客戶那份不會跟著改 |
| **得手什麼** | 平台尚未公開的控制項內容 |
| **修正的做法** | ① `create_resource_library` 在取得版本、確認有目錄之後、複製之前，補「發佈狀態必須是已發佈」，否則 412 `GRC_FRAMEWORK_VERSION_NOT_PUBLISHED`；② 三條入口都經過這支，一處蓋三處；③ 前端建新範本的框架版本選單同步只列已發佈 |
| **狀態** | ✅ 已修（FR-114 CM-2179，BE commit `94275d72f`，1.21.0 出貨）。驗證：草稿版本三條入口都回 412，資料筆數前後不變 |

## M12-7 ⚪ 兩張「解析工作單」資料表照抄了五月就判定壞掉的隔離寫法 {#m12-7}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #197；OWASP：A01 存取控制失效） |
| **在哪裡** | 稽核輪次管理模組：稽核計畫 Word 匯入、稽核結果 Excel 匯入的解析工作單 |
| **攻擊面位置** | 子公司帳號；但程式那一層四步都有檢查公司，今天沒有直接可打的路——是「以為有第二道防線、其實沒有」 |
| **信任邊界位置** | **母公司 ↔ 子公司（在資料庫層）**。規則拿客戶編號去比使用者編號，再用純文字包含比對路徑（子公司路徑 `/1/102/152/` 會命中母公司 102），也沒有平台管理員分支。系統安全計畫那張五月就修對了，七月建這兩張時照抄了修正前的寫法 |
| **元件端點位置** | 資料表 `oscal.ap_docx_parse_jobs`、`oscal.ar_xlsx_parse_jobs`；入口 `POST /api/1.0/ap/{ap_uid}/docx-imports/parse`、`GET /api/1.0/ap-docx-import/{parse_uid}`、`POST /api/1.0/audit-round/{round_uid}/ar-imports/parse`、`GET /api/1.0/ar-import/{parse_uid}` |
| **駭客怎麼打** | ①（假設程式層某處漏檢）子公司帳號查解析工作單；② 資料庫規則把母公司的工作單算成看得到；③ 讀到母公司稽核計畫與稽核結果的解析內容（開發環境模擬：子公司看到母公司 13＋4 筆） |
| **得手什麼** | 母公司的稽核計畫與稽核結果解析內容 |
| **修正的做法** | ① 新 migration 兩表各刪掉舊規則，照標準寫法建查詢／新增（含寫入檢查）／修改／刪除四條，確認隔離開啟；② 舊 migration 檔不改；③ 套用後子公司與別公司都看到 0 筆、自己與平台管理員照常；④ 出貨基線待重產（決策者裁示範圍） |
| **狀態** | ✅ 已修（FR-114 CM-2195，BE commit `56dd4cdc8`，1.21.0 出貨） |

## M24-14 ⚪ 依名稱找雲端資料夾時沒處理反斜線 {#m24-14}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #221；OWASP：A05 注入攻擊） |
| **在哪裡** | 主系統的雲端硬碟整合：專案啟動或重建資料夾時，先在雲端找同名資料夾、找到就沿用 |
| **攻擊面位置** | 資料夾名稱來自專案、稽核計畫、控制項的顯示名稱，能編輯這些名稱的使用者可影響它；觸發建立資料夾需要管理員或專案啟動 |
| **信任邊界位置** | **使用者命名 ↔ Google 雲端硬碟的搜尋語法**。名稱被拼進 `name='…'` 搜尋條件，只跳脫了單引號、沒先跳脫反斜線，反斜線能把跳脫用的那一個再抵銷掉 |
| **元件端點位置** | 主專案 `api/cloud_integration/__init__.py`：`POST /integrations/google-drive/projects/{project_uid}/init-folders`（`DriveSyncProjectInitFoldersRoute`）→ `app/cloud_integration/service/handlers/init_project_folders_handler.py` 的 `_adopt_or_create_folder()` → `infra/cloud_integration/google_drive/google_drive_api_client.py` 的 `find_folders_by_name()` |
| **駭客怎麼打** | ① 能編輯專案或稽核計畫名稱的人，把名稱改成結尾帶反斜線再接引號與額外條件；② 管理員初始化或重建該專案的雲端資料夾；③ 搜尋條件被改寫，系統找到並「沿用」一個不該沿用的資料夾。**尚未實際打過** |
| **得手什麼** | 系統把證據寫進或讀自不該碰的雲端資料夾 |
| **修正的做法** | ① 先把反斜線換成兩個反斜線，再跳脫單引號（順序不能反，反了會把剛加上的跳脫再拆開）；② 驗證：`a\'b`、`x\` 兩種輸入跳脫後都還是一個完整字串 |
| **狀態** | ✅ 已修，1.21.0 出貨（8-C，CM-2213，主專案 commit `a5804a990`／`bde8f9d16`） |

## M10-22 ⚪ 證據蒐集執行進度只驗登入，換專案編號就看到別人的完成數 {#m10-22}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #225；OWASP：A01 存取控制失效） |
| **在哪裡** | 任務與成員管理模組：專案「任務執行進度」（證據蒐集完成幾項）。*（模組頁無逐條表，本條依總表描述與修正 commit 撰寫）* |
| **攻擊面位置** | 同公司任何登入帳號，換網址上的專案編號 |
| **信任邊界位置** | **使用者 ↔ 任務平台服務**。只驗登入，不驗專案參與者 |
| **元件端點位置** | `GET /api/1.0/project/{project_uid}/task-execution/progress`（套件 `jedi_task_platform/task/api/routes/task_execution_route.py` 的 `TaskExecutionProgressRoute`）→ `task/app/service/task_execution_service.py` 的 `get_prep_job_progress()` |
| **駭客怎麼打** | ① 同公司任一帳號；② 換成別人專案的編號打進度；③ 看到那個專案的證據蒐集完成數 |
| **得手什麼** | 別專案的稽核準備進度 |
| **修正的做法** | ① `get_prep_job_progress` 改收使用者，先解專案（查無 404），再走既有的專案角色檢查，四種角色任一即可（唯讀）；② 不符拋 `GRC_NOT_PROJECT_PARTICIPANT`（與主專案同名同值，前端已有文案）；③ 套件不能 import 宿主的角色定義，角色字串以常數寫在服務內 |
| **狀態** | ✅ 已修，1.21.0 出貨（8-A，CM-2211，套件 commit `cdb83526`）。驗證：成員 200、非成員 403 |

## M24-18 ⚪ 預蒐診斷資料時照索引檔的路徑讀檔，沒限制在暫存目錄內 {#m24-18}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #228；OWASP：A01 存取控制失效） |
| **在哪裡** | 主系統：出貨主機的維運指令先在主機端預蒐診斷資料（設定檔、容器紀錄），放進交換目錄，再由容器內的程式依索引檔讀進診斷包 |
| **攻擊面位置** | 能動到交換目錄或索引檔的人（主機上與容器同一使用者身分的帳號） |
| **信任邊界位置** | **主機的交換目錄 ↔ 要送出客戶機房的診斷包**。索引檔裡的檔名與服務名應該只收白名單、且確認還在交換目錄內；當時照單全收 |
| **元件端點位置** | `infra/support/collector/staged_collector.py` 的 `_read_staged()`；主機端 `scripts/installer/guidant` 的 `diag` 段；入口為維運指令 `guidant diag` 與 `POST /api/1.0/support/diagnostic-bundle` |
| **駭客怎麼打** | ① 能寫交換目錄的人改索引檔，把檔名寫成 `../secret-outside.txt` 或 `/etc/hosts`，或放一個指向外面的捷徑；② 管理員產生診斷包；③ 程式照索引讀檔，把交換目錄以外的檔案一起打包；④ 診斷包送出客戶機房 |
| **得手什麼** | 主機上交換目錄以外的檔案（隨診斷包外流） |
| **修正的做法** | ① 讀檔前把真實路徑解開，必須直接落在交換目錄內，`../`、絕對路徑、指向外面的捷徑一律拒讀並記警告；② 服務名只收已知服務清單（與主機版同一份，直接 import 不另抄）；③ 主機端交換目錄改隨機名、權限 700，建立前確認上層不是捷徑；④ 首腦退回再補：進交換目錄後一律用相對路徑寫檔，防目錄被改名換成捷徑 |
| **狀態** | ✅ 已修，1.21.0 出貨（8-D，CM-2214，BE commit `d39181f3b`／`046553756`） |

## M24-13 ⚪ 診斷包的連線紀錄網址欄沒遮 {#m24-13}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #229；OWASP：A09 日誌與告警失效） |
| **在哪裡** | 主系統：系統診斷包會從資料庫撈一段操作日誌（連線紀錄），只遮清單上的欄位 |
| **攻擊面位置** | 拿到診斷包的人。產品設計上通行證不走網址，開發環境只找到 1 筆測試資料符合，所以評低 |
| **信任邊界位置** | **客戶機房 ↔ 原廠**。所有可能夾帶憑證的欄位都該在遮罩清單上；當時清單有請求、回覆、參數、訊息，唯獨漏了網址 |
| **元件端點位置** | `domain/support/entity/diag_bundle.py:MASKED_API_LOG_COLUMNS`；由 `infra/support/diag_db_reader.py:_rows_to_csv` 與 `domain/support/service/diag_slice_domain_service.py` 使用；入口 `POST /api/1.0/support/diagnostic-bundle` |
| **駭客怎麼打** | ① 某個整合把通行證放在網址參數上呼叫系統；② 這筆連線紀錄的網址欄存著 `?token=…`；③ 產診斷包時網址欄原樣匯出；④ 經手診斷包的人拿到通行證 |
| **得手什麼** | 夾在網址裡的通行證 |
| **修正的做法** | ① `MASKED_API_LOG_COLUMNS` 加入 `url`；② 資料庫紀錄改走與日誌相同的遮罩（`mask_text`），不再用只認 JSON 形狀的舊遮罩；③ 打包前再統一過一道 |
| **狀態** | ✅ 已修，1.21.0 出貨（8-B，CM-2212，BE commit `c8534bfcb`／`8f5755f1d`） |

## M24-9 ⚪ 即時通知有一個頻道不驗身分，任何人都能加入任意房間 {#m24-9}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #230；OWASP：A01 存取控制失效） |
| **在哪裡** | 主系統：即時通知頻道（原本給流程討論推播用） |
| **攻擊面位置** | 任何連得到即時通訊服務的人，**不必帳號**。今天沒有影響：伺服器不往那裡送資料、前端用到它的元件也沒接在畫面上 |
| **信任邊界位置** | **網路 ↔ 即時通知頻道**。頻道不驗身分、不檢查房間歸屬；元件哪天接回畫面，就變成不必帳號就能偷聽討論串 |
| **元件端點位置** | 修正前：即時通訊頻道 `/socket/notification`（`app/notification/handler/notification_socketio_handler.py`，註冊在 `config/socketio_namespaces.py`）；流程留言 API `GET`／`PUT /flow-engine/process/comments/{id}`；前端元件 `ProcessDiscussBox` |
| **駭客怎麼打** | ① 不需帳號，連上即時通知頻道；② 加入任意房間；③（若元件接回畫面）收到別人專案的流程討論推播，也能往房間廣播任意內容 |
| **得手什麼** | （接回之後）別人專案的即時討論內容 |
| **修正的做法** | 拆除了什麼：查出整條流程討論前後端都斷了（前端元件沒有頁面引用、後端留言通知送往一個沒註冊的頻道），決策者 10-01 裁定沒在用就拔——刪除頻道處理程式與註冊（即時通訊只剩填問卷那一個頻道）、流程留言 API 與其服務、錯誤碼、前端討論元件；問卷討論、任務留言、問卷共編不動 |
| **狀態** | ✅ 已修（拔除，CM-2373，BE commit `99beceb26`、前端 `2358d96`） |

## M11-40 ⚪ 人員對帳收了公司編號卻沒用，會拿所有客戶的人員來比對 {#m11-40}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #231；OWASP：A01 存取控制失效） |
| **在哪裡** | 合規文件核心：匯入系統安全計畫（Word／Excel）時的「人員對帳」——把文件裡的人名、信箱對到系統帳號 |
| **攻擊面位置** | 在平台管理員身分下執行匯入時；比對結果只寫進附註欄、不影響任何權限 |
| **信任邊界位置** | **客戶 ↔ 客戶**。四處查詢都收了公司編號卻沒拿來篩選；平台管理員視角下資料庫不擋，於是跨所有客戶比對信箱與姓名。同一套功能的「組織對帳」就有正確用上公司編號 |
| **元件端點位置** | `domain/oscal/service/reconciliation/person_reconciler.py` 的四個 `_try_*` 查詢；由 `POST /api/1.0/ssp-docx-imports/parse`、`POST /api/1.0/ssp-excel-imports/parse` 的匯入流程呼叫 |
| **駭客怎麼打** | ① 平台管理員代某客戶匯入系統安全計畫；② 文件裡的人名或信箱剛好與別家客戶的帳號相符；③ 對帳結果把別家客戶的帳號資訊寫進這份計畫的附註 |
| **得手什麼** | 別家客戶人員的帳號對應資訊（寫進附註） |
| **修正的做法** | ① 新增 `_scoped()`，四處查詢都加上公司條件，寫法對齊組織對帳；② 公司編號為空時維持原行為；③ 兩個呼叫端（Word／Excel 匯入）確認都有傳公司編號 |
| **狀態** | ✅ 已修，1.21.0 出貨（8-A，CM-2211，BE commit `fe928859f`） |
