---
title: S 冒充身分
eyebrow: Guidant AI 資安檢視總報告 · STRIDE 威脅分類
h1: S 冒充身分（Spoofing）
lede: 命中這一類的 **22 件**，還原成模組頁的 **24 條原始條目**逐條展開。每一條用白話寫出：在哪裡、攻擊面、信任邊界、元件端點、駭客怎麼打、得手什麼、修正的做法。
chips:
  - { text: "🔴 2", kind: crit }
  - { text: "🟠 11", kind: warn }
  - { text: "🟡 4", kind: accent }
  - { text: "⚪ 7", kind: plain }
  - { text: "已修 22／不修 2", kind: ok }
---

## 這一類是什麼

**假裝成別人或別台機器**。STRIDE 六類裡它破壞的是「身分驗證」這個性質——系統以為在跟 A 講話，其實是 B。依微軟的定義：非法取得並使用別人的身分驗證資訊（例如帳號密碼），或讓系統把不合法的來源當成合法的。

判定時問的是：**攻擊者打下這一條之後，能不能假裝成別人或別台機器？** 兩件最嚴重的問題都在這一類：代理程式不驗身分（整台機器可被冒充）、忘記密碼直接回傳重設鑰匙（任何帳號可被接管）。

這次的 24 條在本專案長成四種形狀：

- **該問「你是誰」的地方沒問**（M01-1、M13-1、M13-2、M13-3、M13-5、M13-6、M18-3、總表#137）：代理程式通道、忘記密碼、公司帳號登入、綁定外部帳號、Google 登入、雙因子驗證碼、開通序號——都是「確認身分」本身的那道門。有的整個沒做，有的做了但能被猜、被繞、被匿名通過。
- **把別人送來的身分當真**（M05-9、M06-2、M24-10、M24-11、M06-27）：問卷署名採用畫面送上來的名字、留言推播看起來像同事發的、授權連結轉寄出去任何人都能按同意、通行碼可用時間差慢慢猜、通知信把暱稱原樣塞進信件。系統沒有冒充的意圖，但它替冒充者背書。
- **連線加密卻不確認對方是誰**（M16-3、M23-4、M04-6、M18-7）：連郵件伺服器、GitHub、暫存資料庫時，傳輸有加密但沒驗對方憑證，或三個環境的公鑰並列、開發環境簽的正式環境也認——站在中間的人可以假扮成那台伺服器。
- **鑰匙外流，拿到就能以系統的名義行事**（M15-5、M16-6、M17-6、M22-1、M02-4、M03-13、M24-1）：程式碼平台通行證、登入憑證簽章金鑰寫進了版控；子公司管理員改得到全公司的登入規則；上傳的網頁檔與授權回呼頁讓攻擊者的程式用受害者的登入身分跑起來。

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

> **為什麼是 24 條不是 22 件**：[總結](../SUMMARY.html)問題總表有 2 件是同一個洞被兩個模組各撈到一次，總表併成一件：#13（M02-4＋M03-13）、#122（M16-6＋M17-6）。本頁拆回模組頁原始條目，每條都列。標題括號內是總表編號，可回去對照；總表#137 只出現在原始掃描總表、模組頁沒有逐條表，以總表編號為標題。

## 條目清單

