---
title: R 事後無法追查
eyebrow: Guidant AI 資安檢視總報告 · STRIDE 威脅分類
h1: R 事後無法追查（Repudiation）
lede: 命中這一類的 **14 件**，還原成模組頁的 **15 條原始條目**逐條展開。每一條用白話寫出：在哪裡、攻擊面、信任邊界、元件端點、駭客怎麼打、得手什麼、修正的做法。
chips:
  - { text: "🟠 3", kind: warn }
  - { text: "🟡 8", kind: accent }
  - { text: "⚪ 4", kind: plain }
  - { text: "已修 15", kind: ok }
---

## 這一類是什麼

**做了事卻能否認，系統沒有能力追查或證明**。STRIDE 六類裡它破壞的是「不可否認性」——事情發生之後，系統要能回答「是誰、在什麼時候、從哪裡、做了什麼」，而且這份紀錄本身要可信。判定時問的是：**做完之後，查不查得到是誰、紀錄可不可信？** 典型樣子是操作不留紀錄、冒名留言、日誌被偽造、刪除不留痕。

對稽核產品來說這一類格外要緊：客戶買我們，買的就是「紀錄可以拿來當證據」。這次命中的 15 條在本專案長成三種形狀：

- **經手人可以被冒**（M06-7、M03-14、M05-8、M05-9、M06-12、M23-2、M06-19）：紀錄照寫，但寫上去的「是誰做的」不對——不該動手的人動了手、名字取自畫面送上來的值、或事件記到別的專案名下。
- **紀錄寫不進去或寫錯來源**（M09-6、M09-7、M08-7）：操作照做，日誌卻漏掉這一筆，或來源 IP 全記成同一台代理伺服器、甚至讀的是對方自己填的值。
- **紀錄本身能被偽造或抹掉**（M09-4、M12-6、M18-8、M08-2、M08-1）：往客戶的監控系統塞假日誌、受稽核方改掉稽核方的紀錄、刪掉停權與鎖定紀錄、讓防竄改機制對竄改視而不見。

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

> **為什麼是 15 條不是 14 件**：[總結](../SUMMARY.html)問題總表的 #37 由稽核流程（M06-7）與弱點檢測（M03-14）兩個模組各撈到一次——同一道守門，被兩邊的功能共用——總表併成一件；本頁拆回模組頁原始條目，兩條都列。標題括號內是總表編號，可回去對照。

## 條目清單

