---
title: E 權限提升
eyebrow: Guidant AI 資安檢視總報告 · STRIDE 威脅分類
h1: E 權限提升（Elevation of Privilege）
lede: 命中這一類的 **44 件**，還原成模組頁的 **53 條原始條目**逐條展開。每一條用白話寫出：在哪裡、攻擊面、信任邊界、元件端點、駭客怎麼打、得手什麼、修正的做法。
chips:
  - { text: "🔴 2", kind: crit }
  - { text: "🟠 18", kind: warn }
  - { text: "🟡 19", kind: accent }
  - { text: "⚪ 14", kind: plain }
  - { text: "已修 45／拆除 6／不修 2", kind: ok }
---

## 這一類是什麼

**拿到超出自己角色的能力**。STRIDE 六類裡它破壞的是「授權」——依微軟的定義：沒有權限的人取得較高的權限，足以危害甚至摧毀整個系統。判定時問的是：**攻擊者打下這一條之後，能不能拿到超出自己角色的能力？** 典型樣子是一般員工把自己改成管理員、子公司管理員改到全站設定、在伺服器上執行自己的程式碼。

對稽核產品來說，權限就是信任的來源：誰能結案、誰能改範本、誰看得到哪家客戶，全靠角色與層級。這次的 53 條在本專案長成六種形狀：

- **自助入口沒守，自己把自己升上去**（M13-4、M13-3、M13-1、M03-2、M10-6、M11-10、M11-13、M24-12、M15-7）：個人資料頁、綁定外部帳號、建立範本、AI 生成這類人人打得到的入口，只問「你有沒有登入」或「你有沒有某一顆權限」，沒問「你是不是本人」「你的角色有沒有這一項」。最低門檻的升權路都在這裡。
- **層級與歸屬算錯，管到不該管的人**（M22-1、M22-4、M18-8、M04-2、M20-4、M03-14、M06-7）：「誰算總部」「子單位能不能改上層」「沒有身分時算誰」「唯讀角色能不能結案」——判斷層級的那道規則寫寬了、寫反了，或把「查不到身分」當成最高權限。
- **資料變成程式，在別人的電腦或我們的伺服器上執行**（M03-1、M02-4、M03-13、M24-1、M09-2、M21-1、M11-19、M11-20、M11-21、M06-26、M15-1、M24-7、M03-5）：上傳的規則包、網頁檔、網址參數、Excel 儲存格、對 AI 打的字、共用目錄裡的捷徑——系統把使用者給的內容交給外部程式、瀏覽器、試算表或 AI 之前沒標成「這是資料」，於是攻擊者借用了伺服器、管理員或主機最高權限帳號的身分。
- **祕密外流，拿到鑰匙就是管理員**（M01-1、M23-1、M16-7、M17-8、M16-8、M17-11、M17-10、M15-6、M16-6、M17-6、M17-7）：資料庫管理密碼、系統管理員密碼、登入憑證簽章金鑰、套件倉庫帳密寫進了版控，或每套安裝共用同一組；代理程式控制面不驗身分也屬這一類——鑰匙本身就是權限，拿到的人不需要再打任何洞。
- **繞過原廠的商務授權**（M18-1、M18-12、M18-11、M18-13、M05-14）：停權、過期唯讀、沒買的模組——客戶自己能換的授權檔、改得到的設定值、排程沒跑的空窗、選單反灰但網址沒擋，都讓客戶用到沒付錢的能力。受損的是原廠收入，不是客戶資料。
- **今天打不到、接上去就是一組沒守的入口**（M10-7、M10-8、M10-18、M06-14、M06-16、M04-14、M10-12、M04-13）：沒人用的寫入功能、找不到紀錄就放行的守門、沒人讀的開關、靠「目前沒人帶外部輸入」撐著的拼接寫法。這一類多半選擇拆除，不修的兩條寫明了理由。

> **STRIDE 與 OWASP 的關係**：OWASP 講「程式哪裡寫錯」，STRIDE 講「攻擊者能拿它做什麼」。同一條問題通常兩邊都命中，所以這一頁的條目會與 [OWASP 各頁](../OWASP-Top-10/A01-broken-access-control.html)重複出現，內容相同——兩邊各自完整，讀者從哪一邊進來都看得完。這一類 44 件裡有 32 件以權限提升為主分類，其餘 12 件主要歸在「冒充身分」「資料外洩」「竄改資料」，權限提升是它們的附帶後果；這些條目的嚴重度欄會註明主分類。

> **為什麼是 53 條不是 44 件**：[總結](../SUMMARY.html)問題總表有 8 件是同一個洞被兩三個模組各撈到一次，總表併成一件：#12（M23-1＋M16-7＋M17-8）、#13（M02-4＋M03-13）、#20（M09-2＋M21-1）、#37（M03-14＋M06-7）、#72（M16-8＋M17-11）、#73（M17-10＋M15-6）、#118（M06-14＋M06-16）、#122（M16-6＋M17-6）。本頁拆回模組頁原始條目，每條都列。標題括號內是總表編號，可回去對照。

> **本頁與總表、模組頁不一致的地方**（都以程式碼與 commit 為準寫在下方，總表與模組頁待統籌決定是否更正）：① **M11-13** 總表狀態欄把 CM-2179（commit `94275d72f`）列為修正，但那個 commit 修的是同模組第 30 條「草稿版本也能拿去建庫」，它自己寫明第 65 項的入口權限是 CM-2040（commit `60015232f`）修的，本卡只確認；② **M05-14** 總表狀態欄只列「填答那半」與「AI 儀表板查問卷」兩個入口，模組頁點名的第三個入口「規劃頁把問卷掛上任務」另由主專案 commit `02bc1179c`（CM-2194 入口三）修掉，總表沒寫；③ **M16-7、M17-8、M16-8、M17-11** 模組頁寫「密碼已換發」，但 CM-2048、CM-2049 兩個 commit 都註明資料庫與系統管理員密碼「本輪不換發、排正式上版前統一處理」；④ **M04-2** 總表狀態欄沒有卡號，修正 commit 由本頁回查補上（FR-094.1 三張卡）；⑤ **M03-5** 總表寫已修，但「代理程式端實際比對指紋」裁定另開小卡，本次只把指紋記進派工資料，未見對應 commit。

## 條目清單