| # | 條目 | 嚴重度 | 出自 | 狀態 |
|---:|---|---|---|---|
| 1 | [M01-1 代理程式五條通訊管道全部不檢查對方是誰](#m01-1) | 🔴 最嚴重 | [遠端代理程式](../M01-remote-agent.html) | ✅ 已修 |
| 2 | [M13-1 忘記密碼把重設鑰匙直接回傳給任何人](#m13-1) | 🔴 最嚴重 | [登入與權限](../M13-iam.html) | ✅ 已修 |
| 3 | [M06-2 流程留言的讀與寫都只檢查有沒有登入](#m06-2) | 🟠 高 | [稽核流程](../M06-flow-engine.html) | ✅ 已修 |
| 4 | [M15-5 程式碼平台通行證四把寫在版控文件裡](#m15-5) | 🟠 高 | [AI 儀表板](../M15-ai-dashboard.html) | ✅ 已修 |
| 5 | [M02-4 上傳的網頁檔，別人點預覽時被當成程式執行](#m02-4) | 🟠 高 | [檔案上傳下載](../M02-file-upload.html) | ✅ 已修 |
| 6 | [M03-13 掃描報告「檔名叫什麼就存什麼」，走同一條預覽路徑](#m03-13) | 🟠 高 | [弱點檢測整合](../M03-detection.html) | ✅ 已修 |
| 7 | [M22-1 子公司管理員能改全公司的登入規則與帳號目錄](#m22-1) | 🟠 高 | [系統設定](../M22-system-core.html) | ✅ 已修 |
| 8 | [M13-2 公司帳號登入：匿名能過、帳號字串能騙查詢、加密連線不認人](#m13-2) | 🟠 高 | [登入與權限](../M13-iam.html) | ✅ 已修 |
| 9 | [M13-3 綁定外部帳號不確認是本人](#m13-3) | 🟠 高 | [登入與權限](../M13-iam.html) | ✅ 已修 |
| 10 | [M13-5 用 Google 登入是沒接通的空殼](#m13-5) | 🟠 高 | [登入與權限](../M13-iam.html) | ✅ 已修 |
| 11 | [M13-6 雙因子驗證碼可以無限次猜](#m13-6) | 🟠 高 | [登入與權限](../M13-iam.html) | ✅ 已修 |
| 12 | [M18-3 開通序號只有 8 個字，猜得到別人的授權](#m18-3) | 🟠 高 | [授權管理](../M18-license.html) | ✅ 已修 |
| 13 | [M24-1 雲端硬碟授權回呼頁，點一個連結就被偷走登入身分](#m24-1) | 🟠 高 | [主系統自己的程式](../M24-host.html) | ✅ 已修 |
| 14 | [M05-9 多人同時填問卷時，署名採用畫面送上來的名字](#m05-9) | 🟡 中 | [問卷](../M05-survey.html) | ✅ 已修 |
| 15 | [M16-3 連郵件伺服器有加密但不認人](#m16-3) | 🟡 中 | [寄信與通知](../M16-notification.html) | ✅ 已修 |
| 16 | [M18-7 三個環境的公鑰並列，開發環境簽的照正式也認](#m18-7) | 🟡 中 | [授權管理](../M18-license.html) | 🚫 不修 |
| 17 | [M23-4 連 GitHub 時把憑證驗證關掉](#m23-4) | 🟡 中 | [意見回饋](../M23-issue.html) | ✅ 已修 |
| 18 | [M16-6 登入憑證簽章金鑰、資料庫密碼、儲存金鑰同在一個測試設定檔](#m16-6) | ⚪ 低 | [寄信與通知](../M16-notification.html) | ✅ 已修 |
| 19 | [M17-6 登入憑證簽章金鑰明文躺在對話紀錄裡](#m17-6) | ⚪ 低 | [公告](../M17-bulletin.html) | ✅ 已修 |
| 20 | [總表#137 系統自動產生的密碼比規定少一個字元](#r137) | ⚪ 低 | [總表](../SUMMARY-owasp-stride.html#r137) | ✅ 已修 |
| 21 | [M04-6 連暫存資料庫時寫死不確認對方身分](#m04-6) | ⚪ 低 | [共用基礎](../M04-common.html) | 🚫 不修 |
| 22 | [M24-10 雲端硬碟授權連結沒綁定發起的瀏覽器](#m24-10) | ⚪ 低 | [主系統自己的程式](../M24-host.html) | ✅ 已修 |
| 23 | [M24-11 雲端硬碟通知的通行碼比對可用時間差慢慢猜](#m24-11) | ⚪ 低 | [主系統自己的程式](../M24-host.html) | ✅ 已修 |
| 24 | [M06-27 通知信把暱稱原樣塞進信件，可放釣魚連結](#m06-27) | ⚪ 低 | [稽核流程](../M06-flow-engine.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 項 |

## 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`）。已核對：該功能成功與失敗都回同一個空白結果 |

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

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

## M02-4 🟠 上傳的網頁檔，別人點預覽時被當成程式執行 {#m02-4}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #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；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`／空字串退回預設名 |

## 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 實打兩種部署模式 |

## 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`）依決策者裁定把**新建設定的「驗證伺服器憑證」改成預設不勾**——後端欄位預設仍是驗；客戶要自己勾才生效 |

## 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-5 🟠 用 Google 登入是沒接通的空殼 {#m13-5}

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

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

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

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

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

## 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 違規 |

## M05-9 🟡 多人同時填問卷時，署名採用畫面送上來的名字 {#m05-9}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #85；OWASP：A06 不安全的設計、A08 軟體或資料完整性失效；主分類為事後無法追查） |
| **在哪裡** | 問卷模組：多人同時線上填答，每次修改即時同步給其他填答者，並記下「這一題是誰改的」 |
| **攻擊面位置** | 已登入、本來就能填這份問卷的**合法填答者**——這不是越權，問題在紀錄可信度 |
| **信任邊界位置** | **瀏覽器 ↔ 即時同步服務**。「是誰」應該取伺服器手上的登入身分；當時直接採用瀏覽器送上來的 `user` 欄位當建立者與修改者 |
| **元件端點位置** | 即時通道 `/socket/fill-survey` 的 `update` 事件 → `jedi_survey/app/handler/fill_survey_socketio_handler.py` 的 `on_update` |
| **駭客怎麼打** | ① 合法填答者打開問卷，進入即時填答；② 修改某一題時，把送出的資料裡「使用者」那一欄改成同事的帳號；③ 伺服器照收，這筆修改記成同事改的，廣播給其他人看到的也是同事的名字；④ 事後追查「誰改了這一題」，指向的是沒做這件事的人 |
| **得手什麼** | 把自己的填答修改署成別人的名字，填答歷程不可信 |
| **修正的做法** | ① 署名改讀伺服器端的登入身分 `get_user_context().login_name`（即時通道每個事件都會注入身分，REST 端的修改本來就是這樣取），不再採用畫面送上來的值；② 廣播給其他填答者的使用者欄位跟著變成真實帳號；③ 同批另修同模組資料夾的三處輸入信任問題（總表 #86／#126／#127） |
| **狀態** | ✅ 已修（CM-2060，套件 jedi-survey `54ac36f7`，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`）依決策者裁定把**新建設定改成預設不勾**、拿掉未勾時的風險提示——報告原寫的「新建預設驗證、設定頁頂部提示建議開啟」已不是現況；現在要客戶自己勾才會驗 |

## M18-7 🟡 三個環境的公鑰並列，開發環境簽的照正式也認 {#m18-7}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #99；OWASP：A08 軟體或資料完整性失效、A04 加密機制失效、A02 安全設定錯誤） |
| **在哪裡** | 授權管理模組：產品驗授權檔簽章時用的公鑰清單 |
| **攻擊面位置** | 授權檔上傳與啟用 API；前提是**手上要有我們某一台內部簽發站的私鑰**（目前三把都在自家機器上） |
| **信任邊界位置** | **各環境簽發站 ↔ 產品的驗章**。驗章是照檔上的代號去清單查公鑰，而開發、STG、POC 三個環境的公鑰並列在同一份編進產品的清單——任何一個環境簽的照，其他環境都承認 |
| **元件端點位置** | `jedi_license_runtime/common/public_keys.py` 的 `PUBLIC_KEYS`；`common/engine.py` 查公鑰；入口 `POST /api/1.0/license/upload`、`/license/activation/upload`、`/license/activation/online`；出貨守門 `scripts/build/build_bundle.sh` 的正式公鑰檢查 |
| **駭客怎麼打** | ① 取得開發環境簽發站的私鑰（例如某台開發機外流）；② 自己簽一張全功能、永不到期的授權檔；③ 上傳到正式環境；④ 產品照代號在清單裡找到開發環境公鑰，驗章通過 |
| **得手什麼** | 用開發環境的私鑰簽出正式環境認得的授權檔，改寫自己的授權內容 |
| **修正的做法** | **不修的理由**：三把公鑰對應三台內部簽發站、私鑰都在內部，目前沒有正式簽發站也沒有外部客戶，要成立得先拿到我們機器上的私鑰。**失效條件**：交付首位客戶前，出貨版改成只認正式簽發站的公鑰（與正式簽章鑰一起處理）。**附帶修掉的一件**：打包流程的正式公鑰守門檢查的是搬遷後已不存在的舊路徑，改成動態解析套件安裝路徑（CM-1581） |
| **狀態** | 🚫 裁定不修（已裁記錄，交付首位客戶前必做）；守門路徑修正 CM-1581，主專案 commit `4b5277046` |

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

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

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #122；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；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`） |

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

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

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

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #139；OWASP：A04 加密機制失效、A07 身分驗證失效；主分類為資料外洩） |
| **在哪裡** | 共用基礎模組：主系統連「暫存資料庫」（快取伺服器，放 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`）刪除 |

## M24-10 ⚪ 雲端硬碟授權連結沒綁定發起的瀏覽器 {#m24-10}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #217；OWASP：A01 存取控制失效） |
| **在哪裡** | 主系統的雲端硬碟整合：管理員按「連線 Google 硬碟」產生授權連結，跳到 Google 同意後回到我們的回呼網址 |
| **攻擊面位置** | **別家公司的人**——只要被騙按下一個轉寄來的授權連結並同意 |
| **信任邊界位置** | **發起授權的那個瀏覽器 ↔ 實際按同意的人**。回呼只認網址上的狀態碼，不確認回來的是不是當初發起的那個瀏覽器；而狀態碼就印在被轉寄出去的連結上 |
| **元件端點位置** | 主專案 `api/cloud_integration/__init__.py`：`POST /api/1.0/integrations/google-drive/auth-url`（`GoogleDriveAuthUrlRoute`，發起）、`GET /api/1.0/integrations/google-drive/callback`（`GoogleDriveCallbackRoute`，回呼）→ `app/cloud_integration/service/google_drive_integration_service.py` 的 `build_auth_url`、`handle_callback` |
| **駭客怎麼打** | ① 攻擊者在自己公司按「連線 Google 硬碟」，拿到授權連結；② 把連結轉寄給別家公司的人，騙他按同意；③ 對方登入自己的 Google 帳號同意；④ **對方的硬碟被接進攻擊者的公司**，對方放進去的檔案攻擊者全看得到，還會被同步成攻擊者公司的證據 |
| **得手什麼** | 把別人的雲端硬碟接進自己公司，讀取並匯入對方的檔案 |
| **修正的做法** | **決策者裁定綁瀏覽器**：① 發起時另產一個與狀態碼無關的隨機值，明文寫進只有那個瀏覽器帶得回來的 HttpOnly cookie，伺服器只存雜湊——偏離卡上建議（「cookie 放狀態碼雜湊」），因為狀態碼就在轉寄出去的連結上，收到的人自己算雜湊就過了；② 回呼時查到狀態碼先作廢（對不上也作廢，轉寄出去的連結不能重試），再用定時比對驗雜湊；缺 cookie、不符、舊格式一律回新錯誤碼 `GRC_400134`，不換票、不寫表；③ cookie 用 SameSite=Lax（Google 跳回來是跨站導覽，Strict 會擋掉正常流程），路徑限回呼網址；④ 回呼頁不論成敗都清掉 cookie |
| **狀態** | ✅ 已修，1.21.0 出貨（8-H，CM-2218，主專案 commit `a671d0b64`）。驗證：不帶或帶錯 cookie 回 nonce_mismatch、同一連結再帶正確 cookie 已作廢；突變把比對改成恆通過 4 支測試轉紅。打折：本機無真 Google，沒走到真的換票與寫表 |

## M24-11 ⚪ 雲端硬碟通知的通行碼比對可用時間差慢慢猜 {#m24-11}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #220〔另見總表第 196 項〕；OWASP：A04 加密機制失效） |
| **在哪裡** | 主系統自己的程式：接收 Google 雲端硬碟「有檔案異動」通知的入口，靠一組通行碼確認通知真的來自 Google |
| **攻擊面位置** | **不需要登入**——這個入口設計上就對外開放給 Google 呼叫，只靠請求標頭裡的通行碼把關。要大量、精準量測回應時間，實務上很難 |
| **信任邊界位置** | **網際網路 ↔ 我們後端**。通行碼比對就是這條線上的守衛；一般的字串比對遇到第一個不同的字就停，「對了越多字、回得越慢」，理論上能一個字一個字猜出來 |
| **元件端點位置** | `POST /api/1.0/webhooks/google-drive/{tenant_id}`（`api/cloud_integration/routes/google_drive_webhook_route.py` 的 `GoogleDriveWebhookRoute`）→ `app/cloud_integration/service/google_drive_webhook_service.py` 的通行碼與頻道比對 |
| **駭客怎麼打** | ① 不需帳號，對通知入口大量送請求，每次換一個通行碼的開頭；② 精準量測每次回應的時間差，找出「比較慢」的那個字；③ 一個字一個字往下猜；④ 猜中後就能偽造「硬碟有異動」通知，讓系統一直多跑同步 |
| **得手什麼** | 偽造雲端硬碟異動通知，讓系統白做同步工作 |
| **修正的做法** | ① 通行碼與頻道編號的比對改成**固定時間比對**（`hmac.compare_digest`），不論對幾個字花的時間都一樣；② 首腦退回補一個洞：字串版比對遇到中文標頭會拋錯，改成先轉成位元組再比的 `_same_secret()`，任何轉換錯誤一律回「不符」；③ 資料庫裡沒設通行碼的租戶一律拒絕，空標頭不能對上未接通的租戶 |
| **狀態** | ✅ 已修，1.21.0 出貨（8-C，CM-2213，commit `a5804a990`／`bde8f9d16`）。驗證：錯誤通行碼打通知入口仍被丟棄 |

## M06-27 ⚪ 通知信把暱稱原樣塞進信件，可放釣魚連結 {#m06-27}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #223；OWASP：A05 注入攻擊） |
| **在哪裡** | 稽核流程模組：三種系統通知信——批次完成任務彙總、批次指派任務、專案啟動 |
| **攻擊面位置** | 修改自己個人資料的 API，任何登入帳號都能改暱稱；專案名稱則由能編輯專案的人控制 |
| **信任邊界位置** | **使用者自填的文字 ↔ 收信人的信件軟體**。信件是網頁格式，暱稱與專案名稱原樣放進去就會被當成網頁語法；應該放進信件前先轉成純文字 |
| **元件端點位置** | 種入：jedi-iam `PUT /user-profile/{uid}`；觸發：jedi-task-platform `POST /project/{project_uid}/jobs/batch-complete` → 主專案 `app/flow_control/service/job_batch_complete_service.py`；`POST /project/{project_uid}/start-task-execution` → `app/flow_engine/service/workflow_execution_service.py` 的 `notify_users_batch_assigned()`；`PUT /project/{uid}`（狀態改為進行中）→ `app/flow_control/service/project_service.py` 的 `_notify_project_started()` |
| **駭客怎麼打** | ① 任一登入帳號把暱稱改成一段網頁語法，例如一個寫著「請重新登入」的連結；② 他去批次完成任務；③ 系統寄出的通知信裡出現一個看起來像系統發的連結；④ 收信的同事點了就上當 |
| **得手什麼** | 用系統寄件身分發釣魚信 |
| **修正的做法** | ① 三處信件的暱稱都先 `html.escape()` 再放進信裡；② 退回補修時再補上專案名稱的跳脫（批次完成彙總、專案啟動兩種信）；③ **只跳脫信件版**，Telegram／Discord 吃純文字，維持原樣 |
| **狀態** | ✅ 已修，1.21.0 出貨（8-C，CM-2213，主專案 commit `a5804a990`／`bde8f9d16`）。驗證：新增 6 條測試＋突變測試，信件裡 `<a` 已成 `&lt;a`、聊天頻道保留原字串 |