| # | 條目 | 嚴重度 | 出自 | 狀態 |
|---:|---|---|---|---|
| 1 | [M08-1 換掉負責核對簽章的零件，整套防竄改失效且零警示](#m08-1) | 🟠 高 | [防竄改檢查](../M08-integrity.html) | ✅ 已修 |
| 2 | [M08-2 刪掉一個檔案，被鎖的機器就能重開、鎖定證據跟著消失](#m08-2) | 🟠 高 | [防竄改檢查](../M08-integrity.html) | ✅ 已修 |
| 3 | [M18-8 子單位帳號刪得掉母公司的停權紀錄與授權異動紀錄](#m18-8) | 🟠 高 | [授權管理](../M18-license.html) | ✅ 已修 |
| 4 | [M03-14 唯讀角色可以對客戶機器發動掃描、刪掉失敗紀錄](#m03-14) | 🟡 中 | [弱點檢測整合](../M03-detection.html) | ✅ 已修 |
| 5 | [M06-7 只能看的人可以完成或退回別人的稽核任務](#m06-7) | 🟡 中 | [稽核流程](../M06-flow-engine.html) | ✅ 已修 |
| 6 | [M05-8 改別人的問卷討論還掛原作者名字，刪除不留痕](#m05-8) | 🟡 中 | [問卷](../M05-survey.html) | ✅ 已修 |
| 7 | [M23-2 任何員工都能改刪別人的意見回饋並關掉外部問題單](#m23-2) | 🟡 中 | [意見回饋](../M23-issue.html) | ✅ 已修 |
| 8 | [M05-9 多人同時填問卷時，署名採用畫面送上來的名字](#m05-9) | 🟡 中 | [問卷](../M05-survey.html) | ✅ 已修 |
| 9 | [M09-4 在欄位裡塞換行，就能往客戶監控系統塞偽造日誌](#m09-4) | 🟡 中 | [系統日誌](../M09-log.html) | ✅ 已修 |
| 10 | [M06-19 甲專案管理人能強制開始乙專案的任務，事件記在甲名下](#m06-19) | 🟡 中 | [稽核流程](../M06-flow-engine.html) | ✅ 已修 |
| 11 | [M09-6 把網址加長，這次操作就不會出現在操作日誌](#m09-6) | 🟡 中 | [系統日誌](../M09-log.html) | ✅ 已修 |
| 12 | [M06-12 推進或退回階段時，顯示的操作者名稱由呼叫端自填](#m06-12) | ⚪ 低 | [稽核流程](../M06-flow-engine.html) | ✅ 已修 |
| 13 | [M09-7 操作日誌的來源 IP 全記成前端代理伺服器](#m09-7) | ⚪ 低 | [系統日誌](../M09-log.html) | ✅ 已修 |
| 14 | [M12-6 專案經理能改寫、再刪掉稽核人員寫的改善建議](#m12-6) | ⚪ 低 | [稽核輪次管理](../M12-compliance-audit.html) | ✅ 已修 |
| 15 | [M08-7 防竄改旁證裡的來源 IP 讀的是用戶端可填的欄位](#m08-7) | ⚪ 低 | [防竄改檢查](../M08-integrity.html) | ✅ 已修 |

---

## M08-1 🟠 換掉負責核對簽章的零件，整套防竄改失效且零警示 {#m08-1}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #17；OWASP：A08 軟體或資料完整性失效；主分類為竄改資料） |
| **在哪裡** | 防竄改檢查模組：客戶機房那台機器開機、每四小時抽查、敏感操作前，都會拿原廠簽過名的檔案清單核對程式有沒有被改過；負責「核對簽名」的那個第三方加解密零件，被打包時歸在開機不檢查的那一層 |
| **攻擊面位置** | 客戶機房那台安裝機的檔案系統。需要**主機管理權限**（落地版客戶自己的維運人員、或已入侵主機的人）；這一面本來就由客戶掌握，防竄改機制正是為了「客戶主機上的人改了東西要看得出來」而存在 |
| **信任邊界位置** | **主機上的檔案 ↔ 原廠簽章**。可信的只有原廠私鑰簽出的清單；核對工具本身必須落在被核對的範圍內，否則等於自己驗自己。當時這個零件被別的套件相依「連坐」帶進來、原樣以 .py 檔附帶在不檢查的那層，邊界內缺了最關鍵的一塊 |
| **元件端點位置** | 無 HTTP 端點，在開機與執行期：驗簽 `common/integrity/adapters.py` 的 `LicenseEngineVerifier.verify` → `jedi_license_runtime/common/engine.py` 的 `verify_payload`（Ed25519，靠 `cryptography`／`cffi`）；三層檢查在 `jedi_integrity/app/startup_gate.py`（開機）、`app/runtime_check.py`（抽查與敏感操作）；打包分層在 `scripts/build/build_release.sh`、`scripts/build/security_critical_pkgs.sh` |
| **駭客怎麼打** | ① 有主機權限的人進到安裝目錄，找到原樣附帶的加解密零件；② 把它換成一支「不管簽章對不對都回答通過」的假貨——不是刪掉（刪掉服務會起不來、動靜很大），是掉包；③ 重開服務，開機核對照常印出「驗證通過」；④ 接著任意改產品程式：拿掉授權鎖、改計分邏輯、改寫稽核紀錄的寫法，四小時抽查與敏感操作即時驗證用的是同一個假零件，三層一起放行、全程零警示（2026-09-10 實測證實） |
| **得手什麼** | 客戶機上的產品可被任意竄改而系統照樣顯示一切正常，之後產出的稽核紀錄都不再可信 |
| **修正的做法** | ① **安全關鍵零件強制編進核心**：新增 `security_critical_pkgs.sh` 列出 `cryptography`、`cffi`，打包時從「不編譯清單」扣除、改以 `--include-package` 整包編成機器碼，不再是可以單獨抽換的檔案；② **打包守門**：產物內這兩支若還有任何 .py 原樣附帶、或找不到編譯痕跡，打包直接失敗（`assert_security_critical_compiled`）；③ CM-2132 修正守門判斷——`cffi` 編譯後不留目錄、只在根目錄留 `_cffi_backend*.so`，原本「看目錄在不在」會誤擋，改成依套件型態各自認編譯證據，且只查產物根目錄，避免被 numpy 內同名子目錄誤判；④ 同批順手：打包版一律忽略「改核對目錄」的環境變數（M08-3 放大器） |
| **狀態** | ✅ 已修，1.21.0 出貨（CM-2053，BE commit `0dda326b8`；守門修正 CM-2132，`52df2cf6c`＋`919432229`）。驗證：188 打包產物竄改實測（CM-2136）確認開機會擋 |

## M08-2 🟠 刪掉一個檔案，被鎖的機器就能重開、鎖定證據跟著消失 {#m08-2}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #18；OWASP：A08 軟體或資料完整性失效；主分類為竄改資料） |
| **在哪裡** | 防竄改檢查模組：偵測到竄改後機器被鎖住、拒絕開機。文件寫有「檔案標記」與「資料庫紀錄」兩道鎖，程式當時只做了檔案那一道 |
| **攻擊面位置** | 客戶機房那台安裝機的持久掛載目錄。需要**主機權限**；不需要任何系統帳號 |
| **信任邊界位置** | **主機檔案系統 ↔ 資料庫裡的竄改事件表**。鎖定紀錄是這套機制要保留的證據，應該放在主機使用者不能隨手刪的地方再核對一次；當時查資料庫的程式寫好了卻沒人呼叫，證據只剩一個任何主機使用者都刪得掉的檔案 |
| **元件端點位置** | 無 HTTP 端點，在開機閘門：`jedi_integrity/app/startup_gate.py`（拒啟判定）、`domain/tamper_marker.py`（檔案標記 `.integrity-tamper`）、`app/boot_db_lock.py` 的 `find_db_lock`／`restore_marker_from_db`；宿主實作 `common/integrity/boot_lock_store.py` 的 `PostgresBootLockStore.find_active_lock`，查 `public.integrity_tamper_events` |
| **駭客怎麼打** | ① 機器因竄改被鎖、畫面顯示鎖定；② 有主機權限的人刪掉那一個標記檔；③ 重開服務，開機只看檔案、看不到標記就放行，機器照常開起來；④ 「這台曾被竄改、被鎖過」這筆證據跟著消失，維護者以為有兩道鎖、也不會想到去查 |
| **得手什麼** | 被鎖的機器自行解鎖，竄改事件的鎖定證據被抹掉 |
| **修正的做法** | ① **開機另查資料庫**（套件 `953697c9`）：檔案無標記且為正式打包版時，用宿主開的短命連線查本機指紋有沒有未解鎖的竄改事件；查到就**把標記檔補回去再拒啟**；開機偵測到竄改時也同步寫資料庫；文件三處改成與程式一致；② **擋手寫解鎖回執**（套件 `ba129ca1`）：驗收發現刪標記＋手寫一張解鎖回執也能排除事件，改成回執必須附原廠解鎖憑證原文，閘門與資料庫回填都重驗簽章、指紋、事件編號才採信；③ **權限不足改拒啟**（CM-2112，套件 `da9294fe`＋BE `a7743f1b9`）：資料庫「連得上但沒權讀」或缺帳密時拒絕開機，不再放行；連不上、表不存在仍放行並印警示（開機時資料庫晚起是常態） |
| **狀態** | ✅ 已修（CM-2053＋CM-2112，1.21.0 出貨）。已知限制：同時刪標記檔又刪竄改紀錄表仍會放行——需主機加資料庫管理員雙權限，超出本機制「擋隨手改檔」的定位 |

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

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

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

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

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

## M05-8 🟡 改別人的問卷討論還掛原作者名字，刪除不留痕 {#m05-8}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #39；OWASP：A01 存取控制失效、A09 日誌與告警失效；主分類為事後無法追查） |
| **在哪裡** | 問卷模組：填答頁每題底下的討論串，「編輯」與「刪除」留言 |
| **攻擊面位置** | 已登入、且持有問卷**修改或刪除權限**的人——一般登入帳號按不動。設計上這群人本來就能改問卷 |
| **信任邊界位置** | **問卷權限 ↔ 留言作者本人**。改刪問卷的權限只回答「你能不能改問卷」，不回答「這則留言是不是你寫的」；該在服務層動手前比對作者，當時沒有；刪除也直接從資料庫抹掉 |
| **元件端點位置** | `jedi_survey/api/routing.py`：`PUT /survey-discussion/{uid}`、`DELETE /survey-discussion/{uid}`（`SurveyDiscussionRoute`，`api/routes/survey_discussion_route.py`）；處理在 `jedi_survey/app/service/survey_discussion.py` 的 `update_discussion`／`delete_discussion`，讀取在 `app/service/survey_discussion_service.py` |
| **駭客怎麼打** | ① 有問卷修改權限的人打開某題討論；② 對別人寫的留言按編輯，把內容改成對自己有利的說法；③ 系統不比對作者就存檔，留言照樣掛著原作者名字；④ 或者直接按刪除，那則留言從資料庫消失、沒有任何紀錄——事後看不出原作者說過什麼、誰改的、誰刪的 |
| **得手什麼** | 竄改或抹除他人在問卷上的討論紀錄，且看起來像原作者自己寫的 |
| **修正的做法** | ① **改**：動手前先載出那則留言比對建立者（不用最後修改者，那會被改留言的人覆寫），不是本人回 403（新錯誤碼 `SURVEY_DISCUSSION_NOT_OWNER`）；② **刪**：改成軟刪除——標記「已刪除／誰刪的／何時刪的」，資料列留著，同樣限本人；刪除端點補傳操作者帳號；③ 讀取端加上「未刪除」條件濾掉已刪的（資料庫門禁不看刪除旗標，濾除寫在讀取層）；④ 軟刪除欄位由前置卡 CM-2025（BE `4e7754de7`）先補上 |
| **狀態** | ✅ 已修（FR-114.1-4a，CM-2024，套件 jedi-survey `81051e8a`，1.21.0 出貨）。管理者代刪他人留言的路徑未做，要做需另開卡 |

## M23-2 🟡 任何員工都能改刪別人的意見回饋並關掉外部問題單 {#m23-2}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #45；OWASP：A01 存取控制失效；主分類為竄改資料） |
| **在哪裡** | 意見回饋模組：回饋清單、明細、新增、修改、刪除、刪附件、匯出七個操作；刪除一筆回饋時會順手關掉外部問題單系統（GitLab／GitHub）上對應的單 |
| **攻擊面位置** | **任何登入帳號**，不需要特殊權限，在正常操作畫面上就做得到——這是檢視時唯一一條「一般員工現在就能實際操作利用」的 |
| **信任邊界位置** | **使用者 ↔ 服務，以及本系統 ↔ 外部問題單系統**。七個操作只有匯出掛權限檢查；改刪從不比對「這筆是不是你的」；而刪除會跨出本系統去關單，一旦送出就收不回 |
| **元件端點位置** | `jedi_issue/api/routing.py`：`POST /feedbacks`（清單）、`GET`／`POST`／`PUT`／`DELETE /feedback/{uid}`、`DELETE /feedback/file/{uid}/{file_uid}`、`POST /feedback/export/{export_type}`；處理在 `jedi_issue/app/feedback/service/feedback_service.py` 的 `update_feedback`／`delete_feedback`／`delete_feedback_file`，能力點守門在 `jedi_issue/api/guards.py` |
| **駭客怎麼打** | ① 一般員工登入，打開意見回饋清單看到別人送的回饋；② 對別人的回饋按修改改掉內容，或按刪除；③ 系統不比對歸屬就執行，刪除時還把開發團隊正在處理的那張外部問題單一起關掉；④ 操作日誌記下的是一個合法登入的人做了合法操作，事後看不出這是動了別人的東西 |
| **得手什麼** | 竄改或刪掉任何人的回饋，並關掉外部問題系統上正在處理的單 |
| **修正的做法** | ① **六支端點補能力點**：清單與明細收「管理」或「唯讀檢視」任一顆，新增收建立、改與刪附件收更新、刪除收刪除；守門改成可收多個能力點名稱；② **改、刪、刪附件補作者比對** `_assert_owner`：比對建立者，不是本人回 403（`FEEDBACK_403002`），取不到操作者身分一律擋；刪除的比對放在關閉外部問題單**之前**，非本人時一次外部呼叫都不送；③ **清單加查看範圍**（我的／全部）：不開第二支入口，「全部」需唯讀檢視能力點，不帶參數依能力點推定；範圍條件用獨立欄位以「且」套用，避免被搜尋條件的「或」群組吃掉變成恆真；④ 主專案接上套件新開的「有沒有這顆能力點」查詢（BE `4b1224c4a`） |
| **狀態** | ✅ 已修（FR-114.1-7，CM-2030，套件 jedi-issue `23c2ee69`，1.21.0 出貨）。驗證：六支端點 × 三種身分 18 組合全對；實查開發環境 14 個角色五顆能力點都有，修後不會誤擋現有使用者 |

## 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 出貨） |

## M09-4 🟡 在欄位裡塞換行，就能往客戶監控系統塞偽造日誌 {#m09-4}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #93；OWASP：A09 日誌與告警失效、A05 注入攻擊；主分類為事後無法追查） |
| **在哪裡** | 系統日誌模組：日誌轉送功能——系統依設定把日誌一行一行即時送到客戶的資安監控系統（syslog） |
| **攻擊面位置** | **不需要登入**。只要有一個使用者填得到、又會被原樣記下來的欄位就行，例如登入時輸入的帳號（登入失敗也會被記下） |
| **信任邊界位置** | **本系統 ↔ 客戶的資安監控系統**。進來時照實記是對的（稽核紀錄本該如實）；該把關的是交出去的出口——對方以換行切分紀錄，送出前要把換行編碼掉；當時沒有，且負責清理的程式只處理了訊息第一段 |
| **元件端點位置** | 轉送格式化 `jedi_api_log/forwarding/common/forwarder.py` 的 `build_rfc5424_formatter`／`_escape_control_chars`；轉送設定 `/log-forwarding`（`jedi_api_log/forwarding/api/routing.py`）；可被利用的輸入口例如 `POST /login`（`jedi_iam/api/routing.py`）的帳號欄位 |
| **駭客怎麼打** | ① 不需要帳號，打開登入頁；② 在帳號欄位填「隨便一個名字＋換行＋一段假日誌」，例如偽造成「某某管理員授予了超級權限」；③ 登入失敗，這個帳號字串被原樣寫進日誌並轉送出去；④ 客戶的監控系統把換行當成下一筆，收到兩筆紀錄，第二筆內容完全由攻擊者決定——事後追查時真假混在一起 |
| **得手什麼** | 在客戶的資安監控系統裡憑空捏造稽核紀錄，混淆事後追查 |
| **修正的做法** | ① 轉送用的格式化器送出前把換行、歸位、定位與反斜線等控制字元**編碼成看得見的轉義字元**（`\n` 變成兩個字元），內容一個位元都不少，多行錯誤訊息仍完整送達、只是排在同一筆裡；② GELF 那條路實測已免疫（JSON 會把換行轉義、訊息邊界是封包），只補註解說明；③ 同批修操作日誌與意見回饋匯出的 Excel 公式中和（M09-2 等） |
| **狀態** | ✅ 已修（CM-2055，套件 `38de2886`，1.21.0 出貨）。驗證：塞換行與定位字元的字串經格式化器輸出零換行、內容完整 |

## M06-19 🟡 甲專案管理人能強制開始乙專案的任務，事件記在甲名下 {#m06-19}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #171；OWASP：A01 存取控制失效；主分類為竄改資料） |
| **在哪裡** | 稽核流程模組：規劃頁上「強制開始」卡在合流的任務，以及查詢任務是否卡住 |
| **攻擊面位置** | 已登入、是**某一個專案管理人**的人。網址同時帶專案編號與任務編號；資料庫隔離只分客戶不分專案，同一家客戶內換個任務編號就進得去 |
| **信任邊界位置** | **專案 ↔ 專案（同一客戶內）**。系統只檢查「你在網址上那個專案的身分」，從不核對「這筆任務屬不屬於那個專案」；稽核事件又依網址上的專案記錄 |
| **元件端點位置** | `api/flow_control/__init__.py`：`GET`／`POST /api/1.0/grc/project/{project_uid}/job/{job_uid}/force-start`（`JobForceStartResource`，`api/flow_control/routes/job_force_start_route.py`）、`/api/1.0/grc/project/{project_uid}/job/{job_uid}/blocked-status`；處理在 `app/flow_engine/service/job_force_start_service.py` 的 `_load`；歸屬判斷 `infra/readmodel/tasks/job_project_ownership_query.py` |
| **駭客怎麼打** | ① 甲專案管理人登入，網址填自己的專案；② 任務編號換成乙專案一張卡在合流的任務；③ 按「強制開始」，系統只確認他是甲的管理人就推進；④ 乙的主管收到通知、但兩條分支的證據其實沒收齊；稽核事件記在甲專案名下，事後查乙的紀錄看不出是誰、從哪裡推的 |
| **得手什麼** | 推進別人專案的任務，且稽核事件記錯專案、追不到 |
| **修正的做法** | ① **建立全系統唯一的「任務屬於哪個專案」判斷** `JobProjectOwnershipQuery`：先走控制項對應→輪次→專案，再走輪次主流程→專案，查無回空、呼叫端回 404；不再查指派表（開發環境 99.7% 的任務沒有指派紀錄，用它會誤擋）；② **強制開始與查卡住共用的 `_load`**：查到任務後比對網址上的專案與任務真正歸屬，不符或查無回「找不到任務」；③ 查卡住狀態另補專案成員檢查（CM-2148，`a3fe63b03`） |
| **狀態** | ✅ 已修，1.21.0 出貨（CM-2113，BE commit `f52a53f1e`）。驗證：別專案或查無一律 404、同專案照舊；未指派與輪次主流程任務 HTTP 實打 200 |

## M09-6 🟡 把網址加長，這次操作就不會出現在操作日誌 {#m09-6}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #174；OWASP：A09 日誌與告警失效；主分類為事後無法追查） |
| **在哪裡** | 系統日誌模組：每一次 API 請求都會寫一筆操作日誌（平台管理員事後查詢用）；日誌表「網址」欄最多 500 字 |
| **攻擊面位置** | **任何已登入的人**，打任何一支 API 都行。網址的查詢參數由呼叫端自由決定長度 |
| **信任邊界位置** | **使用者請求 ↔ 操作日誌**。外部送來的值寫進有長度上限的欄位前應先截斷；當時整串直接寫入，超長就整筆寫入失敗，而寫日誌失敗被設計成「不拖垮請求」，只印一行錯誤、請求照常執行 |
| **元件端點位置** | 所有 API（`/api/1.0/*`）共用的攔截器 `common/middleware/app_mw.py` 的 `init_app_interceptor`（`before_request`／`after_request`／`teardown_request`），欄位上限表 `_API_LOG_COLUMN_LIMITS`、截斷 `_clip`、替代紀錄 `_add_fallback_api_log`；寫入 `jedi-log` 的 `ApiLog` 表；查詢頁 `/log/api-logs` |
| **駭客怎麼打** | ① 已登入的人準備刪資料或改設定；② 在網址後面加一段無意義的查詢參數，把整串網址撐到超過 500 字；③ 請求照常執行，資料真的被刪、設定真的被改；④ 寫操作日誌時因網址太長整筆失敗，平台管理員事後查操作日誌，這次操作就像沒發生過 |
| **得手什麼** | 刪資料、改設定而不在操作日誌留下紀錄 |
| **修正的做法** | ① 網址、來源 IP、瀏覽器資訊、使用者名稱等欄位**寫入前一律截到欄位上限**（對照表 `_API_LOG_COLUMN_LIMITS`）；② 寫入仍失敗時**補一筆最小替代紀錄**（方法、路徑、請求編號、標註「寫入失敗」），替代也失敗才只印錯誤，兩者都不拖垮業務請求；③ 網址上的查詢參數改走密碼遮蔽，`access_token` 之類不再明文落地；④ 同批：請求與回應內容遮罩前先截到 64KB，修掉未登入就能卡死伺服器的遮罩問題（總表另一項） |
| **狀態** | ✅ 已修（FR-114 CM-2171，BE commit `908f9dd0e`，1.21.0 出貨）。驗證：800 字網址的請求成功且操作日誌有這筆、網址長 500 |

## M06-12 ⚪ 推進或退回階段時，顯示的操作者名稱由呼叫端自填 {#m06-12}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #128；OWASP：A06 不安全的設計、A08 軟體或資料完整性失效；主分類為事後無法追查） |
| **在哪裡** | 稽核流程模組：稽核輪次的「推進階段」與「退回階段」；操作者名稱顯示在階段歷程時間軸與流程圖留言的作者欄 |
| **攻擊面位置** | 已登入、且**有推進這個階段權限**的人（等於自己人偽造顯示名稱） |
| **信任邊界位置** | **呼叫端送來的內容 ↔ 伺服器寫入的歷程**。代表身分的帳號欄位由伺服器填、不受影響；但顯示名稱用「呼叫端沒填才由伺服器補」的寫法，呼叫端搶先填了就照收 |
| **元件端點位置** | `api/flow_engine/__init__.py`：`POST /api/1.0/project/{project_uid}/audit-round/{round_uid}/stage/advance`（`StageAdvanceResource`，`api/flow_engine/routes/stage_advance_route.py`）、`POST /api/1.0/project/{project_uid}/audit-round/{round_uid}/stage/rollback`（`StageRollbackResource`，`stage_rollback_route.py`）；內容白名單 `api/flow_engine/serializers/stage_advance.py` 的 `strip_unknown_ctx` |
| **駭客怎麼打** | ① 有推進權限的人在輪次頁按「推進」或「退回」；② 送出的請求裡多帶一個顯示名稱，填成另一位同事；③ 伺服器看到已經有值就不覆寫；④ 階段歷程與留言作者欄顯示的是那位同事——帳號欄位仍是真人，但看畫面的人會被誤導 |
| **得手什麼** | 階段歷程與流程圖留言上的操作者顯示名稱被偽造 |
| **修正的做法** | ① 兩支入口都改成**直接由伺服器指派**登入者的顯示名稱，不再「沒填才補」；② 新增 `strip_unknown_ctx` 白名單，請求內容只放行處理端真的會讀的欄位，其餘丟掉；兩支路由共用同一支，避免兩個入口各長一套 |
| **狀態** | ✅ 已修（CM-2059，BE commit `8edbaf944`，1.21.0 出貨） |

## M09-7 ⚪ 操作日誌的來源 IP 全記成前端代理伺服器 {#m09-7}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #185；OWASP：A09 日誌與告警失效；主分類為事後無法追查） |
| **在哪裡** | 系統日誌模組：操作日誌的「來源 IP」欄；另有登入流程（人機驗證與登入防護會用到來源 IP）讀 IP 的寫法不一致 |
| **攻擊面位置** | 這條本身不是攻擊造成，是**任何操作者**都享有的匿名性：後端記到的是直接連進來的前端代理伺服器。登入那邊另一個問題則是**不需要登入**——呼叫端可自帶轉發標頭冒充 IP |
| **信任邊界位置** | **前端代理伺服器（nginx）↔ 後端**。真實客戶端 IP 只能由最外層那一跳代理告訴後端；當時操作日誌完全沒採信代理轉來的值，登入卻反過來無條件相信任何人都能自填的轉發標頭 |
| **元件端點位置** | `core/app_factory.py` 的 `create_app`（掛 `ProxyFix(x_for=1)`）；操作日誌寫入 `common/middleware/app_mw.py`（`source_ip=request.remote_addr`）；登入取 IP `jedi_iam/api/routes/login_route.py` 的 `_client_ip`（`POST /login`） |
| **駭客怎麼打** | ① 有人用合法帳號做了刪改操作；② 操作日誌照記，但來源 IP 是前端代理的內部位址（STG 22.8 萬筆全是同兩個位址）；③ 出事時要查「是哪台電腦」，日誌給不出答案；④ 若天真地改讀轉發標頭，攻擊者只要在請求裡自帶一個假 IP 就能栽贓別台電腦——登入那邊當時就是這樣 |
| **得手什麼** | 操作出事後查不出來源電腦，或在登入紀錄上冒用別人的 IP |
| **修正的做法** | ① **後端只相信 nginx 那一跳**：`create_app` 掛 `ProxyFix(x_for=1)`，之後一律讀 `request.remote_addr`，掛在 app 建立處讓 API、即時通道與測試三種模式都吃得到；動手前查過安裝版只有 nginx 對外開埠，信一層正確；② 操作日誌改記 `remote_addr`；③ 套件登入的 `_client_ip` 不再先讀轉發標頭，只回 `remote_addr`；④ 防竄改旁證同步改（見 M08-7）——三處讀 IP 的地方統一 |
| **狀態** | ✅ 已修（FR-114 CM-2171，BE commit `908f9dd0e`，套件 jedi-iam commit `960d737e`，1.21.0 出貨）。打折：本機無 nginx，「經 nginx 記到真實客戶端 IP」未實測；直連帶假標頭不被信只在 API 埠不對外時成立（安裝版即如此） |

## M12-6 ⚪ 專案經理能改寫、再刪掉稽核人員寫的改善建議 {#m12-6}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #189；OWASP：A01 存取控制失效；主分類為竄改資料） |
| **在哪裡** | 稽核輪次管理模組：輪次進入「整改中」後，稽核人員在每個風險底下留一筆「改善建議」，系統規定它不能刪 |
| **攻擊面位置** | 已登入、是**該專案經理**、且輪次在整改中——門檻高。專案經理是受稽核的一方 |
| **信任邊界位置** | **受稽核方 ↔ 稽核方**。改善建議是稽核方的紀錄，受稽核方不該碰；「新增整改計畫」這支會把帶既有編號的請求當成更新，沒區分那一筆是誰的、是什麼類型 |
| **元件端點位置** | `api/project/__init__.py`：`POST /api/1.0/audit-round/{round_uid}/ar/risk/{risk_uid}/remediations`（`RiskRemediationsRoute`，`api/project/routes/audit_round_route.py`）、`DELETE /api/1.0/audit-round/{round_uid}/remediation/{remediation_uid}`（`PoamRemediationRoute`）；請求格式 `api/project/serializers/audit_round.py` 的 `CreateRemediationRequest`；處理在套件 `jedi_compliance_audit/app/service/poam_app_service.py` 的 `add_remediation` |
| **駭客怎麼打** | ① 專案經理在整改中的輪次拿到稽核人員那筆改善建議的編號；② 用「新增整改計畫」送出，編號填那筆建議；③ 系統當成更新，把建議改寫成一般整改計畫、內容換成經理想要的；④ 類型一變，「建議不能刪」的檢查就不成立，接著整筆刪掉——事後看不出稽核人員原本寫了什麼 |
| **得手什麼** | 受稽核方改掉並刪除稽核方的建議，破壞稽核獨立性與佐證軌跡 |
| **修正的做法** | ① **套件**：`add_remediation` 對類型是「建議」的一律拒絕新建（409 `GRC_POAM_RECOMMENDATION_PROTECTED`），帶編號且命中既有建議時拒絕覆蓋——建議只能由稽核階段建風險時寫入；② **主專案**：請求格式的 `lifecycle` 欄只收「planned／completed」，「recommendation」直接 400；③ 刪除端原有的建議保護維持，覆蓋路徑堵住後就繞不過去 |
| **狀態** | ✅ 已修（FR-114 CM-2174，BE `a8f1a24b7`／套件 `b7022b8b`，1.21.0 出貨）。驗證：經理以建議編號覆蓋回 409、新建建議類型回 400、刪建議 409、建議內容未被改；正常的整改計畫新增修改刪除照常 |

## M08-7 ⚪ 防竄改旁證裡的來源 IP 讀的是用戶端可填的欄位 {#m08-7}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #233；OWASP：A09 日誌與告警失效；主分類為事後無法追查） |
| **在哪裡** | 防竄改檢查模組：敏感操作即時驗證抓到竄改時，會記下一份「旁證」（當下是誰、從哪台電腦、打哪支功能）供事後追查 |
| **攻擊面位置** | 觸發即時驗證的那個請求的發送者。轉發標頭任何呼叫端都能自己帶 |
| **信任邊界位置** | **呼叫端 ↔ 後端的連線資訊**。來源 IP 只能信最外層代理那一跳；當時旁證直接讀 `X-Forwarded-For`，等於讓被追查的人自己填「我從哪來」 |
| **元件端點位置** | `common/integrity/adapters.py` 的 `_current_request_context`（旁證的 `client.source_ip`），由 `collect_host_trigger_context`／`hotpath_verify` 呼叫；IP 來源由 `core/app_factory.py` 的 `ProxyFix(x_for=1)` 決定 |
| **駭客怎麼打** | ① 已在主機上動過手腳的人，接著從系統做一個會觸發即時驗證的敏感操作；② 請求裡自帶一個假的轉發標頭，填成別台電腦的 IP；③ 系統偵測到竄改、記下旁證，來源 IP 寫的是那個假值；④ 事後追查指向無辜的電腦（不影響防竄改本身擋不擋得住） |
| **得手什麼** | 竄改事件旁證裡的來源 IP 失真，追查被誤導 |
| **修正的做法** | ① 拿掉旁證自讀 `X-Forwarded-For` 的寫法，改讀 `request.remote_addr`；② 真實客戶端 IP 由 app 統一掛的 `ProxyFix(x_for=1)` 放進 `remote_addr`（只信 nginx 一跳），與操作日誌、登入三處一致（見 M09-7） |
| **狀態** | ✅ 已修，1.21.0 出貨（FR-114 CM-2171，BE commit `908f9dd0e`）。這條是主系統掃描查出的 |