| # | 條目 | 嚴重度 | 出自 | 狀態 |
|---:|---|---|---|---|
| 1 | [M01-1 代理程式五條通訊管道全部不檢查對方是誰](#m01-1) | 🔴 最嚴重 | [遠端代理程式](../M01-remote-agent.html) | ✅ 已修 |
| 2 | [M13-1 忘記密碼把重設鑰匙直接回傳給任何人](#m13-1) | 🔴 最嚴重 | [登入與權限](../M13-iam.html) | ✅ 已修 |
| 3 | [M03-2 「測試連線」沒檢查權限，按下去就把維運帳密解密送到指定主機](#m03-2) | 🟠 高 | [弱點檢測整合](../M03-detection.html) | ✅ 已修 |
| 4 | [M23-1 資料庫管理員密碼散在 249 個檔案裡](#m23-1) | 🟠 高 | [意見回饋](../M23-issue.html) | ✅ 已修 |
| 5 | [M16-7 產生文件的腳本寫死展示環境資料庫連線與密碼](#m16-7) | 🟠 高 | [寄信與通知](../M16-notification.html) | ✅ 已修 |
| 6 | [M17-8 可繞過客戶隔離的資料庫管理帳號密碼寫在文件裡](#m17-8) | 🟠 高 | [公告](../M17-bulletin.html) | ✅ 已修 |
| 7 | [M02-4 上傳的網頁檔，別人點預覽時被當成程式執行](#m02-4) | 🟠 高 | [檔案上傳下載](../M02-file-upload.html) | ✅ 已修 |
| 8 | [M03-13 掃描報告「檔名叫什麼就存什麼」，走同一條預覽路徑](#m03-13) | 🟠 高 | [弱點檢測整合](../M03-detection.html) | ✅ 已修 |
| 9 | [M03-1 客戶上傳的檢測規則包會被外部工具當成程式碼執行](#m03-1) | 🟠 高 | [弱點檢測整合](../M03-detection.html) | ✅ 已修 |
| 10 | [M09-2 不必登入就能在操作日誌匯出檔裡種公式](#m09-2) | 🟠 高 | [系統日誌](../M09-log.html) | ✅ 已修 |
| 11 | [M21-1 意見回饋匯出檔可以被種公式](#m21-1) | 🟠 高 | [意見回饋重新檢視](../M21-issue-rescan.html) | ✅ 已修 |
| 12 | [M15-1 打字就能誘導 AI 儀表板選中任何一支查詢](#m15-1) | 🟠 高 | [AI 儀表板](../M15-ai-dashboard.html) | ✅ 已修 |
| 13 | [M22-1 子公司管理員能改全公司的登入規則與帳號目錄](#m22-1) | 🟠 高 | [系統設定](../M22-system-core.html) | ✅ 已修 |
| 14 | [M18-8 子單位帳號改得動、刪得掉母公司的授權與停權紀錄](#m18-8) | 🟠 高 | [授權管理](../M18-license.html) | ✅ 已修 |
| 15 | [M04-2 沒有登入身分時，客戶資料隔離整個關掉](#m04-2) | 🟠 高 | [共用基礎](../M04-common.html) | ✅ 已修 |
| 16 | [M13-3 綁定外部帳號不確認是本人](#m13-3) | 🟠 高 | [登入與權限](../M13-iam.html) | ✅ 已修 |
| 17 | [M13-4 存個人資料時順手把自己改成管理員](#m13-4) | 🟠 高 | [登入與權限](../M13-iam.html) | ✅ 已修 |
| 18 | [M18-1 被停權的客戶自己上傳舊授權檔就能復權](#m18-1) | 🟠 高 | [授權管理](../M18-license.html) | ✅ 已修 |
| 19 | [M24-1 雲端硬碟授權回呼頁，點一個連結就被偷走登入身分](#m24-1) | 🟠 高 | [主系統自己的程式](../M24-host.html) | ✅ 已修 |
| 20 | [M22-4 「誰算總部」只擋得住自家子公司，擋不住另一家客戶的總部](#m22-4) | 🟠 高 | [系統設定](../M22-system-core.html) | ✅ 已修 |
| 21 | [M03-14 唯讀角色可以對客戶機器發動掃描、刪掉執行紀錄](#m03-14) | 🟡 中 | [弱點檢測整合](../M03-detection.html) | ✅ 已修 |
| 22 | [M06-7 只能看的人可以完成或退回別人的稽核任務](#m06-7) | 🟡 中 | [稽核流程](../M06-flow-engine.html) | ✅ 已修 |
| 23 | [M10-6 指派任務時「專案填自己的、任務填別人的」照樣放行](#m10-6) | 🟡 中 | [任務與成員管理](../M10-task-platform.html) | ✅ 已修 |
| 24 | [M11-13 建立資源庫的入口只驗登入、沒有權限檢查](#m11-13) | 🟡 中 | [合規文件核心](../M11-oscal.html) | ✅ 已修 |
| 25 | [M10-7 群組層成員的增改刪因漏接線完全不檢查權限](#m10-7) | 🟡 中 | [任務與成員管理](../M10-task-platform.html) | 🗑️ 已拆除 |
| 26 | [M10-8 流程參與者的增改刪，守門寫在永遠跑不到的位置](#m10-8) | 🟡 中 | [任務與成員管理](../M10-task-platform.html) | 🗑️ 已拆除 |
| 27 | [M16-8 系統管理員密碼寫死在三支腳本裡](#m16-8) | 🟡 中 | [寄信與通知](../M16-notification.html) | ✅ 已修 |
| 28 | [M17-11 資料庫調整腳本「沒設變數就用真密碼」](#m17-11) | 🟡 中 | [公告](../M17-bulletin.html) | ✅ 已修 |
| 29 | [M17-10 套件倉庫與檔案儲存的帳密寫在交接文件裡](#m17-10) | 🟡 中 | [公告](../M17-bulletin.html) | ✅ 已修 |
| 30 | [M15-6 同一份交接文件裡的三樣帳密](#m15-6) | 🟡 中 | [AI 儀表板](../M15-ai-dashboard.html) | ✅ 已修 |
| 31 | [M03-5 網址型規則包放行不加密連線、也不記指紋](#m03-5) | 🟡 中 | [弱點檢測整合](../M03-detection.html) | ✅ 已修 |
| 32 | [M17-7 每套安裝都種下同一組最高權限帳號](#m17-7) | 🟡 中 | [公告](../M17-bulletin.html) | 🚫 不修 |
| 33 | [M20-4 兩支半夜清理程式靠「找不到身分就給最高權限」才跑得動](#m20-4) | 🟡 中 | [客戶資料隔離](../M20-tenant-isolation.html) | ✅ 已修 |
| 34 | [M11-19 控制項現況 Excel 的描述會被寫成公式](#m11-19) | 🟡 中 | [合規文件核心](../M11-oscal.html) | ✅ 已修 |
| 35 | [M11-20 計畫內容裡的公式，在別人下載範本 Excel 時執行](#m11-20) | 🟡 中 | [合規文件核心](../M11-oscal.html) | ✅ 已修 |
| 36 | [M11-21 改自己的暱稱，就在全公司每份範本裡埋公式](#m11-21) | 🟡 中 | [合規文件核心](../M11-oscal.html) | ✅ 已修 |
| 37 | [M11-10 只有建立權限的人走整批匯入就能改掉既有範本](#m11-10) | 🟡 中 | [合規文件核心](../M11-oscal.html) | ✅ 已修 |
| 38 | [M18-12 落地版的商務授權總開關，客戶改一行設定就關掉](#m18-12) | 🟡 中 | [授權管理](../M18-license.html) | ✅ 已修 |
| 39 | [M24-7 診斷指令的暫存資料夾名字猜得到、不檢查捷徑](#m24-7) | 🟡 中 | [主系統自己的程式](../M24-host.html) | ✅ 已修 |
| 40 | [M10-12 更新任務指派時撈不到既有紀錄就跳過管理者檢查](#m10-12) | ⚪ 低 | [任務與成員管理](../M10-task-platform.html) | ✅ 已修 |
| 41 | [M04-14 每次連線都設定、但全系統沒人讀的開關](#m04-14) | ⚪ 低 | [共用基礎](../M04-common.html) | 🗑️ 已拆除 |
| 42 | [M06-14 三支直接拿傳入路徑開檔讀寫的閒置程式](#m06-14) | ⚪ 低 | [稽核流程](../M06-flow-engine.html) | 🗑️ 已拆除 |
| 43 | [M06-16 接好但沒人用、完全沒有權限檢查的備用服務](#m06-16) | ⚪ 低 | [稽核流程](../M06-flow-engine.html) | 🗑️ 已拆除 |
| 44 | [M10-18 沒有對外入口、也沒有把關的任務指派功能](#m10-18) | ⚪ 低 | [任務與成員管理](../M10-task-platform.html) | 🗑️ 已拆除 |
| 45 | [M16-6 登入憑證簽章金鑰、資料庫密碼、儲存金鑰同在一個測試設定檔](#m16-6) | ⚪ 低 | [寄信與通知](../M16-notification.html) | ✅ 已修 |
| 46 | [M17-6 登入憑證簽章金鑰明文躺在對話紀錄裡](#m17-6) | ⚪ 低 | [公告](../M17-bulletin.html) | ✅ 已修 |
| 47 | [M04-13 資料庫隔離設定值用字串拼進查詢語句](#m04-13) | ⚪ 低 | [共用基礎](../M04-common.html) | 🚫 不修 |
| 48 | [M05-14 沒買問卷模組，直接打網址照樣能用「填答」那一半](#m05-14) | ⚪ 低 | [問卷](../M05-survey.html) | ✅ 已修 |
| 49 | [M18-11 授權過期轉唯讀只靠每天一次的排程](#m18-11) | ⚪ 低 | [授權管理](../M18-license.html) | ✅ 已修 |
| 50 | [M18-13 過期唯讀的兩個縫：代理程式網址整段放行、問卷即時填答不經檢查](#m18-13) | ⚪ 低 | [授權管理](../M18-license.html) | ✅ 已修 |
| 51 | [M24-12 驗證雲端硬碟應用程式憑證只查是不是平台管理員](#m24-12) | ⚪ 低 | [主系統自己的程式](../M24-host.html) | ✅ 已修 |
| 52 | [M06-26 匯出任務 Excel 時欄位被當成公式](#m06-26) | ⚪ 低 | [稽核流程](../M06-flow-engine.html) | ✅ 已修 |
| 53 | [M15-7 AI 儀表板生成只檢查有沒有買模組，不檢查角色權限](#m15-7) | ⚪ 低 | [AI 儀表板](../M15-ai-dashboard.html) | ✅ 已修 |

---

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

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🔴 最嚴重（總表 #1；OWASP：A07 身分驗證失效、A01 存取控制失效；主分類為冒充身分，另涉資料外洩、竄改資料） |
| **在哪裡** | 遠端代理程式模組：裝在客戶機房的代理程式與後端之間的五條通道——報到、領工作、確認收到、回報結果、下載證據檔 |
| **攻擊面位置** | 後端對外開放的「代理程式控制面」API。這一面設計上就**不帶使用者登入**（代理程式是機器、不是人），任何連得到後端的人都打得到，不需要帳號 |
| **信任邊界位置** | **客戶機房 ↔ 我們後端**。後端該在入口用「簽過章的身分憑證」確認對方是哪一台；當時外層的雙向憑證已退役，後端只**讀請求裡自報的機器編號就相信**。竄改的那一面在「回報結果」：結果該只收派給這一台的單，且撤銷的機器一律拒收，當時兩件都沒做 |
| **元件端點位置** | `jedi_remote_agent/api/routing.py`：`POST /api/1.0/agents/register`（報到）、`/agents/heartbeat`（領工作）、`/agents/tasks/{uid}/ack`（確認收到）、`/agents/tasks/{uid}/result`（回報結果）；處理在 `app/service/agent_task_service.py`、`agent_enrollment_service.py`、狀態寫入 `AgentTaskDomainService.update_status`；證據檔下載路由在主專案 `api/remote_agent/routes/agent_file_route.py` |
| **駭客怎麼打** | ① 在任何連得到後端的位置，不需要帳號；② 照代理程式平常的格式送「我是代理程式 X」，後端不驗證就把工作與主機帳密交出來；③ 改打回報結果通道，替 X 回報「掃描全部正常」或把真實結果蓋掉——稽核證據被偽造；④ 管理員按「撤銷」後，確認收到與回報結果兩條照樣能用，被撤銷的機器還能繼續送假資料 |
| **得手什麼** | 冒充任一家客戶的機器、拿走主機明文帳密，並偽造或抹除弱點掃描的稽核證據 |
| **修正的做法** | ① **報到時多發一張「控制面通行證」**：用後端既有的簽章私鑰簽，內含機器編號、客戶編號、用途標記與效期；② **其餘通道每個請求都要帶這張證**：新守門 `agent_pass_required`＋`AgentControlAuthService`——驗簽、只認證裡的編號、**每個請求查一次資料庫確認沒被撤銷或停用**；③ **確認收到與回報結果**：交易內再驗一次撤銷狀態，並在 `AgentTaskDomainService.update_status` 比對「這張單是不是派給這台」，不符回「找不到」；「已取消就短路」排在比對之後，不讓別台探出單子狀態；④ 路由表改成管理員／代理程式／報到三級，認不得的等級在啟動時就報錯；⑤ 報到時被撤銷的機器一律拒絕、不再洗回正常；帶舊編號重新登記要附舊私鑰簽的持有證明，且憑證申請檔的名稱必須等於本次機器指紋（驗收退回補上）；⑥ 主專案的證據檔下載改用同一張通行證，拿掉自報編號的標頭 |
| **狀態** | ✅ 已修（CM-2052，套件 commit `c4acc687`＋`9c049540`，主專案 `7a3bdf1ec`）。驗證：套件 150 測試；DEV 實跑撤銷後心跳／確認／回報／下載全部 403、確認別台的單回 404 |

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

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

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

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

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #12，與 M16-7＋M17-8 同一件；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，與 M23-1＋M17-8 同一件；模組頁原評 🟡 中；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，與 M23-1＋M16-7 同一件；模組頁原評 🟡 中；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-4 🟠 上傳的網頁檔，別人點預覽時被當成程式執行 {#m02-4}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #13，與 M03-13 同一件；OWASP：A05 注入攻擊、A06 不安全的設計；主分類為冒充身分） |
| **在哪裡** | 檔案上傳下載模組：證據、附件的「預覽」功能 |
| **攻擊面位置** | 已登入使用者可打的檔案上傳與預覽 API。上傳者只需要一個有效帳號；預覽是設計上就開放的日常功能 |
| **信任邊界位置** | **伺服器 ↔ 受害者的瀏覽器**。檔案內容是上傳者給的、不可信，伺服器把它交給瀏覽器之前應該決定「能不能在我們的網域裡打開、用什麼格式打開」；當時只看上傳者自己填的副檔名，而且允許瀏覽器直接把它畫出來，等於把不可信的內容放進了可信的網域 |
| **元件端點位置** | `jedi_file_upload/api/routing.py`：`POST /file/upload`（上傳）、`GET /file/pdf-preview/{uid}`（`UploadFilePreviewAsPDFRoute`）→ `upload_file_route.py` 的 `_send_preview()` → `common/utils/preview_policy.py` 的 `decide_preview()` |
| **駭客怎麼打** | ① 公司內任一有帳號的人，做一個內含程式的網頁檔（或把網頁檔改名成 `.png`）；② 當成證據或附件上傳；③ 稽核人員在證據清單點「預覽」；④ 伺服器照副檔名把它當網頁送出，瀏覽器在我們的網域裡執行那段程式；⑤ 程式用受害者的登入身分讀資料、改資料，或把登入狀態送出去 |
| **得手什麼** | 受害者的登入身分，能用他的名義做他能做的任何事 |
| **修正的做法** | ① **可預覽清單**：只有 pdf、html、png、jpg、gif、webp 這幾種可以在瀏覽器裡開，清單外一律強制下載；② **伺服器自己驗檔頭**：讀檔案開頭的特徵碼確認內容真的是宣稱的格式，叫 `.png` 但內容是網頁的也強制下載；③ **網頁預覽加隔離指令**：回應帶 CSP sandbox，不給執行程式、不給碰本站，排版與圖片照常顯示；④ 所有回應加「不准瀏覽器自己猜格式」的標頭；⑤ 不再用主機的檔案類型對照表（同一支程式在不同機器會給不同答案），改用套件內固定對照表 |
| **狀態** | ✅ 已修（CM-2062，套件 commit `a061438f`）。驗證：假 `.png` 被強制下載、真 png／pdf／html 正常預覽且 html 帶隔離、exe／svg 強制下載 |

## M03-13 🟠 掃描報告「檔名叫什麼就存什麼」，走同一條預覽路徑 {#m03-13}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #13，與 M02-4 同一件；模組頁原評 🟡 中；OWASP：A05 注入攻擊、A06 不安全的設計；主分類為冒充身分） |
| **在哪裡** | 弱點檢測整合模組：代理程式把掃描報告回傳給後端、存成證據 |
| **攻擊面位置** | 掃描報告的兩個檔名來源——呼叫端帶的檔名提示、代理程式回應標頭裡的檔名——都在系統外面。網頁格式正是掃描報告的正式格式，這是日常路徑，不是罕見情境 |
| **信任邊界位置** | **客戶機房的代理程式 ↔ 我們後端**。後端收到報告時應該自己決定檔名與格式；當時直接採用外面給的檔名，沒去掉路徑、也沒查副檔名，接著這份檔案被寫成證據，就進了 M02-4 那條預覽路徑 |
| **元件端點位置** | `jedi_remote_agent/api/routing.py`：`POST /agents/tasks/{uid}/result`（回報結果）→ `jedi_detection/app/service/detection_result_handler.py` 的 `_fetch_blob()`（取回報告、決定檔名）→ `jedi_detection/common/report_filename.py` 的 `sanitize_report_filename()`；危險的出口是證據清單的 `GET /file/pdf-preview/{uid}` |
| **駭客怎麼打** | ① 控制得了代理程式回應或檔名提示的人，回一份網頁格式的報告；② 後端照單全收檔名，存成證據；③ 「執行歷史」那一區點報告是下載（安全），但報告同時寫了一筆證據；④ 稽核人員從證據清單點「預覽」，網頁裡的程式就在他的瀏覽器裡用他的身分跑起來 |
| **得手什麼** | 稽核人員的登入身分 |
| **修正的做法** | ① 兩個檔名來源一律先過 `sanitize_report_filename()`：切掉目錄片段（含 Windows 與 Linux 兩種分隔符）、洗掉控制字元、查副檔名白名單；② 不合規的一律退回系統自產名 `detection_report_<任務編號>.html`；③ 刻意保留中文——OpenSCAP 報告慣例是中文檔名，濾掉會變一串底線；④ 預覽端由 M02-4 的白名單＋驗檔頭＋隔離指令一起收口 |
| **狀態** | ✅ 已修（CM-2062，套件 commit `a061438f`）。驗證：中文 OpenSCAP 檔名原樣保留，`../../etc/passwd` 類路徑被去掉，`.exe`／空字串退回預設名 |

## M03-1 🟠 客戶上傳的檢測規則包會被外部工具當成程式碼執行 {#m03-1}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #15；OWASP：A05 注入攻擊、A08 軟體或資料完整性失效；主分類為權限提升，另涉竄改資料） |
| **在哪裡** | 弱點檢測整合模組：「檢測基準」管理——建立或改版基準時上傳一包檢測規則（壓縮檔），系統自動解析出裡面有哪些檢查項目 |
| **攻擊面位置** | 已登入、有建立檢測基準權限的客戶管理員可打的基準管理 API。上傳規則包是設計上就開放給客戶的功能；網址型來源連上傳都不用，填一個自己伺服器的網址即可 |
| **信任邊界位置** | **客戶上傳的檔案 ↔ 後端主機上的外部程式（cinc-auditor）**。後端把壓縮檔重新打包後交給 cinc-auditor 解析，它讀設定檔 `inspec.yml` 時會先當成 Ruby 樣板渲染；該守的檢查應在「交給外部程式之前」，當時驗證器只驗結構，**沒有一項看設定檔內容** |
| **元件端點位置** | `jedi_detection/api/routing.py`：`POST /api/1.0/detection-tool-profiles`、`/detection-tool-profiles/{uid}/new-version`、`/detection-tool-profile-versions/{uid}/source`、`/detection-tool-profile-versions/{uid}/extraction`；解析鏈 `DetectionProfileExtractionService` → `common/profile_extractor/inspec.py`：`_repack_flat()` → `cinc-auditor json` |
| **駭客怎麼打** | ① 任一客戶的管理員登入；② 做一包檢測規則，在 `inspec.yml` 寫一段 Ruby 樣板；③ 上傳，結構全部正常、驗證器放行；④ 系統排解析，cinc-auditor 一讀設定檔就以後端程序身分把那段程式跑起來；⑤ 讀出系統密鑰、把資料庫身分提權成超級管理員，**任意改寫全平台客戶的資料** |
| **得手什麼** | 整台後端主機的權限，以及改寫全平台客戶資料的能力 |
| **修正的做法** | ① 在「重新打包」那一步本身擋：`_repack_flat()` 交給 cinc-auditor 前先讀出 `inspec.yml`，含樣板記號 `<%` 就拒收並回明確訊息；② 擋點放在上傳與網址兩條路共用的那一層，第三種來源出現時也擋得到；③ 開工前實查隨包 13 筆基準與 8 支內建規則包都沒用樣板語法，拒收不會打壞正常客戶；④ 同批把三道防炸彈上限下沉到共用層 `common/archive_limits.py`；⑤ **已知打折**：只看 `inspec.yml` 一個檔，若外部工具對 `controls/*.rb` 也做樣板渲染則擋不住；長期正解「把外部工具關進沙箱」未做 |
| **狀態** | ✅ 已修，1.21.0 出貨（CM-2057，套件 commit `c0fe19fd`）。驗證：含 `<%` 的包拒收、只有 controls 含 `<%` 的包放行（打折範圍）；套件 88 測試通過 |

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

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #20，與 M21-1 同一件；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，與 M09-2 同一件；模組頁原評 🟡 中；OWASP：A05 注入攻擊；主分類為權限提升，另涉資料外洩） |
| **在哪裡** | 意見回饋模組：管理員匯出意見回饋成 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](../OWASP-Top-10/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` |

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

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

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

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #23；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`、`2026-10-01-cm2369-license-tables-ancestor-read.sql`；讀這三張的功能如 `GET /api/1.0/license/status`（`jedi_license_runtime/api/routing.py`） |
| **駭客怎麼打** | ① 被停權的客戶用手上任一個子單位帳號登入；② 經任何一條以子單位身分寫到停權紀錄表的路徑送出刪除；③ 資料庫規則把母公司那列算成「這個子單位碰得到的」，連刪除一起放行；④ 停權紀錄「有一列就代表停權中」，刪掉整棵樹當場復權；⑤ 順手改掉授權到期日與模組清單、刪掉異動紀錄（模組頁未點名具體寫入入口，此處依資料庫規則推論） |
| **得手什麼** | 被停權客戶自行復權並改寫授權內容，且刪得掉事後追查用的異動時間線 |
| **修正的做法** | ① **改刪收緊**（CM-2271）：這三張連同弱點檢測、遠端代理共九張表，門禁規則改成前綴比對，只碰得到自己與底下；已裝機另開主線 migration 重建（套件檔以 DO 區塊包，既有庫會跳過）；新增守衛測試斷言全庫不再有拆陣列寫法；② **讀取開回給子單位**（CM-2369）：第①步讓子單位讀不到上層的照、被判成沒買全面 403；三張表各加一條只管「讀」的規則（重用既有的 `app_tenant_is_ancestor_of_session()`），改刪仍只吃前綴規則；③ 真庫測試 12 條：子讀上層＝1、子改刪上層＝0、兄弟互看＝0 |
| **狀態** | ✅ 已修（改刪：CM-2271，主專案 `6c74ff0e3`＋套件 `6e0d0d4b`，1.21.0；讀取：CM-2369，主專案 `3574033a6`＋套件 `ba14e128`，1.21.1） |

## 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-3 🟠 綁定外部帳號不確認是本人 {#m13-3}

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

## M13-4 🟠 存個人資料時順手把自己改成管理員 {#m13-4}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #30；OWASP：A01 存取控制失效；主分類為權限提升，另涉竄改資料） |
| **在哪裡** | 登入與權限模組：右上角「個人資料」頁的儲存——人人都能開，這是門檻最低的一條升權路 |
| **攻擊面位置** | 已登入使用者可打的自助修改 API。**任何一般員工**，在自己的個人資料頁就構成攻擊面 |
| **信任邊界位置** | **使用者 ↔ 帳號權限資料**。自助頁只該收畫面上真的會改的欄位（暱稱、職稱、信箱、電話、密碼），角色、所屬客戶、部門、狀態只能由管理端改。當時自助那支用 `UserRequest(unknown=INCLUDE)` 收整包，`update_user()` 再拿送來的 `user_roles` 整批覆蓋既有角色關聯 |
| **元件端點位置** | `jedi_iam/api/routing.py`：`PUT /user-profile/{uid}`（`UserProfileRoute.put`，`api/routes/user_route.py`）→ `update_user()`；管理端改角色走的是另一支 `PUT /user/{uid}`（`UserRoute.put`，需 `user.update` 能力點） |
| **駭客怎麼打** | ① 一般員工登入，打開自己的個人資料頁；② 按儲存時攔下請求，多加一個「角色＝管理員角色編號」（或把客戶、部門歸屬清空）；③ 系統照收，拿這個欄位整批覆蓋他的角色；④ 重新登入就是管理員（開發環境實測：一般帳號送出後角色真的變成原廠管理員角色） |
| **得手什麼** | 一般員工把自己提升為管理員，或抹掉自己的客戶與部門歸屬 |
| **修正的做法** | ① 新增 `UserProfileRequest`（`unknown=EXCLUDE`），白名單只收自助頁實際會送的欄位，`user_roles`、`tenants`、`org_units`、`status`、`is_super_admin` 一律不收；② 路由內強制用 `get_user_by_uid()` 查資料庫現況，把這四項重建塞回去再交給 `update_user()`——防的不只是「拒收未知欄位」，是「已知欄位也不讓呼叫端決定」；③ 管理端那支不動；④ 新增 4 條測試，突變還原成舊寫法核心斷言轉紅 |
| **狀態** | ✅ 已修（CM-1576，套件 jedi-iam commit `7cf3cf2b`）。已核對：角色、客戶、部門、狀態四項都從資料庫重新取出覆蓋 |

## M18-1 🟠 被停權的客戶自己上傳舊授權檔就能復權 {#m18-1}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #33；OWASP：A06 不安全的設計、A08 軟體或資料完整性失效；主分類為權限提升，另涉竄改資料） |
| **在哪裡** | 授權管理模組：平台把欠費或違約的客戶「停權」（產品轉唯讀），以及客戶上傳、啟用授權檔 |
| **攻擊面位置** | 已登入的客戶帳號可打的授權上傳與啟用 API；其中啟用的幾條**只要登入、不需管理員**——刻意如此，開通當下客戶常還沒有管理員在場 |
| **信任邊界位置** | **客戶送來的授權檔 ↔ 平台的停權決定**。停權原本記在「那一張授權檔」的欄位上，而換照是「新增一張、取代舊的」，新的那張停權欄位是空的。停權是平台對「這個客戶」的決定，不該跟著客戶自己能換掉的資料走 |
| **元件端點位置** | `jedi_license_runtime/api/routing.py`：`POST /api/1.0/license/upload`、`/license/activation/upload`、`/license/activation/online` → `LicenseVerificationService._store_verified_license()`；停權 `POST /license/tenants/{uid}/lock` → `TenantLicenseAdminService.force_lock_tenant()`；執法 `LicenseGuard.snapshot_for()` |
| **駭客怎麼打** | ① 平台把某客戶停權，產品轉唯讀；② 該客戶任一登入帳號把手上那張舊授權檔重新上傳；③ 系統建一筆新的授權紀錄取代現行照，新列的停權欄位是空的；④ 唯讀當場解除，不需要平台同意 |
| **得手什麼** | 被停權的客戶自行改寫自己的授權狀態，平台的最後商務手段形同無效 |
| **修正的做法** | ① **把停權從授權檔搬到客戶身上**：新表 `config.tenant_license_suspensions`，一列＝該客戶目前被停權，解除就刪那一列，與授權檔完全脫鉤；② `force_lock_tenant()`／`unlock_tenant()` 改寫新表，`LicenseGuard.snapshot_for()` 與 `LicenseStatusService.get_my_license_status()` 改讀新表，到期推進改批次查新表決定哪些客戶跳過；③ 舊照上的兩個停權欄位保留當歷史；④ 新表一出生帶著寫反的資料庫門禁，子單位可直接刪掉母公司的停權紀錄——另案修正（見 M18-8） |
| **狀態** | ✅ 已修（CM-1579，主專案 commit `a039aa287`、套件 `8f9155a9`） |

## M24-1 🟠 雲端硬碟授權回呼頁，點一個連結就被偷走登入身分 {#m24-1}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #199；OWASP：A05 注入攻擊；主分類為冒充身分） |
| **在哪裡** | 主系統的雲端硬碟整合：「系統設定 → 雲端硬碟 → 連結 Google 硬碟」，Google 同意後跳回我們系統的那個小頁面 |
| **攻擊面位置** | 回呼網址設計上**不需要登入**（Google 要能把人導回來），網路上任何人都能做一個指向它的連結 |
| **信任邊界位置** | **網址上的參數 ↔ 本站網域裡執行的頁面程式**。這個頁面與產品同網址、登入狀態存在瀏覽器裡，所以頁面程式等同本站身分。網址上的錯誤訊息應該只是「代碼」；當時被原樣塞進頁面的 `<script>` 裡，2026 年 6 月用的 `json.dumps` 不會跳脫 `<` `>` `/`，擋不住 |
| **元件端點位置** | 主專案 `api/cloud_integration/__init__.py`：`GET /integrations/google-drive/callback`（`GoogleDriveCallbackRoute`）→ `api/cloud_integration/routes/google_drive_integration_route.py` 的 `_callback_response()`／`_callback_html()` |
| **駭客怎麼打** | ① 不需要帳號，做一個回呼網址，`error` 參數填 `</script><script>…` 開頭的特製文字；② 寄給一個已登入的使用者；③ 對方一點，頁面被切成兩段，攻擊者的程式在本站網域裡跑；④ 讀走瀏覽器裡的登入權杖送出去；⑤ 用他的名義看、改、刪他看得到的所有稽核資料，受害者是管理員就是整家公司 |
| **得手什麼** | 受害者的登入身分 |
| **修正的做法** | 三道各自有效：① **白名單**：網址上的錯誤只認 `access_denied`，其他情況依錯誤碼對到 `invalid_state`／`exchange_failed` 等固定代碼，例外訊息不再回顯、只進後端紀錄；② **跳脫**：內嵌值再把 `<` `>` `&` 換成跳脫序列；③ **CSP**：頁面只准執行帶「本次隨機碼」的那一段程式；④ 前端改用後端給的固定代碼查翻譯顯示 |
| **狀態** | ✅ 已修，1.21.0 出貨（CM-2200，主專案 commit `f0a740bd7`、前端 `9569cf6`）。驗證：新增 8 條測試＋突變測試，DEV 實打四種情境、瀏覽器無 CSP 違規 |

## M22-4 🟠 「誰算總部」只擋得住自家子公司，擋不住另一家客戶的總部 {#m22-4}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #201；OWASP：A01 存取控制失效；主分類為權限提升） |
| **在哪裡** | 系統設定模組：寄信伺服器、員工帳號目錄（LDAP）、登入政策、日誌轉送這幾組全站只有一份的設定；它是 [M22-1](#m22-1) 修法的漏洞 |
| **攻擊面位置** | **任何一家客戶的總部管理員**。M22-1 修好之後子公司改不動了，但「總部」的判定是「組織樹第二層」，而原廠底下的第二層每一家客戶都是，所以每家客戶的總部管理員都打得到 |
| **信任邊界位置** | **客戶 ↔ 客戶（租戶 ↔ 租戶）**，以及**客戶 ↔ 原廠平台**。全站那一份設定只該由「整套系統的擁有者」改；當時用路徑深度認總部，深度只分得出上下層，分不出「這家客戶」與「另一家客戶」 |
| **元件端點位置** | 判定在主專案 `common/authz/tenant_hq.py` 的 `viewer_is_tenant_hq()`／`require_tenant_hq()`；查「安裝精靈建的那家客戶」在 `infra/setup/setup_state_reader.py` 的 `primary_customer_tenant_id`。受保護的寫入入口同 M22-1：`PUT /api/1.0/system/security-policy`、`PUT/DELETE /api/1.0/system/config/{group}/{key}`、套件 jedi-system-core `POST /system/config`、`PUT/DELETE /system/config/{uid}`、日誌轉送 `PUT /log-forwarding` |
| **駭客怎麼打** | ① 客戶 B 的總部管理員登入（他的公司與客戶 A 一樣掛在原廠底下第二層）；② 打開系統設定的員工帳號目錄或寄信設定；③ 系統問「你是不是總部」——深度符合，放行；④ 他改掉的是全站那一份，客戶 A 與原廠平台本身的登入、寄信從此走他指定的伺服器（統籌者在開發環境用實際公司路徑測過） |
| **得手什麼** | 改掉其他客戶與原廠共用的登入與寄信設定，接管別家的登入流程 |
| **修正的做法** | ① 只改判定、四個呼叫點不動：**平台管理員**一律可改；**雲端多租戶版**（`DEPLOYMENT_MODE` 不是 host）一律不准客戶改；**落地安裝版**只認「安裝精靈建立的那個客戶租戶」——原廠底下第一個建的那家，用繞過隔離的獨立連線查，因為子公司看不到上層會誤判；② 不用客戶名稱或描述欄認（可被編輯）；③ 查不到身分、不在請求裡、查不到客戶租戶一律往拒絕倒；④ 新增 10 條測試，突變改回「深度＝2」4 條轉紅 |
| **狀態** | ✅ 已修，1.21.0 出貨（CM-2202，主專案 commit `6cfcb116e`）。驗證：開發環境兩種部署模式實打，落地版只有精靈建的客戶與平台管理員 200，另兩家客戶與子公司 403；雲端版只有平台管理員能改；讀取各身分不受影響 |

## M03-14 🟡 唯讀角色可以對客戶機器發動掃描、刪掉執行紀錄 {#m03-14}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #37，與 M06-7 同一件；OWASP：A01 存取控制失效；主分類為權限提升，另涉竄改資料、事後無法追查） |
| **在哪裡** | 弱點檢測整合模組：任務頁上的掃描操作——開始掃描、取消、單台重跑、單台取消、立即開始、整組取消、刪單筆、整組刪，共八個會改狀態的功能 |
| **攻擊面位置** | 已登入、且是該專案參與者的任何角色，包含產品定義上「只能看、沒有待辦」的唯讀角色。需要知道任務或執行組編號（專案頁看得到） |
| **信任邊界位置** | **API 層 ↔ 任務經手人**。該守的是「你是不是這張任務的負責人、或這個專案的管理者」；這八支借用的是稽核流程那支「任一專案參與者都放行」的寬鬆守門 |
| **元件端點位置** | `jedi_detection/api/routing.py`：`POST /api/1.0/detection-tools/jobs/{job_uid}/execute`、`/jobs/{job_uid}/cancel`、`/execution-groups/{group_uid}/assignments/{assignment_uid}/rerun`、`…/cancel`、`…/start-now`、`/execution-groups/{group_uid}/cancel`、`DELETE /execution-groups/{group_uid}`、`DELETE /executions/{execution_uid}`；處理在 `jedi_detection/app/service/detection_orchestration_service.py` 八處，守門呼叫主專案 `WorkflowExecutionService.assert_job_operator` |
| **駭客怎麼打** | ① 以唯讀角色登入，打開專案的弱點檢測任務；② 直接按或直接打「開始掃描」，系統只確認他是專案成員就放行；③ 掃描帶著維運帳密連進客戶的正式機器；④ 接著反覆取消再重派干擾正常檢測，或刪掉失敗的執行紀錄——掃描紀錄被一個不該動手的人改掉 |
| **得手什麼** | 以唯讀身分發動帶帳密的掃描，並刪改掃描執行紀錄 |
| **修正的做法** | ① 稽核流程那邊先新開嚴格版守門 `assert_workflow_job_operator`（只放行任務被指派人與專案 manager，見 M06-7）；② **主專案加一層包裝**：套件手上只有流程實例編號，新增 `assert_job_operator_by_workflow_execution_id` 反查後委派嚴格守門，`WorkflowExecutionService.assert_job_operator(workflow_execution_id, template_job_id)` 提供一行呼叫；查無流程實例回 404；③ **套件八處改接**：八支寫入功能由 `assert_project_participant` 換成 `assert_job_operator`；掃描跑完由代理程式回報觸發的自動完成路徑沒有使用者身分，維持原樣 |
| **狀態** | ✅ 已修，1.21.0 出貨（FR-114.1-2，CM-2022，套件 commit `88e519b4`＋主專案 `e10d10a83`） |

## M06-7 🟡 只能看的人可以完成或退回別人的稽核任務 {#m06-7}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #37，與 M03-14 同一件；OWASP：A01 存取控制失效；主分類為權限提升，另涉竄改資料、事後無法追查） |
| **在哪裡** | 稽核流程模組：任務的「完成」與「退回上一關」兩個按鈕 |
| **攻擊面位置** | 已登入、是該專案參與者的任何角色，包含唯讀角色（原意是開放給外部顧問或其他單位同事觀看）。需要知道任務編號 |
| **信任邊界位置** | **API 層 ↔ 任務經手人**。權限政策寫明「任何角色的專案參與者都會通過」，另一處又把這個角色定義成「純瀏覽、沒有待辦」——產品承諾與判斷邏輯互相矛盾，守門只守到「是不是專案的人」 |
| **元件端點位置** | 主專案 `api/flow_engine/__init__.py`：`POST /api/1.0/flow-engine/task/complete/{id}`（`JobCompleteRoute`）、`POST /api/1.0/flow-engine/task/revert/{id}`（`JobRevertRoute`）→ `app/flow_engine/service/workflow_execution_service.py` 的 `complete_job`／`revert_job`；守門 `app/flow_engine/service/job_operator_guard.py`、`common/authz/workflow.py` |
| **駭客怎麼打** | ① 以唯讀角色登入，打開別人負責的稽核任務；② 按「完成」或「退回」，系統只確認他是專案成員就放行；③ 任務、問卷、流程狀態跟著被推進或退回；④ 稽核紀錄上的經手人變成這個只能看的人 |
| **得手什麼** | 以旁觀者身分結案或退回別人的稽核任務，改動流程狀態 |
| **修正的做法** | ① **新開嚴格版守門**（FR-114.1-1）：`common/authz/workflow.py` 新增 `assert_workflow_job_operator`，前段政策與既有那支一致，最後多問「你是這張任務的被指派人、還是專案 manager」；不收嚴既有那支，因為佐證文件的增刪改也吃它；完成與退回各改一行呼叫；新增錯誤碼 `GRC_NOT_JOB_OPERATOR`；② 保留兩條拿掉會壞的旁路：無使用者身分（掃描自動完成）不守、輪次主流程任務沿用已知專案編號；③ **歸屬判斷改走唯一入口**（CM-2119）：「任務屬於哪個專案」原本查指派表，未指派的任務查不到會被誤擋，改委派 `JobProjectOwnershipQuery`（走輪次鏈） |
| **狀態** | ✅ 已修，1.21.0 出貨（FR-114.1-1，CM-2021，主專案 commit `18b2ba06c`；誤擋修正 CM-2119，`e8445ae16`）。驗證：九種角色組合逐條跑過；未指派任務的成員改前 403、改後 200，非成員仍 403 |

## M10-6 🟡 指派任務時「專案填自己的、任務填別人的」照樣放行 {#m10-6}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #41；OWASP：A01 存取控制失效；主分類為權限提升，另涉竄改資料） |
| **在哪裡** | 任務與成員管理模組：新增一筆任務指派（把某張任務指派給某位成員）。模組頁沒有逐條表，此條依總表描述與修正 commit 寫成 |
| **攻擊面位置** | 已登入使用者可打的任務指派 API。**任何人自己開一個新專案就自動是管理者**，門檻等於零 |
| **信任邊界位置** | **專案 ↔ 專案（同一客戶內）**。系統只查「操作者是不是請求裡那個專案的管理者」＋「被指派人是不是那個專案的成員」，**從不核對「這張任務本身屬於哪個專案」**——兩道檢查問的都是呼叫者自己填的那個專案 |
| **元件端點位置** | `jedi_task_platform/participant/api/routing.py`：`POST /api/1.0/task-assignee`（`TaskAssigneeRoute.post`，`api/routes/task_assignee_route.py`）→ `app/service/task_assignee_service.py` 的 `add_task_assignee`；任務歸屬由主專案 `infra/flow_control/task_existence_query.py` 的 `FlowEngineTaskExistenceQuery.get_project_id` 提供 |
| **駭客怎麼打** | ① 小明自己開一個新專案，成為它的管理者；② 送出指派時，專案欄填自己的專案、任務欄填別人專案的任務、被指派人填自己；③ 兩道檢查都問自己的專案，全部通過；④ 別人專案的任務被指派給小明，他就能以經手人身分操作那張任務 |
| **得手什麼** | 把自己塞進別人專案的任務，取得那張任務的經手權 |
| **修正的做法** | ① 套件的任務查詢介面 `ITaskExistenceQuery` 新增 `get_project_id(job_uid)`；② `add_task_assignee` 寫入前比對任務真正所屬專案與請求的專案，查不到或不相等一律回「找不到」，不回 403 以免被當探測工具；③ 主專案實作那支查詢，一行委派全系統唯一的 `JobProjectOwnershipQuery`——刻意不查指派表（那正是這道比對要保護的表）；④ 原本要在流程實例表補專案欄位，決策者裁作廢，避免變成兩份真相；⑤ 其他寫入點逐一核過：改與刪只能動既有紀錄、批次指派是恆回空的殘留功能，不套 |
| **狀態** | ✅ 已修，1.21.0 出貨（FR-114 CM-2071，套件 commit `ef9a313b`、主專案 `fccfe4db7`）。驗證：同專案 200、專案與任務錯配 404 且沒寫入；DEV 現有 166 筆指派紀錄用新判斷不一致 0 筆 |

## M11-13 🟡 建立資源庫的入口只驗登入、沒有權限檢查 {#m11-13}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #65；OWASP：A01 存取控制失效；主分類為權限提升） |
| **在哪裡** | 合規文件核心模組：「合規資源庫」的建立——選一個框架版本，系統複製一整套控制項目錄做成公司自己的範本庫 |
| **攻擊面位置** | 已登入使用者可打的建立資源庫 API，**公司內任何角色（含唯讀帳號）** 都打得到；而隔壁做同樣事情的「新增範本」入口是有要求權限的 |
| **信任邊界位置** | **使用者 ↔ 「建立範本」這項功能權限**。同一件事有三條路（手動建、用 Excel 建、用 Word 建），應該在三條路共同經過的地方問「你有沒有建立範本的權限」；當時只有其中兩條問了。又因為每建一次就要複製整套目錄、沒有次數上限，等於一般帳號可以用很小的代價把資料庫灌大。不跨客戶 |
| **元件端點位置** | 主專案 `api/oscal/__init__.py`：`POST /api/1.0/oscal/resource-libraries`（`ResourceLibraryCreateRoute`，`api/oscal/routes/resource_library_route.py`）→ `app/oscal/service/resource_library_app_service.py` 的 `create_resource_library()`；用 Excel 建新庫那條在 `app/oscal/service/ssp_excel_import_app_service.py` 的 `_verify_source_exists()`；對照組是手動新增 `POST /api/1.0/module-frame`（`ModuleFrameRoute`） |
| **駭客怎麼打** | ① 一個沒被給「建立範本」權限的帳號（例如唯讀角色）登入；② 直接送建立資源庫的請求，選任一已發佈的框架版本；③ 系統只驗登入就複製一整套控制項目錄；④ 寫個迴圈重複送，每次都多一整套目錄，沒有上限 |
| **得手什麼** | 繞過「不給建立權」的設定自建範本庫，並能不受限地灌大資料庫 |
| **修正的做法** | ① 建立資源庫的路由掛上 `@require_capability("module-frame.create")`，與手動新增、Word 建新庫同一顆權限；② 盤點時發現**用 Excel 建新庫那條也沒門**，在 `_verify_source_exists()` 補上同一顆權限檢查（新錯誤碼 `GRC_403067`）——三條建庫路徑只擋兩條等於沒擋；③ 在三條路共同經過的 `create_resource_library()` 加每家客戶每小時 10 次上限（`assert_within_tenant_limit()`，新錯誤碼 `GRC_429001`），上限綁客戶而不綁人，換帳號繞不過去；快取連不上時放行並記警告 |
| **狀態** | ✅ 已修，1.21.0 出貨（CM-2040，主專案 commit `60015232f`）。CM-2179（commit `94275d72f`）只確認上述三處權限仍在，它本身修的是同模組第 30 條「草稿版本也能拿去建庫」，見下方「與總表對不上」說明 |

## M10-7 🟡 群組層成員的增改刪因漏接線完全不檢查權限 {#m10-7}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #68；OWASP：A01 存取控制失效；主分類為權限提升，另涉竄改資料） |
| **在哪裡** | 任務與成員管理模組：「群組層成員」的新增、修改、刪除——這張表被拿來算「哪些專案我看得到」。模組頁沒有逐條表，此條依總表描述與拆除 commit 寫成 |
| **攻擊面位置** | 若有任何呼叫者接上這三支，**守門形同虛設**；查證時全系統零呼叫（沒有路由、前端、DI 容器在用），屬未爆彈 |
| **信任邊界位置** | **呼叫者 ↔ 成員資料（決定誰看得到哪個專案）**。這組服務的守門靠注入的角色服務，但系統啟動時組裝它的那一行從沒接上角色服務（同檔其他手足都有接）——檢查程式在，但永遠拿不到判斷依據 |
| **元件端點位置** | 套件 `jedi_task_platform/participant/app/service/project_group_participant_service.py` 的 `add_project_group_participant`／`update_project_group_participant`／`delete_project_group_participant`；漏接線處在主專案 `di_containers/flow_engine/project_participant_containers.py` 的 `project_group_participant_service`；資料表 `project_group_participants` |
| **駭客怎麼打** | ① 今天沒有入口可打；② 只要哪天有人把這三支接上路由或畫面；③ 任何呼叫者就能把自己加進任一專案的群組層成員；④ 系統算「我看得到哪些專案」時會算進去，等於自己幫自己開了別人專案的門 |
| **得手什麼** | （若被接上）把自己加進任何專案的成員名單 |
| **修正的做法** | **拆除了什麼**：① 三支寫入方法整支刪除；② 刻意不補接線——補上會安靜改變行為；③ 同批刪除同性質的流程成員增改刪（其守門找不到紀錄時提早放行、真正檢查永遠跑不到）與三支舊版任務指派查詢；④ 保留查詢方法（AI 儀表板在用）與專案刪除時的清理 |
| **狀態** | 🗑️ 裁定刪除，已刪除，1.21.0 出貨（CM-2044，套件 commit `9c9029fd`） |

## M10-8 🟡 流程參與者的增改刪，守門寫在永遠跑不到的位置 {#m10-8}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #69；OWASP：A01 存取控制失效；主分類為權限提升） |
| **在哪裡** | 任務與成員管理模組：「流程參與者」的新增、修改、刪除——一整組從沒啟用過的功能。模組頁沒有逐條表，此條依總表描述與拆除 commit 寫成 |
| **攻擊面位置** | 今天沒有：全系統零呼叫（沒有路由、前端、DI 容器在用）。**現在擋住它的，是它本身就壞著** |
| **信任邊界位置** | **呼叫者 ↔ 流程成員名單**。增改刪三支都先呼叫「是不是這個流程的管理者」的檢查，但那支檢查**找不到紀錄時提早結束、當作通過**，真正比對角色的那一行永遠跑不到——守門的形狀在，判斷不在 |
| **元件端點位置** | 套件 `jedi_task_platform/participant/app/service/process_participant_service.py` 的 `add_process_participant`／`update_process_participant`／`delete_process_participant` 與守門 `_assert_manager_for_process()`（皆已刪） |
| **駭客怎麼打** | ① 今天沒有入口可打；② 只要哪天有人把這三支接上路由；③ 任何呼叫者對一個查不到既有成員紀錄的流程送新增，守門提早放行；④ 就把自己加進那個流程的參與者，取得經手權 |
| **得手什麼** | （若被接上）把自己加進任何流程的參與者名單 |
| **修正的做法** | **拆除了什麼**：① 三支寫入方法整支刪除；② 已成孤兒的守門 `_assert_manager_for_process()` 一併刪除；③ 保留查詢方法 `get_process_participants()` 與專案刪除時的清理；④ 與 M10-7、M10-18 同一批，查證全系統零呼叫後才刪 |
| **狀態** | 🗑️ 裁定刪除，已刪除，1.21.0 出貨（CM-2044，套件 commit `9c9029fd`）。驗證：套件成員相關 79 測試、主專案任務指派相關 16 測試通過 |

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

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #72，與 M17-11 同一件；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，與 M16-8 同一件；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`） |

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

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #73，與 M15-6 同一件；OWASP：A03 軟體供應鏈失效、A07 身分驗證失效；主分類為竄改資料） |
| **在哪裡** | 公告模組檢視時順手做的密碼掃描撈到的（與公告功能無關）：一份分散式檔案代理程式的開發交接文件，明文寫著公司內部套件倉庫與檔案儲存服務的帳密 |
| **攻擊面位置** | 主專案的程式碼倉庫——這份文件與它產生的網頁、文件站鏡像都進了版本控制。**任何讀得到程式碼的人**都構成攻擊面，不需要系統帳號；要真的用上帳密還需要連得到公司內網 |
| **信任邊界位置** | **「讀得到程式碼的人」↔「有權往套件倉庫發布元件的人」**。發布權只該在打包機與發版的人手上（帳密放在打包機不進版控的設定檔）；交接文件把帳密原文抄了進去，還寫著「已加入 gitignore」——被忽略的是那個設定檔，不是這份文件 |
| **元件端點位置** | 外流處：`docs/features/FR-039-2606-distributed-file-agent/handoff/2026-06-18-FR039-handoff.md`（＋同目錄 `index.html`、`docs/features-site/` 鏡像）；被打穿的發布口：套件 monorepo 各套件 `publish.sh` 用的 `poetry publish --build -r nexus`；吃這個倉庫的安裝端：主專案 `pyproject.toml` 的 `[[tool.poetry.source]] nexus` |
| **駭客怎麼打** | ① 任何能讀主專案程式碼的人打開這份交接文件；② 照抄套件倉庫的管理員帳密，在內網登入；③ 上傳一個看起來正常、但夾帶後門的內部套件新版本；④ 下次打包機重建時，這個版本被當成公司自己的元件裝進產品，跟著安裝包送到客戶機房；⑤ 同一份文件裡的檔案儲存金鑰則可直接改寫存放在那裡的稽核證據 |
| **得手什麼** | 往客戶安裝包裡塞自己程式碼的位置，以及開發環境檔案儲存的讀寫權 |
| **修正的做法** | ① **換發**：套件倉庫與檔案儲存的帳密都已換發，舊值失效（環境操作，不在 commit 裡）；② **清出版控**：先改來源 md，帳密改寫成「請查部署文件」，同時改掉「已加入 gitignore」的誤導敘述；③ 依序重建網頁、同步文件站鏡像，最後補清對話紀錄殘留——依較寬的搜尋多清出 7 個卡片沒列到的檔；④ 全 repo 交叉搜尋確認殘留樣式 0 命中 |
| **狀態** | ✅ 已修（CM-2048，主專案 commit `e8e134a4e`）。歷史 commit 仍有舊值，因已換發不改寫歷史 |

## M15-6 🟡 同一份交接文件裡的三樣帳密 {#m15-6}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #73，與 M17-10 同一件；OWASP：A03 軟體供應鏈失效、A07 身分驗證失效；主分類為竄改資料） |
| **在哪裡** | AI 儀表板模組檢視範圍外另外撈到的（與儀表板功能無關）：與 M17-10 **同一份交接文件、同一段**，一共明文寫了三樣——套件倉庫管理員帳密、檔案儲存服務的金鑰、一組測試客戶的畫面登入帳密 |
| **攻擊面位置** | 同 M17-10：主專案程式碼倉庫，任何讀得到程式碼的人；三樣都只在公司內網有效，外人拿到用不上——這是評中而非高的理由 |
| **信任邊界位置** | 三條邊界被同一份文件同時打穿：**讀程式碼的人 ↔ 套件倉庫發布權**、**讀程式碼的人 ↔ 檔案儲存（稽核證據）**、**讀程式碼的人 ↔ 測試環境登入畫面**。三樣都該只在部署文件或各機器不進版控的設定裡 |
| **元件端點位置** | 外流處：`docs/features/FR-039-2606-distributed-file-agent/handoff/2026-06-18-FR039-handoff.md`（套件倉庫帳密那行、`UPDATE public.system_configs` 設定檔案儲存那段、測試客戶登入帳號兩處）；對應服務：主專案 `pyproject.toml` 的套件倉庫來源、檔案儲存的連線設定存在 `system_configs` 表 |
| **駭客怎麼打** | ① 讀得到程式碼的人打開交接文件；② 拿套件倉庫管理員帳密推一個帶後門的元件，等下次打包裝進客戶手上的程式；③ 拿檔案儲存金鑰直接改寫存放的稽核證據，繞過系統所有權限檢查；④ 拿那組畫面帳密直接登入測試環境 |
| **得手什麼** | 往安裝包塞東西的位置、稽核證據的讀寫權、一個可直接登入的帳號 |
| **修正的做法** | ① **三樣都換發**，舊值失效（環境操作）；② CM-2048 清掉交接文件裡三處明文，全部改寫成「請查 .env 或部署文件」，並重建網頁與文件站鏡像；③ 同一組套件倉庫密碼還散在打包腳本與前端三支設定檔，另由 M23-6 收——只清那四個檔、漏了這份交接文件等於沒清乾淨 |
| **狀態** | ✅ 已修（CM-2048，主專案 commit `e8e134a4e`）。三樣已換發，殘留字串已清 |

## M03-5 🟡 網址型規則包放行不加密連線、也不記指紋 {#m03-5}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #78；OWASP：A08 軟體或資料完整性失效、A04 加密機制失效；主分類為竄改資料） |
| **在哪裡** | 弱點檢測整合模組：用「網址」建立檢測基準——平台下載解析一次，派工時把網址交給客戶機房的代理程式自己去下載並執行 |
| **攻擊面位置** | 平台與代理程式下載規則包的那段網路路徑。不需要我們系統的帳號，需要的是**站在傳輸路徑上能動手腳**的位置（同網段、DNS、代理伺服器） |
| **信任邊界位置** | **外部網址下載回來的內容 ↔ 客戶機房的代理程式**。上傳型規則包有指紋、代理程式會比對；網址型當時連 `http://` 也收、而且不記指紋——代理程式拿到什麼就跑什麼。後端自己的下載器本來只准 https，同一系統兩套標準 |
| **元件端點位置** | `jedi_detection/api/routing.py`：`POST /api/1.0/detection-tool-profiles`、`PATCH /detection-tool-profile-versions/{uid}/source` → `DetectionProfileService._resolve_source()`；平台下載與落庫 `DetectionProfileExtractionService`；派工 `common/detection_profile_ref.py` 的 `build_payload()` |
| **駭客怎麼打** | ① 客戶管理員用一個 http 網址建了檢測基準；② 攻擊者站在代理程式下載的網路路徑上；③ 代理程式領到掃描工作去下載時，回一包換過的規則；④ 代理程式手上有客戶主機帳密，直接執行那包規則，掃描報告也一起造假；⑤ 因為不加密，連破解都不用 |
| **得手什麼** | 在客戶內網的代理程式上執行任意程式碼，並偽造掃描結果 |
| **修正的做法** | ① 新登記只收 https：`_resolve_source()` 的白名單直接取既有的 `safe_http_fetch.ALLOWED_SCHEMES`，不另立一份；不合格回新錯誤碼 `DETECTION_TOOLS_400013`；② 既有 http 基準不強制失效（避免客戶既有基準一次全滅），但抽取時寫一筆醒目警告；③ 平台第一次抽取成功時，把下載到的壓縮檔 sha256 落庫，並由 `build_payload()` 隨派工下發給代理程式；④ **打折**：代理程式端實際比對指紋已裁定另開小卡，本次只把指紋記進派工資料 |
| **狀態** | ✅ 已修，1.21.0 出貨（CM-2057，套件 commit `c0fe19fd`）。代理端比對指紋屬後續小卡，本次未見對應 commit |

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

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

## M20-4 🟡 兩支半夜清理程式靠「找不到身分就給最高權限」才跑得動 {#m20-4}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #108；OWASP：A10 例外狀況處理不當、A01 存取控制失效；主分類為權限提升，另涉讓服務停擺） |
| **在哪裡** | 客戶資料隔離專項：每天凌晨兩支自動清理程式——01:00 清法規框架解析的舊工作單、03:10 清「指向已刪除任務」的孤兒資料 |
| **攻擊面位置** | 不是攻擊面，是**規劃上的相依**：兩支程式沒有使用者身分，當時能看到全部客戶資料，靠的是資料庫連線那層「沒有身分就給最高權限」的放行規則（CM-1559 已記錄那條規則本身就是個洞） |
| **信任邊界位置** | **背景程式 ↔ 資料庫隔離規則**。要跨客戶掃描的背景程式應該**顯式宣告**「我是系統、我要跨客戶」，而不是靠「找不到身分」這個失敗狀態被放行；靠失敗狀態放行的那一天被收緊，兩支程式會看不到任何資料、安靜地什麼都不做 |
| **元件端點位置** | 主專案 `core/scheduler.py`：`_framework_parse_job_cleanup_tick()`（01:00）、`_job_binding_orphan_cleanup_tick()`（03:10）→ `app/flow_control/service/job_binding_orphan_cleanup_service.py` 的 `run_once()`；具名身分工具在套件 `jedi_common.session.auth.system_context` |
| **駭客怎麼打** | ①（不是駭客，是未來的修補動作）有人去收緊「沒有身分就給最高權限」那條規則（遲早要做，本身就是一個洞）；② 兩支清理程式從此連線時看不到任何客戶資料；③ 它們不報錯，只是每晚掃到 0 筆；④ 孤兒資料與過期工作單一直堆積，沒有人發現 |
| **得手什麼** | 沒有人得手；是清理機制在不知不覺中停擺 |
| **修正的做法** | ① 套件新增 `system_context("<排程名>")`，把「繞過隔離」從失敗狀態的副作用改成**一個要顯式宣告、有名字的動作**；② 兩支排程各自包上具名系統身分；孤兒清理那支要在服務自開資料庫連線**之前**就設好身分，否則那條連線讀不到；③ 原本「沒有身分就給最高權限」的說明文字改寫成反向警語；④ 加守衛測試掃排程程式碼，確認跨客戶的排程都有具名身分，突變（拿掉）會轉紅 |
| **狀態** | ✅ 已修（CM-1767／FR-094.1a，主專案 commit `3af065133`、套件 commit `2bd93918`）。2026-09-16 複查確認兩支都以具名系統身分執行 |

## M11-19 🟡 控制項現況 Excel 的描述會被寫成公式 {#m11-19}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #151；OWASP：A05 注入攻擊；主分類為權限提升） |
| **在哪裡** | 合規文件核心模組：控制項實作現況（SoA）批次匯出成 Excel，改完再匯回 |
| **攻擊面位置** | 能寫控制項現況描述的使用者。描述欄是自由文字 |
| **信任邊界位置** | **我們的資料 ↔ 複核者的電腦**。E 欄現況描述、H 欄檢查項目描述原樣寫進儲存格，`=` 開頭就被判成公式；應該在寫出時標成純文字 |
| **元件端點位置** | 主專案 `api/oscal/__init__.py`：`GET /ssp/{ssp_uid}/control-implementations/export`（`SspControlImplExportRoute`）→ `app/oscal/service/ssp_control_impl_import_service.py` 的 `export_excel()`（兩個寫入點） |
| **駭客怎麼打** | ① 能寫現況描述的人填一段 `=` 開頭的公式；② 複核的人匯出 Excel 打開；③ 按下「啟用內容」，公式在複核者自己的電腦上執行 |
| **得手什麼** | 在複核者電腦上執行內容 |
| **修正的做法** | ① 兩個寫入點改走 jedi-common 的 `set_text_cell()`：把儲存格標「強制文字」旗標（quotePrefix），值一個字不改；② **不用加單引號那種做法**——這份檔會被匯回，單引號會被讀成內容，每來回一趟多一撇 |
| **狀態** | ✅ 已修，1.21.0 出貨（FR-114 CM-2055，主專案 commit `f85a82edb`，共用函式在套件 `38de2886`）。驗證：存檔讀回值原樣、無多餘單引號 |

## M11-20 🟡 計畫內容裡的公式，在別人下載範本 Excel 時執行 {#m11-20}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #166；OWASP：A05 注入攻擊；主分類為權限提升） |
| **在哪裡** | 合規文件核心模組：下載 SSP 匯入範本 Excel（空白或已填）、框架版本空白範本、帳號匯入範本 |
| **攻擊面位置** | 能編輯計畫內容（元件名、系統名、範本名等）的使用者 |
| **信任邊界位置** | **我們的資料 ↔ 下載者的電腦**。資料從進系統到變成 Excel，六個關卡（上傳解析、存檔、讀出、組下拉選單、寫儲存格、檔案設定）可以擋、實際擋了零個；產生的檔案還設成「打開時重新計算」 |
| **元件端點位置** | 主專案 `api/module_frame/__init__.py`：`GET /module-frame/{uid}/ssp-import-template`、`GET /ssp/{ssp_uid}/excel-template`、`GET /ssp-import-template`；jedi-iam `GET /user/download/sample`（`UserUploadSampleDownloadRoute`）→ `app/module_frame/excel_template/generator.py`、`lookup_builder.py`、`app/auth/service/user_import_template_app_service.py`；上傳端 `app/oscal/service/excel_parser/sheet_handlers.py` |
| **駭客怎麼打** | ① 能編輯計畫的人把元件名或系統名改成 `=HYPERLINK(...)` 之類的公式；② 別人下載這份範本 Excel；③ 打開時檔案自動重算，公式在下載者電腦上執行 |
| **得手什麼** | 在下載者電腦上執行內容 |
| **修正的做法** | ① 七個寫出點全改用 `set_text_cell()`（標純文字、值不改）：下拉選單值、輔助欄、逐列表、直式表值欄、說明頁兩欄（說明頁含使用者命名的範本名）、帳號匯入範本的下拉值；② **刻意不動**系統自己寫的自動帶入公式與「打開時重算」設定；③ 上傳端讀格時遇到 `=` `+` `-` `@` 開頭只記一筆警告（表名、格位、前 20 字），內容不改、不擋（負數、條列會誤報） |
| **狀態** | ✅ 已修，1.21.0 出貨（FR-114 CM-2189，主專案 commit `895db0ef8`）。驗證：DEV 四種範本下載 15/15 項通過。打折：以 openpyxl 讀回型別判定，沒用真 Excel 開檔看畫面 |

## M11-21 🟡 改自己的暱稱，就在全公司每份範本裡埋公式 {#m11-21}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #167；OWASP：A05 注入攻擊；主分類為權限提升） |
| **在哪裡** | 合規文件核心模組：所有範本 Excel 都帶一個隱藏的「使用者」下拉分頁，內容是暱稱 |
| **攻擊面位置** | 修改自己個人資料的 API，**任何登入帳號**都能改自己的暱稱，門檻比 M11-20 低得多 |
| **信任邊界位置** | **使用者自填的暱稱 ↔ 全公司下載者的電腦**。暱稱被寫進每份範本的下拉清單，寫出時應標純文字；當時原樣寫入 |
| **元件端點位置** | 種入：jedi-iam `PUT /user-profile/{uid}`（`UserProfileRoute`）；引爆：同 M11-20 的四支範本下載 → `app/module_frame/excel_template/lookup_builder.py` 的 `build_lookup_sheet()`（`_lookup_users` 分頁） |
| **駭客怎麼打** | ① 任一登入帳號把自己的暱稱改成一段公式；② 之後公司裡任何人下載任何一份範本（連空白範本）都帶著它；③ 顧問把範本帶到客戶那裡打開，公式在他的電腦上執行 |
| **得手什麼** | 在全公司任何下載者電腦上執行內容 |
| **修正的做法** | 與 M11-20 同一批檔案一次改完：`lookup_builder.py` 組下拉值與輔助欄時改用 `set_text_cell()`，暱稱存成純文字、值一字不改 |
| **狀態** | ✅ 已修，1.21.0 出貨（FR-114 CM-2189，主專案 commit `895db0ef8`）。驗證：DEV 暱稱暫改 `=1+1`，三種範本的使用者分頁都是純文字（測完已還原） |

## M11-10 🟡 只有建立權限的人走整批匯入就能改掉既有範本 {#m11-10}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #168；OWASP：A01 存取控制失效；主分類為權限提升，另涉竄改資料） |
| **在哪裡** | 合規文件核心模組：範本的「整批匯入」——帶既有範本編號時是覆蓋，不帶時是建新範本。範圍是同一家公司內 |
| **攻擊面位置** | 已登入、**只被授權「建立範本」、沒被授權「修改範本」**的人 |
| **信任邊界位置** | **建立權 ↔ 修改權**。同一件事畫面上一條條改要修改權限，走匯入只要建立權限——兩個入口、兩套門 |
| **元件端點位置** | 主專案 `api/module_frame/__init__.py`：`POST /api/1.0/module-frame/import`（`ModuleFrameImportDataRoute`，`routes/module_frame_import_route.py`）→ `app/module_frame/service/module_frame_import_service.py` 的 `save_import_module_frame_data` |
| **駭客怎麼打** | ① 只有建立權的人（被刻意不給修改權）；② 準備一份整批匯入檔，帶上某份既有範本的編號；③ 系統只查建立權就走覆蓋；④ 既有範本被整份改掉 |
| **得手什麼** | 繞過「不給修改權」的設定，改掉既有範本 |
| **修正的做法** | **決策者裁定只有建立權不能覆蓋既有範本**：`save_import_module_frame_data` 帶既有範本編號時另查 `module-frame.update` 能力點，錯誤碼沿用既有的 `GRC_CAPABILITY_REQUIRED`；同一入口也走歸屬檢查 |
| **狀態** | ✅ 已修，1.21.0 出貨（FR-114 CM-2178，主專案 commit `abf1cf8a4`）。驗證：只有建立權帶編號 403；租戶管理員對子公司範本 403 |

## M18-12 🟡 落地版的商務授權總開關，客戶改一行設定就關掉 {#m18-12}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #202；OWASP：A06 不安全的設計、A02 安全設定錯誤；主分類為權限提升） |
| **在哪裡** | 授權管理模組：判斷「這家客戶買了哪些模組、授權過期沒、子公司數有沒有超過」的總開關 |
| **攻擊面位置** | **客戶自己的主機管理員**。落地版裝在客戶機房，主機權限在客戶手上，設定檔他改得到；設計上這一面就是暴露的 |
| **信任邊界位置** | **客戶主機 ↔ 原廠的商務規則**。授權執法全靠兩個環境變數（`LICENSE_ENFORCEMENT_ENABLED`、`LICENSE_READONLY_GATE_ENABLED`），設成 false 就整套放行；防竄改只核對程式檔、不核對設定值，改了不會觸發任何警示。商務規則的開關不該放在被管的人手上 |
| **元件端點位置** | 主專案 `common/authz/license.py` 的 `enforcement_enabled()`／`readonly_gate_enabled()`（所有模組守門與唯讀攔截都經這兩支）；設定讀取 `config/config.py`；唯讀攔截 `common/middleware/license_readonly_mw.py`；安裝程式 `scripts/installer/install.sh`；診斷包 `app/support/service/diag_bundle_app_service.py` |
| **駭客怎麼打** | ① 客戶的主機管理員打開系統設定檔；② 把授權執法開關改成 false，重啟服務；③ 沒買的模組全部開放、過期照常寫資料、子公司數不設限；④ 原廠端看不到任何警示 |
| **得手什麼** | 不付錢使用沒買的模組、授權過期照寫，受損的是原廠商務收入 |
| **修正的做法** | 決策者裁定落地版拿掉這兩個開關：① `enforcement_enabled()`／`readonly_gate_enabled()` 在 `DEPLOYMENT_MODE=host` 時一律回「要執法」、忽略設定；雲端版（原廠自己管機器）保留當保險絲；② 新增 `disabled_switches_ignored_on_host()`，開機時若發現落地版被設成 false 就記警告，診斷包也回報「曾嘗試關閉」留痕給原廠；③ 安裝程式不再寫這兩行；④ 真要暫時放行，改由原廠補發測試用授權；⑤ 新增 14 條測試，突變拿掉落地版判斷即轉紅 |
| **狀態** | ✅ 已修，1.21.0 出貨（CM-2205，主專案 commit `c8aa7d026`）。驗證：開發環境以落地版＋開關 false 起服務，開機兩筆警告、沒買的模組回 403；雲端版＋false 照舊放行 |

## M24-7 🟡 診斷指令的暫存資料夾名字猜得到、不檢查捷徑 {#m24-7}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #210；OWASP：A01 存取控制失效；主分類為權限提升，另涉竄改資料） |
| **在哪裡** | 主系統的維運指令：客戶主機上用最高權限跑的「匯出診斷包」，會在容器與主機共用的目錄建一個交換資料夾，讓容器把預先收集的紀錄放進來 |
| **攻擊面位置** | **容器裡只有一般權限的人**（例如取得應用程式執行身分的人），能寫那個共用目錄 |
| **信任邊界位置** | **容器一般權限 ↔ 主機最高權限**。主機那一側用猜得到的名字（`.diag-stage-<程序編號>`）建交換資料夾、也不檢查是不是捷徑；最高權限帳號後續照路徑寫檔，等於讓一般權限的人決定最高權限要寫到哪裡 |
| **元件端點位置** | 主專案 `scripts/installer/guidantai` 的診斷段（`mktemp -d`、`_diag_stage_cleanup`）；容器端讀檔 `infra/support/collector/staged_collector.py` 的 `_read_staged`；匯出 API `GET /api/1.0/support/diagnostic-bundle`（`DiagnosticBundleRoute`） |
| **駭客怎麼打** | ① 容器裡的一般權限者預先在共用目錄放一個捷徑，名字取成下次會用到的交換資料夾名；② 維運人員在主機上以最高權限執行「匯出診斷包」；③ 指令照路徑把設定副本、紀錄寫進「交換資料夾」——其實順著捷徑寫到攻擊者指定的地方；④ 等於借最高權限改主機上的檔案（執行者已在自己機器上重現） |
| **得手什麼** | 借主機最高權限改寫主機上的任意檔案 |
| **修正的做法** | ① 交換資料夾改用 `mktemp -d` 隨機名；建立前確認上層存在且不是捷徑；給容器寫的子目錄從 777 改成指定擁有者＋700；找產出檔時只收一般檔、不收捷徑；② 容器端 `_read_staged` 讀檔前解析真實路徑，必須直接落在交換資料夾內，`../`、絕對路徑、指向外面的捷徑一律拒讀；③ **驗收退回再補**：上層目錄可被一般權限改名，對方能把隨機名資料夾改名再換成捷徑——改成建好就 `cd` 進去、之後全用相對路徑寫檔，交給容器前確認原路徑仍指向同一個資料夾，清理只在資料夾編號相符時才刪，不再用完整路徑 `rm -rf` |
| **狀態** | ✅ 已修，1.21.0 出貨（8-D，CM-2214，主專案 commit `d39181f3b`／`046553756`）。打折：主機端只在本機模擬攻擊驗過，真機實跑等出包後在 190 驗 |

## M10-12 ⚪ 更新任務指派時撈不到既有紀錄就跳過管理者檢查 {#m10-12}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #110；OWASP：A10 例外狀況處理不當、A01 存取控制失效；主分類為權限提升） |
| **在哪裡** | 任務與成員管理模組：修改某個任務的被指派人 |
| **攻擊面位置** | 已登入使用者可打的任務指派 API。**今天打不穿**：後面還有一道「這筆指派是否已存在」的檢查頂著，實際結果是「查無資料」而不是「未經授權的寫入」 |
| **信任邊界位置** | **使用者 ↔ 任務指派服務**。「是不是這個專案的管理者」應該在任何寫入前無條件檢查；當時寫成「撈得到既有紀錄才檢查」，撈不到時整段檢查被跳過、繼續往下走，等於把守門交給後面那道剛好擋得住的檢查 |
| **元件端點位置** | `jedi-task-platform/jedi_task_platform/participant/api/routing.py`：`PUT /task-assignee`（`TaskAssigneeRoute.put`）→ `participant/app/service/task_assignee_service.py` 的 `update_task_assignee()` |
| **駭客怎麼打** | ① 任一登入帳號；② 送一個「更新任務指派」的請求，指向一筆根本不存在的指派；③ 程式撈不到既有紀錄，管理者檢查整段跳過；④ 今天靠後面那道檢查回「找不到」；但哪天有人把那支改成「查不到就當新增」，這個請求就會直接寫進去，而且沒有任何守門 |
| **得手什麼** | 今天什麼都得不到；是一道會靜默消失的把關 |
| **修正的做法** | ① 撈不到既有紀錄就**明確拒絕**（回「找不到」，錯誤碼 `GRC_TASK_ASSIGNEE_NOT_FOUND`），不再跳過守門往下走；② 形狀照抄同一支檔案「刪除指派」已驗證過的寫法；③ 程式旁留警語「撈不到就拒絕、不可跳過守門」，防日後改成新增時把把關一起拿掉 |
| **狀態** | ✅ 已修（FR-114.1-3／CM-2023，套件 commit `50c6e0f0`，1.21.0 出貨）。驗證：實跑「更新撈不到被擋、正常更新放行」 |

## M04-14 ⚪ 每次連線都設定、但全系統沒人讀的開關 {#m04-14}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #117；OWASP：A06 不安全的設計；主分類為權限提升） |
| **在哪裡** | 共用基礎：每次開資料庫交易時，告訴資料庫「這個人是誰、能看什麼」的那一段（所有功能共用） |
| **攻擊面位置** | 今天沒有：這個開關完全沒有效果。它的風險在**讀程式的人**——誤以為已經有一道部門層級的檢查，於是不去補真正需要的那道 |
| **信任邊界位置** | **使用者 ↔ 部門層級的資料隔離**。開關名叫「能不能管部門」，每次有身分時都無條件設定，但資料庫 259 條隔離規則沒有一條讀它；真正被讀的是旁邊那個「有條件才設」的手足開關。看起來有守，實際上這條線上沒有守衛 |
| **元件端點位置** | 套件 jedi-common `jedi_common/session/database/db.py` 的 `session_scope()`：`SET LOCAL app.can_manage_orgs` 三行（已刪）；對照的活手足 `set_can_read_all_orgs()` 被出貨基線 `scripts/init/02-schema.sql` 5 條規則讀取 |
| **駭客怎麼打** | ① 今天什麼都打不到；② 風險在之後：有人要做部門層級的權限，看到這個開關以為已經有了；③ 沒去補真正的檢查；④ 部門之間的資料就一直沒有隔開，而大家以為有 |
| **得手什麼** | 今天什麼都得不到；是一個讓人誤判「已經有防護」的假象 |
| **修正的做法** | **拆除了什麼**：① `session_scope()` 裡設定這個開關的三行刪除，旁邊的使用者編號與超級管理員設定不動；② 四份提到它的說明文件同步改掉，避免文件描述不存在的東西；③ 刪除前查證：21 支套件、主專案、前端、測試、代理程式、授權中心、開發環境隔離規則、資料庫自寫程式、出貨基線、全部資料庫異動腳本皆零命中 |
| **狀態** | 🗑️ 裁定刪除，已刪除，1.21.0 出貨（FR-114.3-3，CM-2045，套件 commit `599ab7c5`；文件同步主專案 `2b4e172b3`）。驗證：session 相關 41 條測試通過 |

## M06-14 ⚪ 三支直接拿傳入路徑開檔讀寫的閒置程式 {#m06-14}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #118，與 M06-16 同一件；OWASP：A01 存取控制失效；主分類為竄改資料，另涉資料外洩） |
| **在哪裡** | 稽核流程模組（套件）：流程圖產生器裡「存流程圖到檔案」「匯出 XML」「從 XML 匯入」三支，現在完全沒人呼叫 |
| **攻擊面位置** | 今天沒有——全系統零呼叫。**現在無害的唯一理由是沒人用它** |
| **信任邊界位置** | **呼叫者給的路徑 ↔ 主機檔案系統**。三支都直接拿傳入的路徑開檔讀寫，沒有任何限制；只要哪天有人把使用者輸入接到參數上，就變成任意檔案讀寫 |
| **元件端點位置** | 套件 `jedi_flow_engine/common/utils/bpmn_generator.py` 的 `save_bpmn_to_file`、`export_xml`、`import_xml`（已刪）；同檔保留的 `export_xml_string`／`import_xml_string` 不碰檔案 |
| **駭客怎麼打** | ① 今天打不到；② 若有人日後把某個 API 的參數接到這三支；③ 送一個指向系統設定檔的路徑；④ 讀走或覆寫主機上的任意檔案 |
| **得手什麼** | （若被接上）主機上任意檔案的讀寫 |
| **修正的做法** | **拆除了什麼**：三支死碼整支刪除，連帶移除只給匯入那支用的 `os` 引入；有人在用的「字串版」匯出匯入保留。刪除前查證全 repo 零呼叫 |
| **狀態** | 🗑️ 裁定刪除，已刪除，1.21.0 出貨（隨 CM-2059，套件 jedi-flow-engine commit `4a416ece`） |

## M06-16 ⚪ 接好但沒人用、完全沒有權限檢查的備用服務 {#m06-16}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #118，與 M06-14 同一件；OWASP：A01 存取控制失效；主分類為竄改資料，另涉資料外洩） |
| **在哪裡** | 稽核流程模組：套件提供的「任務新增刪改」服務，主系統組裝好放在那裡備用，**整個程式庫沒有任何地方呼叫它** |
| **攻擊面位置** | 今天沒有。與 M06-14 同一種：只要哪天有人接上去，就憑空多出一組沒有檢查的入口 |
| **信任邊界位置** | **呼叫者 ↔ 任務資料**。套件本身零守門是設計（套件不知道誰有權限，那是用它的人要決定的），防線只能在主系統這層補——而這組服務沒經過任何主系統的守門 |
| **元件端點位置** | 主專案 `di_containers/flow_engine/workflow_excution_containers.py` 註冊的 `job_execution_service`（`jedi_flow_engine.app.service.job_execution_service.JobExecutionService`，已拆） |
| **駭客怎麼打** | ① 今天打不到；② 若日後有人在路由上直接注入這支服務；③ 任何登入者就能新增、修改、刪除任意任務執行資料，沒有歸屬也沒有權限檢查 |
| **得手什麼** | （若被接上）任意改寫任務執行資料 |
| **修正的做法** | **拆除了什麼**：拆掉沒人注入的 `job_execution_service` 註冊與其引入（全庫只剩容器自己引用）；它底下的 domain service 仍被主系統的流程服務用到，保留 |
| **狀態** | 🗑️ 已拆除，1.21.0 出貨（8-E，CM-2215，主專案 commit `822216b6c`）。驗證：後端起得來、各端點正常 |

## M10-18 ⚪ 沒有對外入口、也沒有把關的任務指派功能 {#m10-18}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #120；OWASP：A01 存取控制失效；主分類為權限提升） |
| **在哪裡** | 任務與成員管理模組：任務指派服務裡幾支查詢與配套功能。模組頁沒有逐條表，此條依總表描述與拆除 commit 寫成 |
| **攻擊面位置** | 今天沒有：沒有任何路由、前端、DI 容器呼叫。與 M10-7、M10-8 同一種——只要哪天有人接上去，就多一組沒有把關的入口 |
| **信任邊界位置** | **呼叫者 ↔ 任務指派資料**。這幾支直接回某專案、某使用者的指派清單與待辦佇列，不問呼叫者是誰；守門只能由接上它的那一層補，而它們從沒被接過——其中一支是被主專案新版取代後留下的舊版，不是「還沒接」 |
| **元件端點位置** | 套件 `jedi_task_platform/participant/app/service/task_assignee_service.py` 的 `get_task_assignee_menu()`、`get_task_assignees_with_inherits()`、`get_user_task_queue()`（舊版），連同下游 `TaskAssigneeDomainService.get_user_task_queue()`、`ITaskAssigneeRepo.get_user_task_queue()`、`TaskAssigneeRepoImpl.get_user_task_queue()`（皆已刪）；新版是主專案 `infra/readmodel/tasks/my_grc_jobs_query.py` 的 `get_user_task_queue_by_sp()` |
| **駭客怎麼打** | ① 今天打不到；② 若日後有人把這幾支接上 API；③ 任何登入者帶別人的專案或使用者編號呼叫；④ 拿到那個專案的指派清單或那個人的待辦佇列 |
| **得手什麼** | （若被接上）別人專案的任務指派與別人的待辦清單 |
| **修正的做法** | **拆除了什麼**：① 三支零呼叫的查詢整支刪除，連同下游整條孤兒鏈；② **留下第四支**「依專案刪掉所有指派」`delete_by_project_id()`——六層成員表都有同名方法，專案刪除時逐層清理要用到；③ 前端快速配置在用的批次指派與四支活路由方法原樣保留 |
| **狀態** | 🗑️ 裁定刪除，已刪除，1.21.0 出貨（CM-2044，套件 commit `9c9029fd`） |

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

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

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

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

## M04-13 ⚪ 資料庫隔離設定值用字串拼進查詢語句 {#m04-13}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #141；OWASP：A05 注入攻擊；主分類為權限提升） |
| **在哪裡** | 共用基礎：每個資料庫交易開始時，把「這個人是誰、能看哪些客戶與部門」寫進資料庫的隔離設定 |
| **攻擊面位置** | 目前**沒有**外部可控的輸入走到這裡。三個值（使用者編號、客戶路徑、部門路徑）都由系統自己產生，部門路徑根本沒人填 |
| **信任邊界位置** | **我們的程式 ↔ 資料庫的隔離規則**。安全完全依賴「沒有人把外部輸入帶進這條路」這個當下的事實；哪天有人加一條新路徑把外部輸入帶進來，漏洞就成立，而且加的人不會知道 |
| **元件端點位置** | jedi-common `jedi_common/session/database/db.py` 的 `session_scope()`：`SET LOCAL app.user_id／app.allowed_tenant_paths／app.allowed_org_paths` 三句 f-string；值來自 `jedi_iam/middleware/context.py`（登入身分的客戶路徑）與 `jedi_common/session/auth/tenant_context.py`（機器情境） |
| **駭客怎麼打** | 現況打不進來。成立條件是：① 未來某條新路徑讓外部輸入影響客戶或部門路徑的值；② 攻擊者在那個值裡放一個單引號與額外語句；③ 隔離設定被改寫，例如把可見範圍改成全部客戶 |
| **得手什麼** | 現況無；條件成立時是跨客戶讀寫 |
| **修正的做法** | 不修的理由：三個值目前都是乾淨的，部門路徑沒人填。而且這不是順手能改的——這個資料庫指令**不支援把語句和值分開傳**，真正的改法是改呼叫資料庫內建的設定函式，並確認隔離規則仍讀得到值，這正是當初這段改很久的原因 |
| **狀態** | 🚫 裁定不修（無工單） |

## M05-14 ⚪ 沒買問卷模組，直接打網址照樣能用「填答」那一半 {#m05-14}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #186；OWASP：A01 存取控制失效；主分類為權限提升） |
| **在哪裡** | 問卷模組：任務問卷的「填答」那一半——列任務問卷、讀寫答案、看與還原歷史版本、匯入答案；另有兩個入口同病：AI 儀表板查問卷、規劃頁把問卷掛上任務 |
| **攻擊面位置** | **沒買問卷模組的客戶**的任何登入帳號。選單會反灰，但網址本身打得到 |
| **信任邊界位置** | **客戶 ↔ 原廠的商務授權**。「設計問卷」那半 33 個入口有 32 個問了「這家有沒有買問卷」，「填答」那半 14 個一個都沒問。每個入口仍有問卷歸屬檢查，所以看不到別家的東西——這是商務授權被繞過，不是資料外洩 |
| **元件端點位置** | 套件 jedi-survey `api/routing.py` 的 `mount_answer_routes()`：`POST /api/1.0/task-surveys`、`GET/PUT /task-survey/{uid}`、`/task-survey/import-template/{task_uid}`、`/task-survey/import-answers`、`/task-survey/configured/{uid}`、`GET/PUT /project-survey/answers/{uid}`、`…/patch`、`…/checkpoint`、`/project-survey/histories`、`/histories/menu/{uid}`、`/history-details`、`/history/revert`；規劃頁建任務 `POST /api/1.0/grc/project/{project_uid}/assessment-object/{ao_uid}/jobs`、改任務 `PUT /api/1.0/grc/project/{project_uid}/job/{job_uid}`（`app/flow_control/service/job_service.py`）；AI 儀表板查問卷 `di_containers/dashboard_apis/survey.py` |
| **駭客怎麼打** | ① 沒買問卷的客戶任一帳號登入；② 照有買的客戶畫面上的網址，直接送列任務問卷、填答、匯入答案的請求；③ 系統只驗登入與問卷歸屬就放行；④ 或在規劃頁建任務時把任務類型填成「問卷」，或透過 AI 儀表板查問卷——沒買的功能照用 |
| **得手什麼** | 不付錢使用加購的問卷填答功能，受損的是原廠商務收入 |
| **修正的做法** | 三個入口分三刀：① **填答那半**：14 支方法各在「必須登入」下一行加 `@require_license("survey")`，只加裝飾器不動內容；② **規劃頁建／改任務**：`job_service` 新增 `_assert_job_type_licensed()`，任務類型必須在這家客戶已買的範圍內，清單與匯入那條共用 `licensed_job_types()`；③ **AI 儀表板查問卷**：套件讓每支查詢能申報 `required_license`，問卷兩支申報 `survey`，主專案注入 `viewer_module_licensed` 檢查，沒買就拒絕 |
| **狀態** | ✅ 已修，1.21.0 出貨（CM-2194：套件 commit `0ced8e15`、主專案 `02bc1179c`；AI 儀表板入口 CM-2220：主專案 `80dedfbb5`、套件 `fb271590`）。驗證：以替身拿掉授權快照裡的問卷，14 支全 403 `LICENSE_403001` |

## M18-11 ⚪ 授權過期轉唯讀只靠每天一次的排程 {#m18-11}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #213；OWASP：A06 不安全的設計；主分類為權限提升） |
| **在哪裡** | 授權管理模組：授權到期後產品轉唯讀 |
| **攻擊面位置** | **授權已過期的客戶**的任何登入帳號，在排程下一次跑完之前；若部署方式讓排程沒在跑，就一直有效 |
| **信任邊界位置** | **時間 ↔ 寫入權限**。「過期即唯讀」應該在每次判斷寫入權限時當場算；當時讀的是資料庫裡存的狀態，而那個狀態要等每天一次的排程（台灣時間早上 10 點）才更新 |
| **元件端點位置** | 主專案 `common/authz/license.py` 的 `LicenseSnapshot` 與 `viewer_is_readonly()`；唯讀攔截 `common/middleware/license_readonly_mw.py`；排程 `core/scheduler.py` 的 `_license_expiry_state_machine_tick()`；到期判斷純函式在套件 jedi-license-runtime `domain/service/license_expiry_state_machine.py` 的 `compute_target_status()` |
| **駭客怎麼打** | ① 客戶的授權在某天午夜過期；② 排程還沒跑，資料庫裡的狀態仍是「正常」；③ 客戶照常新增、修改資料，最長將近一天；④ 若排程沒在跑，就一直可寫 |
| **得手什麼** | 授權過期後繼續寫入資料 |
| **修正的做法** | 決策者裁定「過期即唯讀」：① `LicenseSnapshot` 多帶到期日與寬限規則，新增 `effective_status()`，用排程同一支 `compute_target_status()` 當場按時間重算；② 與資料庫存的狀態取較嚴的那個——存的已是唯讀就仍唯讀，不因重算放寬；③ `viewer_is_readonly()` 改看它；④ 排程照留，負責把狀態寫進資料庫給清單、報表與寄通知用 |
| **狀態** | ✅ 已修，1.21.0 出貨（8-G，CM-2217，主專案 commit `3ed47c7a9`）。驗證：新增「剛過期 5 分鐘、資料庫仍正常 → 唯讀」等 3 條測試，突變轉紅；開發環境以實際租戶授權把時間推到到期後驗（未改授權資料，屬打折） |

## M18-13 ⚪ 過期唯讀的兩個縫：代理程式網址整段放行、問卷即時填答不經檢查 {#m18-13}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #216；OWASP：A01 存取控制失效；主分類為權限提升） |
| **在哪裡** | 授權管理模組：過期轉唯讀後，全域擋寫入的那道攔截 |
| **攻擊面位置** | **授權已過期的客戶**：管理員還能產生新的代理程式註冊碼；任何能填問卷的人還能透過即時填答通道寫入答案 |
| **信任邊界位置** | **唯讀攔截 ↔ 兩個例外通道**。代理程式回報結果是機器對機器，該放行；但當時把整段 `/agents/` 都放行，連管理員產註冊碼也一起放掉。問卷即時填答走另一種連線（SocketIO），本來就不經過網頁請求那道攔截，套件裡也沒有自己的唯讀檢查 |
| **元件端點位置** | 主專案 `common/middleware/license_readonly_mw.py` 的 `_EXEMPT_PREFIXES`／`_should_block()`；被誤放行的是 jedi-remote-agent `POST /api/1.0/agents/enroll-token`、`/agents/enroll-token/revoke`；問卷即時通道是套件 jedi-survey `app/handler/fill_survey_socketio_handler.py`（命名空間 `/socket/fill-survey`）的 `on_update`，主專案接線在 `core/plugins/survey.py` |
| **駭客怎麼打** | ① 客戶授權過期，產品轉唯讀；② 管理員照常按「產生註冊碼」，因為整段代理程式網址被放行；③ 填問卷的人打開問卷，透過即時共編通道送答案更新，不經過唯讀攔截就寫進資料庫 |
| **得手什麼** | 唯讀期間仍能註冊新代理程式、寫入問卷答案 |
| **修正的做法** | ① 放行範圍收窄成代理程式自己打的三段（`/agents/register`、`/agents/heartbeat`、`/agents/tasks/`），產生與撤銷註冊碼不再放行；② 套件 jedi-survey 新增選填接口 `readonly_check`，`on_update` 在寫入前問它，唯讀就不寫、只回一個錯誤事件（`LICENSE_403002`，與網頁唯讀攔截同碼）；只做共編狀態廣播、不寫資料庫的事件不擋；③ 主專案把 `viewer_is_readonly` 接到這個接口 |
| **狀態** | ✅ 已修，1.21.0 出貨（8-G，CM-2217，主專案 commit `3ed47c7a9`、套件 `8ccfac73`）。驗證：產／撤註冊碼唯讀下被擋，代理程式三段照放；問卷唯讀擋、可寫放行，突變轉紅 |

## M24-12 ⚪ 驗證雲端硬碟應用程式憑證只查是不是平台管理員 {#m24-12}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #218；OWASP：A01 存取控制失效；主分類為權限提升） |
| **在哪裡** | 主系統的雲端硬碟整合：系統設定 → 雲端硬碟 → 「驗證應用程式憑證」按鈕 |
| **攻擊面位置** | **平台管理員群組裡、沒被分到「儲存設定」權限的人**。門檻是平台管理員身分 |
| **信任邊界位置** | **平台管理員 ↔ 「儲存設定」這項功能權限**。設定頁本身要求儲存設定權限；驗證按鈕只問「是不是平台管理員」，沒問這一項，兩個入口兩套門 |
| **元件端點位置** | 主專案 `api/cloud_integration/__init__.py`：`POST /api/1.0/integrations/google-drive/verify-credentials`（`DriveAppCredentialVerifyRoute`）→ `app/cloud_integration/service/drive_app_credential_verify_service.py` 的 `verify()` |
| **駭客怎麼打** | ① 平台管理員群組裡一個沒被分到儲存設定權限的人；② 直接送驗證請求，帶上任意一組 Google 應用程式憑證；③ 系統只確認他是平台管理員，就拿去向 Google 驗證並回報對錯；④ 他可以拿這支功能反覆試憑證 |
| **得手什麼** | 不該碰儲存設定的人能用系統替他試憑證對錯 |
| **修正的做法** | ① `verify()` 在平台管理員檢查之後再疊一道 `viewer_has_capability("storage-config.read")`，與設定頁同一顆權限；② 沒有就回 403 `GRC_403022`，**擋在打 Google 之前**；③ 新增「平台管理員但沒有這項權限 → 403 且不打 Google」測試，突變拿掉檢查即轉紅 |
| **狀態** | ✅ 已修，1.21.0 出貨（8-A，CM-2211，主專案 commit `fe928859f`）。驗證：租戶管理員 403、持權限的平台管理員 200 照常 |

## M06-26 ⚪ 匯出任務 Excel 時欄位被當成公式 {#m06-26}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #222；OWASP：A05 注入攻擊；主分類為權限提升） |
| **在哪裡** | 稽核流程模組：稽核計畫底下的任務清單匯出成 Excel，改完再匯回 |
| **攻擊面位置** | 能編輯任務欄位（名稱、說明、負責人等）的專案成員 |
| **信任邊界位置** | **我們的資料 ↔ 下載者的電腦**。十個欄位原樣寫進儲存格，`=` `+` `-` `@` 開頭就被當公式；應該在寫出時標純文字 |
| **元件端點位置** | jedi-task-platform `task/api/routing.py`：`GET /project/{project_uid}/ap/{ap_uid}/jobs/export`（`JobExportRoute`）→ 主專案 `app/flow_control/service/job_import_service.py` 的 `export_excel()` |
| **駭客怎麼打** | ① 專案成員在任務欄位填一段公式；② 有人匯出任務 Excel 打開；③ 按下「啟用內容」，那段指令在他自己的電腦上執行 |
| **得手什麼** | 在下載者電腦上執行內容 |
| **修正的做法** | ① 十欄改走 jedi-common 的 `set_text_cell()`（沿用 CM-2189 那支，不另寫）；② 這份檔會被匯回，用「強制文字」旗標而不加單引號，值不變；③ 欄位順序抽成 `_EXPORT_COLUMNS`，與匯入依位置讀取對齊 |
| **狀態** | ✅ 已修，1.21.0 出貨（8-C，CM-2213，主專案 commit `a5804a990`／`bde8f9d16`）。驗證：DEV 真實專案匯出 60 列 0 個公式格 |

## M15-7 ⚪ AI 儀表板生成只檢查有沒有買模組，不檢查角色權限 {#m15-7}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #234；OWASP：A01 存取控制失效；主分類為權限提升） |
| **在哪裡** | AI 儀表板模組：「生成儀表板」功能 |
| **攻擊面位置** | 已買 AI 儀表板模組的客戶裡，**被客戶管理員刻意沒開這項權限的角色**（例如稽核人員）。選單上看不到，網址照樣打得到 |
| **信任邊界位置** | **使用者 ↔ 「AI 儀表板」這項功能權限**。生成端點只問「這家有沒有買」，沒問「這個人的角色有沒有這項權限」——客戶的權限設定在這裡形同虛設，而且每用一次要付兩次外部 AI 費用，由原廠吸收 |
| **元件端點位置** | 套件 jedi-ai-dashboard：`POST /api/1.0/ai-dashboard/auto-generate`（`api/routes/ai_dashboard_route.py`，`AUTO_GENERATE_URL`）、守門 `api/guards.py` 的 `require_capability()`、接線契約 `plugin/contract.py`；主專案接線 `core/plugins/ai_dashboard.py` |
| **駭客怎麼打** | ① 一個角色沒有「AI 儀表板」權限的帳號（例如稽核人員）登入；② 選單上看不到這個功能，但直接送生成請求；③ 系統只確認這家有買就開始生成，連打兩次外部 AI；④ 反覆送，費用由原廠吸收（已實測） |
| **得手什麼** | 繞過客戶的角色權限設定使用 AI 儀表板，並讓原廠持續付 AI 費用 |
| **修正的做法** | ① 套件新增接線欄位 `capability_required`，列為必填——主系統不接就拒絕掛載，不會有人忘了接；② 生成端點在「有沒有買」之後疊 `@require_capability("ai-dashboard.read")`，權限名沿用既有那一顆、不另造；③ 主專案把 `common.authz.require_capability` 接上；④ 突變拿掉路由上的權限檢查即轉紅 |
| **狀態** | ✅ 已修，1.21.0 出貨（8-J，CM-2220，主專案 commit `80dedfbb5`、套件 `fb271590`）。驗證：沒有權限的稽核人員帳號 403 `GRC_403022`，有權限的帳號過守門（本機無 AI 金鑰，未走到真正生成，屬打折） |
