---
title: D 讓服務停擺
eyebrow: Guidant AI 資安檢視總報告 · STRIDE 威脅分類
h1: D 讓服務停擺（Denial of Service）
lede: 命中這一類的 **34 件**，還原成 **34 條原始條目**（32 條出自模組頁，2 條只在總表、沒有模組頁逐條表）。每一條用白話寫出：在哪裡、攻擊面、信任邊界、元件端點、駭客怎麼打、得手什麼、修正的做法。
chips:
  - { text: "🟠 3", kind: warn }
  - { text: "🟡 19", kind: accent }
  - { text: "⚪ 12", kind: plain }
  - { text: "已修 33／不修 1", kind: ok }
---

## 這一類是什麼

**讓正常使用者用不了服務**。STRIDE 六類裡它破壞的是「可用性」這個性質——資料沒外洩、身分也沒被冒充，但產品變慢、卡住或整個不回應。依微軟的定義：拒絕或降低對合法使用者的服務，例如讓網站暫時無法使用。

判定時問的是：**攻擊者打下這一條之後，能不能讓產品對所有人變慢或停擺？** 這一類在本產品特別容易成立，原因是一個共同的前提：**落地版預設只有 4 條處理程序、每條一次處理一個請求、120 秒才強制中止，而且多數檔案解析是在請求當下同步做完的。** 所以只要一個請求能佔住一條處理程序幾十秒，四個同時送就讓所有客戶一起連不上。

這次的 34 條可以歸成四種形狀：

- **小檔案解開變巨物**（M18-2、M03-3、M03-4、M11-23、M11-24、M12-2、M12-3、M06-23、M11-34、M11-36、M07-8）：系統只量上傳檔多大、不量解開後多大。授權檔、規則包、Excel、Word、PDF、YAML 範本，同一個病出現在十一個入口——幾 MB 的檔解開成幾 GB，記憶體吃光、處理程序被砍。
- **特製文字讓比對越跑越慢**（M04-15、M11-25、M11-26、M11-35、M11-37、M12-8）：程式用來比對文字的規則遇到刻意設計的內容，耗時隨長度平方甚至更快成長。最嚴重的 M04-15 **不必登入**——系統在檢查身分之前就先拿請求內容去遮密碼。
- **該有上限的量沒有上限**（M06-3、M06-4、M04-7、M19-3、M14-1、M03-10、M03-11）：一頁幾筆、聊天訊息多長、同時開幾條背景工作、一個網段展開幾台、流程圖分岔幾層——都由呼叫者說了算。
- **一個小錯誤讓整條路停擺**（M06-8、總表#103、M20-4、M21-3、M02-12、總表#145、M24-16、M24-17、M04-9、M24-15）：一筆壞資料讓所有人的清單頁一直出錯、寫日誌失敗把使用者的請求一起弄壞、刪檔順序反了讓原廠範本全部打不開。這一組多半不是駭客能挑的入口，而是正常操作就會踩到的「功能自己停擺」。

修法也有共同的形狀：**檢查要放在第一次打開檔案之前**（只讀壓縮檔目錄、不解壓）、**比對規則改成不會回頭重試的寫法並先截長度**、**每一種「量」都給一個寬鬆但不是無限的上限**——上限抓太小頂多有人回報功能不夠用，看得到、改得掉；不設上限則是全產品停擺，看不到、來不及救。

> **STRIDE 與 OWASP 的關係**：OWASP 講「程式哪裡寫錯」，STRIDE 講「攻擊者能拿它做什麼」。同一條問題通常兩邊都命中，所以這一頁的條目會與 [OWASP 各頁](../OWASP-Top-10/A10-mishandling-of-exceptional-conditions.html)重複出現，內容相同——兩邊各自完整，讀者從哪一邊進來都看得完。
>
> **兩條沒有模組頁出處**：總表#103（寫日誌失敗連累請求）與總表#145（公告空值崩潰）來自掃描總表的「非資安但確實是錯誤」清單，模組頁沒有逐條表，本頁依總表描述、原始掃描報告與修正 commit 還原。

## 條目清單

| # | 條目 | 嚴重度 | 出自 | 狀態 |
|---:|---|---|---|---|
| 1 | [M06-3 一張特製的流程圖讓伺服器永遠算不完](#m06-3) | 🟠 高 | [稽核流程](../M06-flow-engine.html) | ✅ 已修 |
| 2 | [M18-2 授權檔驗章之前就先解壓縮，而且沒有上限](#m18-2) | 🟠 高 | [授權管理](../M18-license.html) | ✅ 已修 |
| 3 | [M04-15 不必登入，送一個特製內容的請求就讓整個產品停止回應](#m04-15) | 🟠 高 | [共用基礎](../M04-common.html) | ✅ 已修 |
| 4 | [M06-4 「檢查流程圖」任何登入者都能打，而且不限大小](#m06-4) | 🟡 中 | [稽核流程](../M06-flow-engine.html) | ✅ 已修 |
| 5 | [M21-3 沒有部門的一般使用者送意見回饋必定失敗、畫面不給理由](#m21-3) | 🟡 中 | [意見回饋重新檢視](../M21-issue-rescan.html) | ✅ 已修 |
| 6 | [M03-3 網址型規則包完全繞過壓縮檔的三道上限](#m03-3) | 🟡 中 | [弱點檢測整合](../M03-detection.html) | ✅ 已修 |
| 7 | [M03-4 規則包「最多一萬個檔」的上限對 zip 形同虛設](#m03-4) | 🟡 中 | [弱點檢測整合](../M03-detection.html) | ✅ 已修 |
| 8 | [M03-10 手動重新解析無條件開一條背景工作，可以把主機打掛](#m03-10) | 🟡 中 | [弱點檢測整合](../M03-detection.html) | ✅ 已修 |
| 9 | [M03-11 換掃描工具時舊的超大網段原封不動跟過去](#m03-11) | 🟡 中 | [弱點檢測整合](../M03-detection.html) | ✅ 已修 |
| 10 | [M04-7 一次要求回傳幾筆資料，沒有上限](#m04-7) | 🟡 中 | [共用基礎](../M04-common.html) | ✅ 已修 |
| 11 | [M06-8 一筆空白內容的流程範本讓所有人的清單頁一直出錯](#m06-8) | 🟡 中 | [稽核流程](../M06-flow-engine.html) | ✅ 已修 |
| 12 | [M14-1 聊天沒有長度上限，也沒有次數限制](#m14-1) | 🟡 中 | [AI 聊天助手](../M14-ai-bot.html) | ✅ 已修 |
| 13 | [總表#103 寫日誌進資料庫失敗會連帶弄壞使用者的請求](#r103) | 🟡 中 | [總表](../SUMMARY-owasp-stride.html#r103) | ✅ 已修 |
| 14 | [M20-4 兩支半夜清理程式靠「找不到身分就給最高權限」才跑得動](#m20-4) | 🟡 中 | [客戶資料隔離](../M20-tenant-isolation.html) | ✅ 已修 |
| 15 | [M11-23 系統安全計畫的 Excel 匯入只量上傳檔、不量解開後多大](#m11-23) | 🟡 中 | [合規文件核心](../M11-oscal.html) | ✅ 已修 |
| 16 | [M11-24 系統安全計畫的 Word 匯入同樣只量上傳檔大小](#m11-24) | 🟡 中 | [合規文件核心](../M11-oscal.html) | ✅ 已修 |
| 17 | [M11-25 Excel「說明」頁填一百萬個數字，版本號比對就卡住](#m11-25) | 🟡 中 | [合規文件核心](../M11-oscal.html) | ✅ 已修 |
| 18 | [M11-26 一份幾 KB 的特製 Word 讓系統安全計畫匯入卡到逾時](#m11-26) | 🟡 中 | [合規文件核心](../M11-oscal.html) | ✅ 已修 |
| 19 | [M12-2 稽核計畫 Word 匯入的壓縮炸彈](#m12-2) | 🟡 中 | [稽核輪次管理](../M12-compliance-audit.html) | ✅ 已修 |
| 20 | [M12-3 稽核紀錄 Excel 在很後面填一格，系統就從頭一列列讀到那裡](#m12-3) | 🟡 中 | [稽核輪次管理](../M12-compliance-audit.html) | ✅ 已修 |
| 21 | [M06-23 匯入任務的 Excel 沒有上傳檢查、整份攤開在記憶體](#m06-23) | 🟡 中 | [稽核流程](../M06-flow-engine.html) | ✅ 已修 |
| 22 | [M02-12 刪原廠共用檔時實體先被刪、紀錄卻刪不掉](#m02-12) | 🟡 中 | [檔案上傳下載](../M02-file-upload.html) | ✅ 已修 |
| 23 | [M04-9 兩種運作紀錄的詳細程度被寫死成最詳細](#m04-9) | ⚪ 低 | [共用基礎](../M04-common.html) | ✅ 已修 |
| 24 | [M07-8 分類程式沒有記憶體與處理器上限，逾時也殺不掉](#m07-8) | ⚪ 低 | [證據自動分類](../M07-evidence-classification.html) | ✅ 已修 |
| 25 | [M19-3 設備清單一頁要顯示幾筆，沒有上限](#m19-3) | ⚪ 低 | [設備與資訊系統清冊](../M19-asset.html) | ✅ 已修 |
| 26 | [總表#145 公告查無此筆對空值設值、沒有登入身分時建立公告崩潰](#r145) | ⚪ 低 | [總表](../SUMMARY-owasp-stride.html#r145) | ✅ 已修 |
| 27 | [M11-34 幾 KB 的特製 YAML 範本讓伺服器展開到當掉](#m11-34) | ⚪ 低 | [合規文件核心](../M11-oscal.html) | ✅ 已修 |
| 28 | [M11-35 範本匯入檢核送一格很長的怪字串，佔住處理程序兩分鐘](#m11-35) | ⚪ 低 | [合規文件核心](../M11-oscal.html) | ✅ 已修 |
| 29 | [M11-36 頁數極多的 CMMC PDF 讓解析時記憶體撐爆](#m11-36) | ⚪ 低 | [合規文件核心](../M11-oscal.html) | ✅ 已修 |
| 30 | [M11-37 PDF 裡一行超長的字讓「是不是目錄」的比對卡住](#m11-37) | ⚪ 低 | [合規文件核心](../M11-oscal.html) | ✅ 已修 |
| 31 | [M12-8 稽核計畫 Word 塞幾萬個空白，解析比對卡到逾時](#m12-8) | ⚪ 低 | [稽核輪次管理](../M12-compliance-audit.html) | ✅ 已修 |
| 32 | [M24-16 首次安裝精靈設定碼塞中文或特殊字元，伺服器回 500](#m24-16) | ⚪ 低 | [主系統自己的程式](../M24-host.html) | ✅ 已修 |
| 33 | [M24-15 雲端硬碟通知說有去重複、實際沒做](#m24-15) | ⚪ 低 | [主系統自己的程式](../M24-host.html) | 🚫 裁定不修 |
| 34 | [M24-17 匯出診斷包時一邊帶時區一邊不帶，程式當掉回 500](#m24-17) | ⚪ 低 | [主系統自己的程式](../M24-host.html) | ✅ 已修 |

---

## M06-3 🟠 一張特製的流程圖讓伺服器永遠算不完 {#m06-3}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #16；OWASP：A06 不安全的設計） |
| **在哪裡** | 稽核流程模組：系統推算「下一步輪到誰」的那段程式——打開稽核輪次頁面、完成或退回任務、啟動一輪稽核時都會跑 |
| **攻擊面位置** | 需要登入且有「流程範本新增／修改」權限，把一張特製的流程圖存進去。存進去之後攻擊者什麼都不用做——**任何看得到那個專案的人打開稽核輪次頁面，就替他觸發了**。也不一定要有人存心：流程畫得太複雜的客戶可能無意間做出同樣的效果 |
| **信任邊界位置** | **使用者畫的流程圖 ↔ 伺服器的推算程式**。流程圖是使用者提供的資料，推算程式應該把它當不可信輸入，走訪時記住走過哪裡、設深度上限；當時兩支函式互相呼叫、不記走過的連線，每多一層分岔下游就整段重算一次，工作量一層層相乘。而存檔時的兩道檢查擋的是「繞回自己的圈」與「分岔沒寫條件」，這張圖兩道都過得了 |
| **元件端點位置** | 推算核心在套件 `jedi_flow_engine/common/utils/bpmn_uilts.py`：`get_next_jobs()` 與 `_get_jobs_from_exclusive_gateway()` 互相呼叫。觸發入口：主專案 `GET /project/{project_uid}/audit-round/{round_uid}/stage/info`（`StageInfoResource` → `app/flow_engine/service/stage_advance_service.py` 的 `_peek_next_stage_code_via_jobs()`）、`/stage/advance`、`/flow-engine/task/complete/{id}`、`/flow-engine/task/revert/{id}`（`app/flow_engine/service/workflow_execution_service.py`）。存進去的入口：`POST /flow-engine/flow-templates`、`PUT /flow-engine/flow-templates/{uid}`（`flow_template_app_service.create()`／`update()`） |
| **駭客怎麼打** | ① 一個有流程範本編輯權限的帳號，畫一張「一層接一層連續分岔」的流程圖——不需要繞回自己，是一張完全合法的圖；② 存檔，新增與修改當時都不跑檢查，就算跑也過得了；③ 把這份範本用在某個專案的稽核輪次；④ 任何成員打開那一輪的頁面，畫面自動問「下一步輪到誰」，伺服器開始推算——10 層 2 秒、11 層 9 秒、12 層算不完，不報錯也不留紀錄；⑤ 四個人同時打開（或攻擊者自己開四個分頁），4 條處理程序全卡住，整個產品對所有客戶停止回應 |
| **得手什麼** | 讓整個產品對所有客戶停止回應，而且每次有人開頁面就重新觸發 |
| **修正的做法** | ① **走訪記住走過哪裡**：兩支函式之間傳「已走過的連線」與目前深度，走過的不再展開；深度超過 8 層或已走訪超過 100 條就記一筆警告後停止，上限值與主專案寫入端同一組，避免「存得進去卻走不完」；② **消掉指數成本**：原本取預設分支時把同一個分岔點再算一遍，每過一個分岔點工作量翻倍，改成算一次傳下去；③ **主專案範本的新增與修改也跑檢查**：補上原本只有發布才跑的兩支驗證，節點數／分岔深度上限擴及所有寫入路徑；④ 另擋「節點連回自己」，但**不擋所有環**——正常的送審退回本來就走回上一個節點，出貨內建範本就有這種迴圈 |
| **狀態** | ✅ 已修（CM-2059，套件 commit `4a416ece`、主專案 commit `8edbaf944`，1.21.0 出貨）。驗證：攻擊圖修前永不返回、修後 0.001 秒返回並被寫入端擋下；DEV 27 份範本修前修後走訪結果逐份一致、0 份被新上限擋到。模組頁修法第⑤件「啟動輪次要求範本已發布」另由 CM-2083 補上（套件 commit `d3cc1bc8`：起輪次三條取範本路徑的匯合處檢查範本狀態，未發布一律擋下） |

## M18-2 🟠 授權檔驗章之前就先解壓縮，而且沒有上限 {#m18-2}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #34；OWASP：A06 不安全的設計） |
| **在哪裡** | 授權管理模組：離線開通——把原廠回寄的授權檔上傳進來 |
| **攻擊面位置** | **任何一個登入帳號**都打得到：這支是刻意設計成「任何登入者可用」（授權過期的客戶唯一自救路徑），而且可以無限重複送 |
| **信任邊界位置** | **使用者上傳的檔案 ↔ 驗章程式**。授權檔要先解壓才能驗章（簽章簽的是解開後的原文，順序不能對調），所以「還沒驗證過的資料」會先進到解壓這一步；這一步應該邊解邊設上限，當時完全沒有上限，在簽章來得及發揮保護之前記憶體就吃光了 |
| **元件端點位置** | 套件 `jedi_license_runtime/api/routing.py`：`POST /license/activation/upload`（`LicenseActivationUploadRoute`，`AUTH_LOGIN`）→ `jedi_license_runtime/common/engine.py` 的 `_unpack_payload()`；上限常數在 `common/constant.py` |
| **駭客怎麼打** | ① 任何一個登入帳號，不需要特殊權限；② 準備一個「1GB 全零」的假內容，壓縮後約 1MB、編碼後約 1.4MB，包成授權檔格式；③ 從開通頁上傳；④ 系統在驗章之前先整份解開，處理程序記憶體吃到 1GB；⑤ 連送幾次，落地版是單一容器，整個產品被系統砍掉 |
| **得手什麼** | 用一個 1.4MB 的請求把整個產品打掛 |
| **修正的做法** | ① **解碼之前先擋長度**：編碼字串超過 32 MiB 連解碼都不做；② **解壓時有界**：改用可設上限的解壓方式，最多解出 16 MiB，還有沒解完的就判超限拒絕——上限是實測最大正常授權檔的約 18 倍；③ 程式旁邊寫明「兩道都不可省、順序不可對調」的理由；④ 另兩處同款寫法（防竄改套件的測試工具與測試設定）一併改成有界，避免日後照抄舊寫法 |
| **狀態** | ✅ 已修（CM-1572，套件 commit `7091e046`）。總表狀態欄未帶卡號，修正依據為依「解壓炸彈」搜尋套件 commit 找到。驗證：新增 4 條測試（含記憶體量測），突變拿掉修法兩條轉紅 |

## M04-15 🟠 不必登入，送一個特製內容的請求就讓整個產品停止回應 {#m04-15}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟠 高（總表 #149；OWASP：A06 不安全的設計）。全案唯一不必登入就能讓產品停擺的一條 |
| **在哪裡** | 共用基礎：每個請求進來時，系統先把請求內容「遮密碼」再記進操作日誌——所有網址都經過這一段，包含不存在的網址 |
| **攻擊面位置** | **網路上任何人，不需要帳號**。這一段跑在檢查身分之前，所以連登入都不必，打一個不存在的網址也會中 |
| **信任邊界位置** | **匿名網路 ↔ 後端入口**。在還不知道對方是誰之前，系統應該只做便宜、有上限的事；當時卻把整份請求內容丟進一段比對規則去遮密碼，而且一個請求遮兩遍。那段規則把欄位名寫成「前後各一段可任意長、夾著關鍵字」，遇到特定形狀的內容耗時隨長度平方成長 |
| **元件端點位置** | 任何路徑（例如不存在的網址）。主專案 `common/middleware/app_mw.py`：`init_app_interceptor()` 的 `before_request` → `_loggable_body()` → 套件 `jedi-common/jedi_common/utils/common_utils.py` 的 `mark_password()` |
| **駭客怎麼打** | ① 網路上任何人，不需要帳號；② 對我們的網站打一個不存在的網址，請求內容放 128KB 的特製文字（同一小段重複很多次）；③ 系統還沒檢查身分就先拿它去遮密碼，比對卡住 6 秒以上（16KB 就要近 3 分鐘的形狀也存在）；④ 同時送幾個，4 條處理程序全卡住，所有客戶一起連不上 |
| **得手什麼** | 不必任何帳號，讓整個產品對所有客戶停止回應 |
| **修正的做法** | 兩層都修：① **根因（套件）**：遮密碼規則改成一次比對一組「欄位名：值」，所有重複次數改成「吃了不吐」的寫法、欄位名加 256 字上限，是不是機密改在比對完之後另外判斷，耗時變成隨長度線性；值沒收尾（內容被截斷）時把後半視為值、照樣遮；② **入口（主專案）**：遮之前先把內容截到 64KB 並標「已截斷，原長 N bytes」，記日誌與寫資料庫共用同一份，一個請求只遮一次；③ 網址參數也改用同一張關鍵字表遮（`mask_query_string`） |
| **狀態** | ✅ 已修（FR-114 CM-2171，套件 jedi-common commit `cc61f18e`、主專案 commit `908f9dd0e`，1.21.0 出貨；同卡另一支 jedi-iam commit `960d737e` 修的是來源 IP，與本條無關）。驗證：不登入打不存在網址帶 16～128KB 攻擊內容，修後 0.02～0.04 秒回 404；新增 64KB 三種攻擊形狀須 0.5 秒內的效能測試，換回舊規則即轉紅 |

## M06-4 🟡 「檢查流程圖」任何登入者都能打，而且不限大小 {#m06-4}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #40；OWASP：A01 存取控制失效、A06 不安全的設計） |
| **在哪裡** | 稽核流程模組：流程圖編輯器即時檢查畫得對不對的那支功能 |
| **攻擊面位置** | **任何一個登入帳號**：同一個檔案裡其他功能都掛了權限檢查，只有這一支沒有；送進來的內容也不限長度、節點數與連線數 |
| **信任邊界位置** | **使用者 ↔ 流程範本服務**。該檢查的是「你有沒有編輯流程範本的權限」與「送進來的東西多大」，兩道都沒有。原本報告寫「打幾次就讓全站停擺」，實測後確認不成立（這支走的是另一套檢查邏輯，一千個節點 0.027 秒），實質影響是「沒權限的人可以用它、而且可以丟很大的東西進來」 |
| **元件端點位置** | 主專案 `api/flow_engine/__init__.py`：`POST /flow-engine/flow-templates/validate`（`FlowTemplateValidateRoute.post`，`api/flow_engine/routes/flow_template_route.py`）→ `app/flow_engine/service/flow_template_app_service.py` 的 `validate()`；請求格式 `api/flow_engine/serializers/flow_template.py` 的 `FlowTemplateValidateRequestSchema` |
| **駭客怎麼打** | ① 任何一個登入帳號，即使沒有流程範本權限；② 直接打這支檢查功能，送一份很大的流程圖內容；③ 系統不問權限、不限大小，照收照算；④ 單獨打不到停擺，但這是一扇沒有任何關卡的門 |
| **得手什麼** | 沒有權限的人能使用流程範本功能，並能丟很大的內容進來消耗資源 |
| **修正的做法** | ① 路由補上「流程範本修改」權限檢查（`flow_template.update`），與同檔其他寫入功能同級；② 請求格式加長度上限 35 萬字（DEV 最大範本約 14.7KB，等比放大到 100 節點再乘 3 倍）；③ 服務層新增 `_check_size_limits()`：節點數上限 100、分岔深度上限 8，走訪時遇到環直接停；④ 超限時**維持原本「200＋警告清單」的回應方式**，不改成錯誤——前端編輯器的即時警告靠這個畫；⑤ 之後 CM-2059 把同一組上限擴到範本新增、修改、發布所有寫入路徑 |
| **狀態** | ✅ 已修（FR-114.1-8／CM-2031，主專案 commit `aa04ca1a7`；上限擴及寫入路徑見 CM-2059 `8edbaf944`）。驗證：150 節點與 10 層分岔各回對應警告且仍是 200；35 萬零 1 字被擋 |

## M21-3 🟡 沒有部門的一般使用者送意見回饋必定失敗、畫面不給理由 {#m21-3}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #44；OWASP：A10 例外狀況處理不當） |
| **在哪裡** | 意見回饋模組：使用者在「意見回饋」頁按送出 |
| **攻擊面位置** | 不是攻擊面，是**功能自己壞掉**：任何一個還沒被分配部門的正常帳號都會踩到，新客戶剛裝好、部門還沒建起來時特別容易 |
| **信任邊界位置** | **應用程式 ↔ 資料庫隔離規則**。資料庫那層的「新增」規則多要求了「送出者必須有部門」，跟另外三條（查、改、刪）形狀不同；規則擋下時應用程式沒有把原因翻成白話，畫面只看到失敗 |
| **元件端點位置** | `jedi_issue/api/routing.py`：`POST /feedback`（`FeedbackRoute.post`）→ `feedback_service.add_feedback()`；擋下它的是資料庫規則 `feedback_issues_insert`，定義在 `jedi-issue/jedi_issue/migrations/004-feedback-issues-rls-grants.sql`（既有庫由主專案 `scripts/sql/2026-09-22-fr114-feedback-issues-insert-relax-org.sql` 換掉） |
| **駭客怎麼打** | ①（不是駭客，是一般使用者）管理員剛建好帳號、還沒指定部門；② 這個人打開意見回饋、填好內容按送出；③ 資料庫的新增規則看到「部門是空的」就擋下，畫面只顯示失敗；④ 管理員去查這個人的權限，每一項都對，查不出原因 |
| **得手什麼** | 沒有人得手；是正常使用者用不了功能，而且沒人看得出為什麼 |
| **修正的做法** | ① **決策者裁定放寬規則**：送意見回饋本來就與部門無關，不採「規定帳號必有部門」（影響所有建帳流程）也不採「只把錯誤講清楚」（沒解決卡住）；② 「新增」規則改成跟另外三條一模一樣：超級管理員可略過，否則只看「這筆屬於哪一家客戶、你是不是那家的」，「必須有部門」整個拿掉；③ 套件裡給新裝庫用的那份與主專案給既有庫換規則的 migration 兩處同時改，少一邊就會「新裝的沒事、舊庫還卡著」；④ 客戶隔離沒跟著鬆：客戶欄位留空的列仍插不進去 |
| **狀態** | ✅ 已修（FR-114.1-9／CM-2032，主專案 commit `fc9fca7d0`、套件 commit `e98fea29`）。驗證：沒部門的人送得出去且客戶欄位正確；跨客戶送、客戶欄位留空仍被擋 |

## M03-3 🟡 網址型規則包完全繞過壓縮檔的三道上限 {#m03-3}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #76；OWASP：A06 不安全的設計） |
| **在哪裡** | 弱點檢測整合模組：建立檢測基準（規則包）——可以上傳壓縮檔，也可以填一個網址讓系統去下載 |
| **攻擊面位置** | 需要登入且有「建立檢測基準」權限的帳號（客戶的管理員層級）。選「填網址」這條路就能把一個惡意壓縮檔放在自己的網站上給系統抓 |
| **信任邊界位置** | **外部網址的內容 ↔ 解析程式**。防壓縮炸彈的三道上限（檔案數／解開總量／膨脹倍數）應該放在兩種來源都會經過的那一層；當時只接在「上傳檔案」那條路，網址下載回來直接解開，三道一道都不會碰到 |
| **元件端點位置** | 套件 `jedi_detection/api/routing.py`：`POST /detection-tool-profiles`（`DetectionProfilesRoute.post`，網址型走 JSON）、`POST /detection-tool-profiles/{uid}/new-version` → `app/service/detection_profile_extraction_service.py` → 兩種來源共用的 `common/profile_extractor/inspec.py` 的 `_read_entries()`；上限實作新開在 `common/archive_limits.py` |
| **駭客怎麼打** | ① 有建立檢測基準權限的帳號；② 在自己的網站放一個 45MB、解開數十 GB 的壓縮檔；③ 新增檢測基準時選「網址」、填這個網址；④ 系統下載回來直接解開，上傳那條路的三道檢查完全沒跑；⑤ 伺服器記憶體被吃光，所有客戶一起停擺 |
| **得手什麼** | 讓整個產品對所有客戶停擺 |
| **修正的做法** | ① **三道上限下沉到兩種來源唯一共用的那一層**：新開 `archive_limits.py`，接進 `_read_entries()`——現有兩種與未來第三種來源都天然涵蓋；② 刻意**不**在網址分支另補一次檢查，那是補第二條路，下次加來源照樣會漏；③ 原本只住在上傳驗證器裡的那份實作改成共用同一份，兩處不再各有一套；④ 網址下載本身原已邊收邊算、上限 50MB，不必另補 |
| **狀態** | ✅ 已修（CM-2057，套件 commit `c0fe19fd`，1.21.0 出貨）。驗證：隨包規則包重打包前後一致；膨脹倍數單測 2MB 宣告 500MB 被擋 |

## M03-4 🟡 規則包「最多一萬個檔」的上限對 zip 形同虛設 {#m03-4}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #77；OWASP：A06 不安全的設計） |
| **在哪裡** | 弱點檢測整合模組：上傳檢測規則包時的「最多一萬個檔」檢查 |
| **攻擊面位置** | 需要登入且有「建立檢測基準」權限的帳號，上傳一個特製的 zip 檔 |
| **信任邊界位置** | **上傳的壓縮檔 ↔ 解析程式**。檔案數上限應該在打開壓縮檔**之前**就檢查；當時要等整份檔案清單展開進記憶體之後才開始數，數到第一萬零一個才喊停時記憶體早就吃掉了。程式旁的說明文字還寫反（說「不會先把清單整份展開」，對 tar 成立、對 zip 不成立），這句話正是它通過歷次檢視的原因 |
| **元件端點位置** | 套件 `jedi_detection/api/routing.py`：`POST /detection-tool-profiles`（上傳型）→ `common/detection_profile_archive.py` 的 `ProfileArchiveValidator`（驗證器）與抽取器兩個入口；開檔前的目錄筆數檢查在 `common/archive_limits.py` |
| **駭客怎麼打** | ① 有建立檢測基準權限的帳號；② 做一個 49.7MB、裡面塞 59.5 萬個空檔的 zip；③ 上傳；④ 系統光是打開它就多吃 360MB 記憶體，之後才報「壓縮檔不安全」——日誌看起來一切正常，代價已經付掉；⑤ 連送幾次，伺服器停擺 |
| **得手什麼** | 讓伺服器停擺，而且日誌看不出異狀 |
| **修正的做法** | ① **打開之前先讀壓縮檔檔尾的目錄筆數欄位**（含大型 zip 的延伸格式），超過上限直接擋，根本不建立檔案清單；② 驗證器與抽取器兩個入口都加；③ 改掉那段寫反的說明文字；④ tar 格式維持逐筆計數（tar 本來就是邊讀邊數） |
| **狀態** | ✅ 已修（CM-2057，套件 commit `c0fe19fd`，1.21.0 出貨）。驗證：手造 6 萬筆 zip 在讀目錄階段即被擋（兩個入口都試過）；1.2 萬筆 tar.gz 逐筆計數擋下 |

## M03-10 🟡 手動重新解析無條件開一條背景工作，可以把主機打掛 {#m03-10}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #80；OWASP：A06 不安全的設計） |
| **在哪裡** | 弱點檢測整合模組：檢測基準的「手動重新解析」按鈕（解析失敗後的補救途徑） |
| **攻擊面位置** | 需要登入且有「建立檢測基準」權限的帳號。按鈕每按一次就是一個請求，可以用程式連打 |
| **信任邊界位置** | **使用者請求 ↔ 背景工作**。開背景工作之前應該先問「這一版是不是已經在跑」與「現在總共跑了幾條」；當時每收一次請求就開一條，沒有任何上限。而且這支會先把狀態壓回「等待中」再排工作——如果只在後面加判斷，判斷永遠看到「等待中」、擋不到任何東西 |
| **元件端點位置** | 套件 `jedi_detection/api/routing.py`：`POST /detection-tool-profile-versions/{uid}/extraction`（`DetectionProfileVersionExtractionRoute.post`）→ `app/service/detection_profile_extraction_service.py` 的 `retry()` → `schedule()`；並行控管新開在 `common/extraction_concurrency.py`；上限設定在主專案 `config/config.py`（`DETECTION_PROFILE_EXTRACTION_MAX_CONCURRENT`） |
| **駭客怎麼打** | ① 有檢測基準權限的帳號；② 對同一版規則包連打幾百次「重新解析」；③ 每一次都開一條背景工作，每條把最大 50MB 的檔整包讀進記憶體、再開一支最長 15 分鐘的外部程式；④ 幾百條同時存在，同一台機器上所有客戶一起變慢 |
| **得手什麼** | 讓同一台機器上的所有客戶一起變慢甚至停擺 |
| **修正的做法** | ① **同一版去重**：同一版已在跑就回 409「這一版正在解析」；② **同時執行上限 4 條**：超出的排隊等、不丟棄（丟棄會讓那一版永遠轉圈）；③ **排隊深度上限**（上限的 10 倍），滿了才回 409「系統忙」；④ 🔴 **新判斷放在「壓回等待中」之前**——放在之後看起來正確卻一個都擋不到；⑤ 名額在背景工作自己結束時才歸還；自動重抽那條路也走同一道控管 |
| **狀態** | ✅ 已修（CM-2058，套件 commit `f8e3281a`、主專案設定 commit `ebc57b96f`，1.21.0 出貨）。驗證：同版第二次被拒；排隊滿 40 後拒收 |

## M03-11 🟡 換掃描工具時舊的超大網段原封不動跟過去 {#m03-11}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #81；OWASP：A06 不安全的設計） |
| **在哪裡** | 弱點檢測整合模組：任務綁定檢測工具時的「要掃哪些機器」，以及按下執行時把網段逐台展開 |
| **攻擊面位置** | 需要登入且能新增或修改專案任務的帳號（專案管理者層級） |
| **信任邊界位置** | **使用者送的部分更新 ↔ 已存的綁定**。台數上限應該用「這次更新完成後實際會生效的值」重新檢查；當時只看「這次有沒有送範圍」，沒送就沿用舊值、不重驗。而展開網段的那一端註明「這裡刻意不擋」，完全相信前面已經檢查過 |
| **元件端點位置** | 主專案 `POST /project/{project_uid}/assessment-object/{ao_uid}/jobs`（`ProjectJobCreateResource`）、`PUT /project/{project_uid}/job/{job_uid}`（`ProjectJobDetailResource.put`）→ `app/flow_control/service/job_service.py` 的 `create_job()`／`update_job()` → 套件 mixin `jedi_detection/app/service/detection_job_binding_handler.py` 的 `_replace_detection_tool_binding()`（工具參數）與 `_replace_detection_tool_agent_assignments()`（分派列）；執行：`POST /detection-tools/jobs/{job_uid}/execute` → `app/service/detection_orchestration_service.py` 的 `_expand_scan_target_fields()` |
| **駭客怎麼打** | ① 專案管理者先把任務綁一支**不設台數上限**的工具，掃描範圍填一個超大網段（例如一個 /8）；② 再送第二次修改，只把工具換成有台數上限的那支、不帶範圍；③ 系統只看「這次沒送範圍」就不檢查，舊的超大網段原封不動留在新工具底下；④ 按下執行，系統把網段逐台展開——1,677 萬台，記憶體瞬間吃爆、服務當掉；⑤ 統籌者驗收時另找到同一個洞在分派列那一頭的第二個發生點 |
| **得手什麼** | 一次執行就讓整個服務當掉 |
| **修正的做法** | ① **工具參數區**：新增 `_effective_hosts()`，這次沒送範圍時拿既有綁定的值，用新工具的上限重驗；② **分派列**：原本「沒送分派就直接返回」，改成先拿既有各列的範圍對本次生效的工具重驗一次；③ **展開端加絕對上限 65536 台**（新錯誤碼 `DETECTION_TOOLS_400020`），用「算數量、不展開」的方式判斷——為了擋「展開會炸」而先展開一次等於先炸一次；④ 改掉「這裡刻意不擋」那段說明，否則下一個人會以為新上限是誤加而拿掉 |
| **狀態** | ✅ 已修（CM-2058，套件 commit `f8e3281a`，1.21.0 出貨）。驗證：`_effective_hosts` 四種情境（沒送／送了沒該欄／送新值／明確清空）皆正確；/8 網段算出 16,777,214 台被擋 |

## M04-7 🟡 一次要求回傳幾筆資料，沒有上限 {#m04-7}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #83；OWASP：A06 不安全的設計） |
| **在哪裡** | 共用基礎：全站所有清單畫面共用的「第幾頁、一頁幾筆」設定——11 支套件加主專案都吃這一份 |
| **攻擊面位置** | **任何一個登入帳號**，在任何清單畫面的請求裡改一個數字 |
| **信任邊界位置** | **使用者 ↔ 資料庫**。「一頁幾筆」是使用者填的，應該有上限；當時「第幾頁」有檢查下限，「一頁幾筆」的上限漏掉了一半。既有的「請求大小上限」擋不住，因為送出去的請求很小、要回來的資料很大 |
| **元件端點位置** | 套件 `jedi-common/jedi_common/interfaces/schema/common.py`：`PagerSchema.page_size`（`RequestMetaSchema` 的分頁欄位）；所有繼承它的清單端點，例如主專案 `POST /projects/list`、套件 `POST /devices` |
| **駭客怎麼打** | ① 任何一個登入帳號，打開任一個清單頁；② 把請求裡的「一頁幾筆」改成一個超大數字；③ 資料庫照做，把整張表一次撈出來、組好回傳；④ 重複幾次，整個產品對所有客戶停止回應 |
| **得手什麼** | 讓整個產品對所有客戶停止回應 |
| **修正的做法** | ① **先盤點再加上限**（順序不能顛倒）：有些統計是「撈全部回來自己數」，直接加上限會讓數字悄悄變小；grep 全部繼承者與呼叫端，現況最大請求值就是 1000；② `page_size` 加範圍檢查 1～1000（`MAX_PAGE_SIZE`），等於現況最大值、不擋既有功能；③ **不開白名單參數繞過**——參數能繞等於沒設上限；真的要全撈走專屬匯出或內部分頁迴圈；④ 一處改、全站十二個地方一起生效 |
| **狀態** | ✅ 已修（CM-2065／FR-114.5C-4，套件 commit `a9562dd0`，1.21.0 出貨）。驗證：新增測試，突變拿掉上限即轉紅 |

## M06-8 🟡 一筆空白內容的流程範本讓所有人的清單頁一直出錯 {#m06-8}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #87；OWASP：A10 例外狀況處理不當） |
| **在哪裡** | 稽核流程模組：流程範本的儲存與讀取（法規框架裡的流程圖、每輪稽核的流程） |
| **攻擊面位置** | 需要登入且持有「法規框架修改」權限（`module-frame.update`）。寫壞一筆是一次性動作，**之後受害的是其他正常使用者**，不是寫壞它的人 |
| **信任邊界位置** | **寫入端 ↔ 資料庫 ↔ 所有讀取端**。寫入端用「這欄有沒有值」當判斷，空字串在程式裡算「沒有值」，就跳過驗證直接存；讀取端又相信資料庫裡的一定是合法流程圖、無條件解析不防呆。兩邊都沒守 |
| **元件端點位置** | 寫入：主專案 `api/module_frame/__init__.py` 的 `PUT /module-frame/item/xml/{uid}`（`ModuleFrameItemXmlRoute`，`xml` 欄位 `payload.get("xml", "")`）→ 套件 `jedi_flow_engine/infra/repository/workflow_template_repo_impl.py` 的 `add()`／`update()`；讀取：`jedi_flow_engine/infra/mapper/workflow_template_mapper.py` 的 `to_entity()`（所有查詢的唯一轉換點），例如 `GET /flow-engine/process-definition/{uid}` 與各範本清單 |
| **駭客怎麼打** | ① 一個有法規框架修改權限的帳號，打開流程圖編輯；② 送出時把流程圖內容留空（或直接不帶）；③ 寫入端判斷「沒有值」就跳過檢查，空字串照樣存進資料庫；④ 之後任何人打開會讀到這筆範本的清單或詳細頁，解析器對空字串報錯、整頁 500；⑤ 不會自己恢復，要有人進資料庫把那筆修掉 |
| **得手什麼** | 讓所有人的流程範本清單與相關頁面持續打不開 |
| **修正的做法** | ① **寫入端分清三種情況**：抽出 `_sync_json_from_xml()`——沒送（維持舊值）、空白字串（直接拒絕，新錯誤碼 `FLOW_ENGINE_400004`）、有值（解析並驗證）；② **讀取端防呆**：`to_entity()` 遇空值跳過解析、解析失敗退成「0 個任務」並記警告，一筆壞資料不再拖垮整份查詢；③ 主專案這邊範本的新增與修改也補上「先驗流程圖再存」，原本只有發布時才驗 |
| **狀態** | ✅ 已修（CM-2059，套件 commit `4a416ece`、主專案 commit `8edbaf944`，1.21.0 出貨）。驗證：DEV 27 份範本修前修後逐份比對走訪結果一致，0 份被新規則擋下 |

## M14-1 🟡 聊天沒有長度上限，也沒有次數限制 {#m14-1}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #94；OWASP：A06 不安全的設計） |
| **在哪裡** | AI 聊天助手模組：產品裡的聊天框，使用者打的內容直接轉給外部 AI |
| **攻擊面位置** | **任何一個最基層的員工**，不需要特殊權限，四到八個連線就夠 |
| **信任邊界位置** | **使用者 ↔ 外部 AI 服務**。轉給外部服務之前應該限制訊息長度、每人呼叫次數，並設等待逾時；當時三樣都沒有——外部 AI 沒回應時要等 10 分鐘（套件預設值），而系統 120 秒就會強制砍掉卡住的請求，這中間的落差正是處理程序被佔住的原因。整站「請求不得超過 50MB」的限制對聊天訊息等於沒擋 |
| **元件端點位置** | 套件 jedi-ai-bot：`POST /api/1.0/ai-chatbot`（`AiBotRoute.post`，路徑在 `plugin/contract.py` 的 `DEFAULT_ENDPOINT`，`plugin/assembly.py` 掛載）→ `app/service/ai_bot_service.py`；主專案接線 `core/plugins/ai_bot.py`、計次器 `common/util/redis_rate_limiter.py`、429 處理在 `core/app_factory.py` |
| **駭客怎麼打** | ① 任何一個登入的員工，打開聊天框；② 開四到八個分頁，各送一則超大的訊息；③ 每一則都直接轉給外部 AI、等它回，外部回得慢就一直等；④ 4 條處理程序全被佔住，其他人連登入畫面都打不開；⑤ 另一種後果是把公司共用的 AI 額度燒光，直接變成帳單 |
| **得手什麼** | 讓整個產品對所有人停止回應，或燒光公司的 AI 額度 |
| **修正的做法** | ① **訊息長度上限 4000 字**，在路由最外層先擋，超過回 400；② **呼叫外部 AI 設 60 秒逾時**，讓系統自己放棄、不等到被強制砍掉；③ **套件開「每人每分鐘最多幾次」的插槽**，主專案用 Redis 計次器接上，預設每人每分鐘 10 次，超過回 429 並帶「幾秒後可再試」；計次跨處理程序共用；④ AI 儀表板同款問題同一次修（每分鐘 3 次，因為一次生成打兩次 AI）；⑤ 限流刻意**故障時放行**——它是防濫用不是身分疆界，擋掉所有正常客戶的代價更高 |
| **狀態** | ✅ 已修（CM-2064，套件 commit `248132ec`、主專案 commit `0d321d98a`，1.21.0 出貨）。驗證：正常 200、5000 字 400、第 11 次 429 帶等待秒數、計次器故障時放行 |

## 總表#103 🟡 寫日誌進資料庫失敗會連帶弄壞使用者的請求 {#r103}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #103；OWASP：A10 例外狀況處理不當、A09 日誌與告警失效） |
| **在哪裡** | 共用基礎：系統把操作紀錄與錯誤寫進資料庫的那一段（每個請求都可能經過） |
| **攻擊面位置** | 不是駭客能主動挑的入口（檢查員三票一致認定非資安問題）；只要資料庫那一刻寫不進去（連線斷、表被鎖、權限被收回），**任何正常使用者的請求**都可能被連累。正式環境原本不走這條路，CM-1920 為了「只放行稽核事件與錯誤」在正式／STG 環境補掛了這段，風險因此放大 |
| **信任邊界位置** | **業務程式 ↔ 紀錄機制**。紀錄是附帶動作，失敗時應該自己吞掉、印到旁邊；當時寫資料庫那支完全沒有錯誤處理，錯誤沿著「記一筆日誌」的呼叫點往上冒，回到業務程式變成使用者看到的系統錯誤 |
| **元件端點位置** | 套件 `jedi-common/jedi_common/logger/db_log/db_handler.py`：`DBLogHandler.emit()`；掛載設定在同套件 `logger/config_prod.py`（CM-1920 commit `4bf7906` 補掛） |
| **駭客怎麼打** | ①（不是駭客主動打）一般使用者在正式環境正常操作，某個動作會記一筆紀錄；② 那一刻資料庫寫不進去；③ 錯誤從紀錄機制一路往上冒，把使用者原本已經做完的請求一起弄成失敗；④ 另一個隱藏問題：這支用到的時間欄位要靠前面的紀錄處理器順便填好，它能正常運作純粹是因為剛好排在第三個，換個順序就會自己出錯 |
| **得手什麼** | 沒有人得手；「記錄失敗」這種小事變成使用者的操作失敗 |
| **修正的做法** | ① `emit()` 整支包上錯誤處理，失敗時呼叫紀錄機制的標準出口 `handleError(record)`（印到錯誤輸出、不回呼紀錄機制，不會無限迴圈）；② 拆掉隱性的排序依賴：時間改讀一定有值的 `record.created`，不再依賴前一個處理器填好的格式化時間；③ 操作者帳號與暱稱改成「有就記、沒有就空著」，缺了只少記操作者、不再出錯 |
| **狀態** | ✅ 已修（CM-2065／FR-114.5C-4，套件 commit `a9562dd0`，1.21.0 出貨） |

## 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-23 🟡 系統安全計畫的 Excel 匯入只量上傳檔、不量解開後多大 {#m11-23}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #150；OWASP：A06 不安全的設計） |
| **在哪裡** | 合規文件核心：合規資源庫與專案的系統安全計畫（SSP）Excel 匯入 |
| **攻擊面位置** | **任何能匯入系統安全計畫的登入帳號**，上傳一份特製的 Excel |
| **信任邊界位置** | **使用者上傳的檔案 ↔ 解析程式**。Excel 其實是一個壓縮檔，檢查應該在打開之前就問「解開後多大」；當時只量上傳檔本身多大，打開時整份攤進記憶體，而且讀取方式是「每取一格都從頭掃」 |
| **元件端點位置** | 主專案 `api/oscal/__init__.py`：`POST /ssp-excel-imports/parse`（`SspExcelImportParseRoute`）、`POST /ssp/{ssp_uid}/excel-import/upload`（`SspScopedExcelImportUploadRoute`）→ `app/oscal/service/ssp_excel_import_app_service.py` → `app/oscal/service/excel_parser/parser.py`、`sheet_handlers.py`；共用檢查在套件 `jedi_compliance_audit/app/service/import_adapter/archive_guard.py` 的 `check_ooxml_archive()` |
| **駭客怎麼打** | ① 任何能匯入系統安全計畫的帳號；② 做一份 2MB、解開好幾 GB 的「試算表炸彈」；③ 上傳匯入；④ 系統打開時整份塞進記憶體，處理程序記憶體爆掉被砍，同一台上其他人的請求一起失敗；⑤ 每分鐘送幾次，產品就持續不可用 |
| **得手什麼** | 讓同一台主機上所有人的請求持續失敗 |
| **修正的做法** | ① **開檔前只讀壓縮檔目錄、不解壓**：解開後總量上限 200MB、最多 5,000 段、單段 1MB 以上者膨脹比不得超過 100 倍，不是合法壓縮檔也拒；超限轉成既有的「解析失敗」狀態，沒新增錯誤碼；② **四個入口共用同一支檢查**（SSP Excel、SSP Word、稽核計畫 Word、稽核結果 Excel），放在套件裡；③ **Excel 改成唯讀串流、一列一列讀**，讀完關檔；④ 上限依真實檔實測訂，至少 15 倍餘裕 |
| **狀態** | ✅ 已修（FR-114 CM-2181，主專案 commit `48638cbdb`、套件 commit `2cb9d2cd`，1.21.0 出貨；同型第五入口見 [M06-23](#m06-23)）。驗證：解開 400MB 的 Excel 炸彈 0.37 秒回失敗、處理程序記憶體不變；新舊程式對真實範本解析結果逐位元組相同 |

## M11-24 🟡 系統安全計畫的 Word 匯入同樣只量上傳檔大小 {#m11-24}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #152；OWASP：A06 不安全的設計） |
| **在哪裡** | 合規文件核心：專案的系統安全計畫 Word 匯入 |
| **攻擊面位置** | **任何能匯入系統安全計畫的登入帳號**，上傳一份特製的 Word |
| **信任邊界位置** | **使用者上傳的檔案 ↔ 解析程式**。Word 也是壓縮檔，同 M11-23；而且同一份上傳會被完整打開三次，所以檢查必須放在**第一次**打開之前 |
| **元件端點位置** | 主專案 `api/oscal/__init__.py`：`POST /ssp-docx-imports/parse`（`SspDocxImportParseRoute`）→ `app/oscal/service/ssp_docx_import_app_service.py`（第一次開檔在 `_normalize_docx_revisions` 之前）；共用檢查 `jedi_compliance_audit/app/service/import_adapter/archive_guard.py` 的 `check_ooxml_archive()` |
| **駭客怎麼打** | ① 任何能匯入系統安全計畫的帳號；② 做一份 0.32MB、裡面 300 段各 1MB 的 Word；③ 上傳；④ 打開一次就吃掉 1.9GB 記憶體，而且會被打開三次；⑤ 處理程序被砍，同一台上其他人一起失敗 |
| **得手什麼** | 用不到 1MB 的檔讓伺服器記憶體耗盡 |
| **修正的做法** | ① 在第一次開檔（整理修訂紀錄那一步）之前呼叫同一支 `check_ooxml_archive()`，之後的兩次開檔自然受保護；② 超限丟例外，被既有錯誤處理轉成「解析失敗」狀態與既有錯誤碼，沒新增錯誤碼；③ 與 M11-23、M12-2、M12-3 同一張卡、同一支檢查函式 |
| **狀態** | ✅ 已修（FR-114 CM-2181，主專案 commit `48638cbdb`、套件 commit `2cb9d2cd`，1.21.0 出貨）。驗證：解開 300MB 的 Word 炸彈 0.33 秒回失敗、處理程序記憶體 449→449MB 不漲；正常 SSP Word 照常進入待審 |

## M11-25 🟡 Excel「說明」頁填一百萬個數字，版本號比對就卡住 {#m11-25}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #154；OWASP：A06 不安全的設計） |
| **在哪裡** | 合規文件核心：系統安全計畫 Excel 匯入時，檢查「說明」頁那一格的範本版本號 |
| **攻擊面位置** | **任何能匯入系統安全計畫的登入帳號**，在 Excel 的說明頁版本格填一長串數字 |
| **信任邊界位置** | **檔案內容 ↔ 比對規則**。版本號的比對規則應該頭尾綁定、位數有上限；當時系統裡寫了兩份版本規則，一份寫對、一份寫錯（沒綁頭尾、沒位數上限），解析時用的是寫錯那份，遇到超長數字會反覆重試 |
| **元件端點位置** | 主專案 `POST /ssp-excel-imports/parse`、`POST /ssp/{ssp_uid}/excel-import/upload` → `app/oscal/service/excel_parser/parser.py`（原 `_SEMVER_PATTERN`）→ 改用 `app/oscal/service/excel_parser/version_check.py` 新增的 `extract_version()` |
| **駭客怎麼打** | ① 任何能匯入系統安全計畫的帳號；② 拿一份正常範本，在說明頁版本那一格填一百萬個數字；③ 上傳；④ 版本號比對卡住，一個請求佔住處理程序到 120 秒逾時；⑤ 四個請求讓整個系統沒回應 |
| **得手什麼** | 用四個請求讓整個系統沒回應 |
| **修正的做法** | ① **刪掉寫錯的那份規則**，版本規則只留 `version_check.py` 一處；② 新增 `extract_version()`：輸入先截 200 字，規則限制每段最多 5 位數 |
| **狀態** | ✅ 已修（FR-114 CM-2181，主專案 commit `48638cbdb`，1.21.0 出貨）。驗證：版本格塞一百萬位數字 0.24 秒回格式錯誤（舊規則 5 萬位就要 5 秒） |

## M11-26 🟡 一份幾 KB 的特製 Word 讓系統安全計畫匯入卡到逾時 {#m11-26}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #175；OWASP：A06 不安全的設計） |
| **在哪裡** | 合規文件核心：系統安全計畫 Word 匯入的整條解析路（找佔位字、比對控制項編號、走訪段落與表格、取冒號後的文字） |
| **攻擊面位置** | **任何能匯入系統安全計畫的帳號**；壓縮炸彈（M11-24）修好之後門檻升一級，但幾 KB 的檔就夠 |
| **信任邊界位置** | **檔案內容 ↔ 解析程式**。解析在請求當下同步做完、佔住一條處理程序；解析路上六個地方遇到刻意做壞的內容會退化成平方或三次方時間——比對規則會回頭重試、每取一段都重建整份段落清單、每取一次樣式都線性掃樣式表、表格一格宣告一億欄就展開成一億個物件 |
| **元件端點位置** | 主專案 `POST /ssp-docx-imports/parse` → `app/oscal/service/ssp_docx_import_app_service.py` → `domain/oscal/parser/docx_parser_core.py`、`docx_section_extractors.py`、`control_id_matcher.py`、`domain/oscal/adapter/cmmc_ssp_adapter.py`；新增結構檢查 `domain/oscal/parser/docx_input_limits.py` 的 `check_docx_structure()` |
| **駭客怎麼打** | ① 能匯入系統安全計畫的帳號；② 拿一份真實的 SSP Word，在佔位字那一段塞 3,200 個空白，或塞兩三千個空段落，或讓一格表格宣告一億欄；③ 上傳；④ 解析卡 46～96 秒，表格那種直接逾時被砍；⑤ 四個請求同時送，整個產品對所有客戶停擺 |
| **得手什麼** | 用幾 KB 的檔讓整個產品停擺 |
| **修正的做法** | ① **解析前先掃一次結構**：段落（含表格格內）超過 3,000、任一格合併超過 200 欄、表格超過 200 欄就拒收——真實 SSP 約 600 段，上限約 5 倍；整份先擋一次，12 個展開表格的呼叫點不必各自改；② **樣式名稱依文件快取**，不再每段線性掃樣式表（佔總耗時 95%）；③ 佔位字四條規則改成「吃了不吐」的寫法，判準不變；超過 500 字直接判「不是佔位字」；④ 控制項編號比對排除同種開括號並限長 200；⑤ 段落走訪改成直接包底層元素，不再每次重建清單；兩處「找自己在第幾段」改成外層計數；⑥ 取冒號後文字輸入先截 2,000 字 |
| **狀態** | ✅ 已修（FR-114 CM-2182，主專案 commit `9edcf4de4`，1.21.0 出貨）。驗證：五種惡意形狀全帶 81 秒→0.49 秒、加上一億欄 逾時→0.04 秒拒收；7 份真實 SSP 新舊輸出逐位元組相同；惡意檔解析期間正常請求 0.09 秒回應 |

## M12-2 🟡 稽核計畫 Word 匯入的壓縮炸彈 {#m12-2}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #176；OWASP：A06 不安全的設計） |
| **在哪裡** | 稽核輪次管理：稽核計畫頁匯入稽核計畫 Word |
| **攻擊面位置** | 需要是某個專案的稽核員或管理者，且計畫還在規劃階段 |
| **信任邊界位置** | **使用者上傳的檔案 ↔ 解析程式**。同 M11-23、M11-24 的病，病灶在另一個套件的共用讀檔程式：只量上傳檔大小、不量解開後多大，打開時整份塞進記憶體 |
| **元件端點位置** | 主專案 `api/project/__init__.py`：`POST /ap/{ap_uid}/docx-imports/parse`（`ApDocxImportUploadRoute`）→ 套件 `jedi_compliance_audit/app/service/ap_docx_import_app_service.py` 的 `upload_and_parse()` → `app/service/import_adapter/registry_base.py` 的 `RegistryBase.parse_file()` |
| **駭客怎麼打** | ① 某專案的稽核員或管理者；② 做一份不到 20MB、解開幾十 GB 的 Word；③ 在稽核計畫頁匯入；④ 處理程序被砍；⑤ 連送幾份，所有客戶一起停擺 |
| **得手什麼** | 讓所有客戶一起停擺 |
| **修正的做法** | ① 在這個模組共用的讀檔程式 `RegistryBase.parse_file()` 裡，**開檔之前**呼叫同一支 `check_ooxml_archive()`，稽核計畫 Word 與稽核結果 Excel 一次蓋到；② 不論成敗都在最後關閉活頁簿（唯讀模式會一直握著檔案）；③ 與 M11-23、M11-24、M12-3 四個入口同一張卡、同一支檢查函式 |
| **狀態** | ✅ 已修（FR-114 CM-2181，套件 commit `2cb9d2cd`、主專案 commit `48638cbdb`，1.21.0 出貨）。驗證：Word 炸彈（解開 300MB）修前吃到 681MB 後炸、修後 0 秒拒絕；DEV 經 API 打 0.3～0.6 秒回失敗、處理程序記憶體不變 |

## M12-3 🟡 稽核紀錄 Excel 在很後面填一格，系統就從頭一列列讀到那裡 {#m12-3}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #177；OWASP：A06 不安全的設計） |
| **在哪裡** | 稽核輪次管理：匯入稽核結果（稽核紀錄 Excel） |
| **攻擊面位置** | 需要是某個專案的稽核員或管理者。成本比壓縮炸彈更低——幾 KB、看起來正常的稽核表就能騙過格式檢查 |
| **信任邊界位置** | **檔案內容 ↔ 解析程式**。Excel 的「有資料的範圍」是檔案自己宣告的，解析程式應該設列數上限、遇到大段空白就停；當時從第一列一列列讀到最後一格有值的列，每一列都建物件，而且這段時間資料庫交易一直開著 |
| **元件端點位置** | 主專案 `api/project/__init__.py`：`POST /audit-round/{round_uid}/ar-imports/parse`（`ArImportUploadRoute`）→ 套件 `jedi_compliance_audit/app/service/ar_import_app_service.py` → `app/service/ar_report_parser/airasia_cmmc_l1_ar_v1.py` 的 `_parse_rows()` |
| **駭客怎麼打** | ① 某專案的稽核員或管理者；② 拿一份正常的稽核表，在第 20 萬列填一格；③ 匯入，檔案只有 4.9KB；④ 系統從頭讀到第 20 萬列，耗時 11 秒、吃掉 355MB；推算填到 Excel 最後一列約 57 秒、1.9GB；⑤ 連送幾份，處理程序全被佔住 |
| **得手什麼** | 用幾 KB 的普通檔佔住處理程序與資料庫交易 |
| **修正的做法** | ① 稽核結果改用唯讀模式、一列一列串流讀；② **連續 200 列全空就視為檔尾**；③ **超過 5,000 列資料直接報錯**（真實表只有一百多列）；④ 表頭與基本資料那幾格（只有前 5 列）照舊直接取 |
| **狀態** | ✅ 已修（FR-114 CM-2181，套件 commit `2cb9d2cd`、主專案 commit `48638cbdb`，1.21.0 出貨）。驗證：第 20 萬列填一格修前 1.6 秒／505MB、修後 0.01 秒／51MB；真實稽核表新舊解析結果逐位元組相同 |

## M06-23 🟡 匯入任務的 Excel 沒有上傳檢查、整份攤開在記憶體 {#m06-23}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #207；OWASP：A06 不安全的設計） |
| **在哪裡** | 稽核流程：專案規劃頁的「匯入任務 Excel」——「試算表炸彈」的第五個入口，先前修好的四個入口沒列到它 |
| **攻擊面位置** | 需要登入且有該專案任務匯入權限的帳號 |
| **信任邊界位置** | **使用者上傳的檔案 ↔ 解析程式**。同 M11-23；這個入口用 `load_workbook(file)` 一次把整份攤開，沒有任何壓縮炸彈檢查 |
| **元件端點位置** | 套件 `jedi_task_platform/task/api/routing.py`：`POST /project/{project_uid}/ap/{ap_uid}/jobs/import`（`JobImportRoute.post`）→ 主專案 `app/flow_control/service/job_import_service.py` 的 `validate_import()` → 新增的 `_iter_upload_rows()`、`_iter_data_rows()` |
| **駭客怎麼打** | ① 有任務匯入權限的帳號；② 做一份 9.8MB 的特製 Excel；③ 在規劃頁匯入任務；④ 系統讀 22 秒、吃掉 1.46GB；⑤ 連送幾份，所有客戶一起連不上。另一種：4.8KB 的檔只在第 1,048,000 列放一格，要迭代一百萬列 |
| **得手什麼** | 讓所有客戶一起連不上 |
| **修正的做法** | ① **開檔前用 CM-2181 的共用檢查** `check_ooxml_archive()` 擋炸彈，不另寫第二套；② **改唯讀模式逐列讀**；並重設檔案自己宣告的範圍，避免宣告寫小的檔少讀資料列；③ 首腦驗收退回後補：**連續 200 列空白即停、資料超過 5,000 列報錯**，與稽核結果 Excel 同規則；④ 任何失敗（含迭代途中才發現的毀損）都轉既有的「檔案格式錯誤」，不新增錯誤碼 |
| **狀態** | ✅ 已修（CM-2203，主專案 commit `eb4ff79cd`＋`040c3c827`，1.21.0 出貨）。驗證：壓縮炸彈 0.22 秒回 400、記憶體不漲；第 1,048,000 列放一格的檔 0.34 秒整趟回應；DEV 兩份真實匯出檔新舊回應逐字相同；突變拿掉檢查即放行 |

## M02-12 🟡 刪原廠共用檔時實體先被刪、紀錄卻刪不掉 {#m02-12}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | 🟡 中（總表 #212；OWASP：A01 存取控制失效、A10 例外狀況處理不當） |
| **在哪裡** | 檔案上傳下載模組：刪除檔案；受害的是原廠給所有客戶共用的檔案（框架匯入的公版指引、檢測規則包等） |
| **攻擊面位置** | **任何一家客戶的一般登入帳號**都構成攻擊面，只要知道一個原廠共用檔的編號 |
| **信任邊界位置** | **客戶 ↔ 原廠共用資源**。刪除時應該先問「這個檔是不是原廠的、你能不能刪」，而且資料庫紀錄沒刪成就不能動實體檔。當時順序是先刪實體、再刪紀錄；紀錄那一步被資料庫隔離規則擋下，但實體已經沒了，而程式不看刪除結果、照樣回成功 |
| **元件端點位置** | `jedi_file_upload/api/routing.py`：`DELETE /file/upload/{uid}`（`UploadFileRoute.delete`）→ 儲存後端 `infra/adapter/minio/minio_adapter.py`（落地版的 SeaweedFS 也走這支）與 `infra/adapter/local/local_file_adapter.py` 的 `delete_file()`／`delete_files_by_uids()`；歸屬檢查在主專案 `core/plugins/file_upload.py` 的 `SystemAssetFileOwnershipProvider` |
| **駭客怎麼打** | ① 任何一家客戶的一般員工，在畫面上看到一個原廠範本檔的編號；② 對這個編號按刪除；③ 系統先把儲存空間裡的實體檔刪掉；④ 接著刪資料庫紀錄時被隔離規則擋下（不是原廠），但程式沒看結果，回「刪除成功」；⑤ 紀錄還在、檔案沒了，所有客戶打開這個範本都是壞檔 |
| **得手什麼** | 一個帳號就讓原廠給所有客戶的共用範本全部打不開 |
| **修正的做法** | 分兩步：① **原廠共用檔禁刪**：歸屬登記表裡，原廠共用檔對「寫入（刪除）」一律回「找不到」，只有平台管理員走例外口（CM-2033 接上歸屬檢查時一起做）；② **刪除順序反過來**：新開共用的 `record_first_delete.py`，先刪紀錄，**紀錄真的刪掉才刪實體**；實體刪失敗只記日誌（留孤兒實體檔比留壞紀錄好）；批次刪除刪完再查一次，只對真的刪掉的那幾筆動實體；衍生的 PDF 轉檔同樣改順序 |
| **狀態** | ✅ 已修（禁刪：CM-2033 主專案 commit `a334f5385`；刪除順序：CM-2228 套件 commit `5ec9b617`，1.21.0 出貨）。驗證：新增 10 條測試，突變改回「先刪實體」3 條轉紅 |

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

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #125；OWASP：A02 安全設定錯誤） |
| **在哪裡** | 共用基礎：正式環境的紀錄設定——資料庫對映與資料庫事件兩種運作紀錄 |
| **攻擊面位置** | 不是攻擊面：沒有安全問題（這兩種紀錄只寫檔案、不進資料庫），是設定本身讓紀錄量暴增、吃掉磁碟。任何正常運作都會觸發 |
| **信任邊界位置** | **正式設定 ↔ 開發設定**。正式環境的紀錄等級應該只留需要的；當時兩處被寫死成最詳細（除錯等級），連輸出到畫面的那一層也寫死最詳細，不跟隨紀錄等級設定 |
| **元件端點位置** | 不是端點，是設定檔：套件 `jedi-common/jedi_common/logger/config_prod.py`（`sqlalchemy.orm`、`pymongo.event_loggers` 兩個紀錄器與 `handlers.app.level`）；選哪份設定由 `logger/config_logger.py` 依 `RUN_ENV` 決定 |
| **駭客怎麼打** | ①（不是駭客）產品在正式環境正常開機運作；② 資料庫對映紀錄每一次設定都寫一行；③ 開機一小時倒出約 7,200 行，其中約 6,050 行是這一種；④ 日積月累吃滿磁碟，主機上其他服務跟著出問題，真正要看的紀錄也被淹沒 |
| **得手什麼** | 沒有人得手；是磁碟被紀錄塞滿 |
| **修正的做法** | ① 兩處紀錄器從除錯等級調回一般等級（CM-2065）；② 實測發現資料庫對映的設定紀錄本身就是一般等級、調回一般擋不住，再改成**只留警告以上**（CM-2270）；③ 輸出那一層不再寫死最詳細，改成「跟隨 `LOG_LEVEL`、但最多到一般等級」——寫死一般會堵住客戶開除錯排查的路，直接跟隨又會在設成警告時把業務紀錄也砍掉；④ 資料庫事件紀錄器沒有掛任何監聽，一般等級下零輸出，維持不動 |
| **狀態** | ✅ 已修（CM-2065 套件 commit `a9562dd0`；補修 CM-2270 套件 commit `bf97b904`）。驗證：開機約 10 秒，改前輸出 6,096 行（對映紀錄 6,049）、改後 51 行（對映紀錄 0）；設 `LOG_LEVEL=DEBUG` 時除錯紀錄照常出現 |

## M07-8 ⚪ 分類程式沒有記憶體與處理器上限，逾時也殺不掉 {#m07-8}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #131；OWASP：A06 不安全的設計） |
| **在哪裡** | 證據自動分類模組：系統開一個獨立的分類程式（容器）去拆解證據檔、交給 AI 判斷 |
| **攻擊面位置** | 需要登入且能對證據批次按「開始分類」的帳號（專案管理者），上傳一份做過手腳的壓縮檔。評低的前提：掃描當時落地版刻意沒裝分類程式的執行環境，客戶端根本啟動不了，只影響我們自己的開發機——這是產品決策不是技術事實，落地版一開放就必須先修 |
| **信任邊界位置** | **證據檔內容 ↔ 主機資源**。拆解不可信檔案的程式應該被關在有上限的籠子裡、逾時就能確實終止；當時沒有記憶體、處理器、程序數上限，逾時時只砍得掉本機的指令，容器本身照跑、照燒 AI 額度 |
| **元件端點位置** | 套件 `jedi_evidence_classification/api/routing.py`：`POST /evidence-batches/{batch_uid}/classify`（`EvidenceBatchClassifyRoute.post`）→ 開發機路徑 `infra/classifier_container_runner.py` 的 `run()`（直接起容器）；落地版路徑走常駐服務，上限在主專案 `docker/production/docker-compose.yml` 的 `guidant-classifier` |
| **駭客怎麼打** | ① 專案管理者把一份刻意做過手腳的壓縮檔放進證據批次；② 按開始分類；③ 分類程式解開的當下把主機記憶體吃光，連後端服務一起被系統砍掉；④ 逾時後容器沒被停掉，繼續跑、繼續消耗 AI 額度 |
| **得手什麼** | 讓主機上的後端服務被系統砍掉，並持續燒 AI 額度 |
| **修正的做法** | ① **落地版**改走常駐分類服務時，由部署設定直接限住：記憶體 2G、處理器 2 顆、程序數 256、唯讀檔案系統（CM-2223）；② **開發機直接起容器那條路**補同樣的上限，並給容器取名 `classifier-<工作編號>`；③ 逾時時對這個名字下「強制停止」，停不掉只記日誌、不蓋掉原本的逾時錯誤；④ 上限不必精算——只要不是無限大、留得夠給後端服務 |
| **狀態** | ✅ 已修（CM-2227，套件 commit `ecddb72c`＋`85370a17`；落地版常駐服務 CM-2223 主專案 commit `750b635de`，1.21.0 出貨）。驗證：真實容器檢查到記憶體 2G、處理器 2、程序數 256；4 秒逾時後容器確實已不在 |

## M19-3 ⚪ 設備清單一頁要顯示幾筆，沒有上限 {#m19-3}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #136；OWASP：A06 不安全的設計） |
| **在哪裡** | 設備與資訊系統清冊：設備清單頁的「一頁顯示幾筆」 |
| **攻擊面位置** | **任何一個登入帳號**，在設備清單的請求裡改一個數字 |
| **信任邊界位置** | **使用者 ↔ 資料庫**。根源不在這一塊，在所有模組共用的分頁設定（同 [M04-7](#m04-7)）；設備清單的請求格式繼承那一份，所以一起沒有上限 |
| **元件端點位置** | 套件 `jedi_asset/api/routing.py`：`POST /devices`（`DeviceListRoute`，請求格式 `DevicePageQueryRequest`）→ 繼承 `jedi-common/jedi_common/interfaces/schema/common.py` 的 `PagerSchema` |
| **駭客怎麼打** | ① 任何一個登入帳號，打開設備清單；② 把「一頁幾筆」改成超大數字；③ 資料庫把整張設備表一次撈出來 |
| **得手什麼** | 消耗資料庫與伺服器資源，重複打可拖慢整個產品 |
| **修正的做法** | 修在共用底層、一處全站生效：`PagerSchema.page_size` 加上限 1000（取值依據與盤點見 [M04-7](#m04-7)）；設備清單的請求格式繼承它，不必另改 |
| **狀態** | ✅ 已修（CM-2065，套件 commit `a9562dd0`，1.21.0 出貨） |

## 總表#145 ⚪ 公告查無此筆對空值設值、沒有登入身分時建立公告崩潰 {#r145}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #145；OWASP：A10 例外狀況處理不當） |
| **在哪裡** | 公告模組：修改公告、建立公告 |
| **攻擊面位置** | 掃描當時**沒有任何呼叫者**：主專案走自己那份公告程式，套件那份是死碼，所以打不到。兩處都只會讓該次請求 500，不是資料外洩也不是權限繞過 |
| **信任邊界位置** | **路由層 ↔ 服務層的身分傳遞**。服務層應該信任路由層交給它的「操作者是誰」，自己不去抓；當時服務層自己去取登入者再取 `.uid`，取不到時對空值取值直接崩潰。另一處是存在性檢查查錯對象 |
| **元件端點位置** | 套件 `jedi_bulletin/api/routing.py`：`POST /bulletin`（`BulletinCreateRoute`）、`PUT /bulletin/{uid}`（`BulletinDetailRoute.put`）→ `jedi_bulletin/app/service/bulletin_service.py` 的 `add_bulletin()`／`update_bulletin()`；存在性檢查在 `jedi_bulletin/infra/repository/bulletin_repo_impl.py` 的 `update()` |
| **駭客怎麼打** | ①（當時沒有入口，以下是有入口時的樣子）有人修改一則不存在的公告；② 程式想確認「這筆存在嗎」，卻檢查了傳進來的參數、不是查詢結果，以為存在；③ 下一行對空值設值，該次請求 500；④ 另一條：背景程式或排程在沒有登入身分的情況下建公告，取登入者編號時崩潰 |
| **得手什麼** | 沒有人得手；是該次請求失敗 |
| **修正的做法** | 公告整塊從主專案搬進套件時一併處理：① 存在性檢查改成檢查查詢結果（`if not bulletin_model`），查無此筆回空、上層轉成「公告不存在」；② 服務層不再自己抓登入者，改由路由層用 `current_user_login_name()` 取帳號傳進來，服務層只收「操作者是誰」這個參數 |
| **狀態** | ✅ 已修（FR-080 第 5 棒／CM-1625，套件 commit `3f27793f`）。⚠️ 該 commit 是整塊搬遷，訊息沒有點名本條，修正依據是逐行比對 diff 確認兩處都已改 |

## M11-34 ⚪ 幾 KB 的特製 YAML 範本讓伺服器展開到當掉 {#m11-34}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #180；OWASP：A06 不安全的設計） |
| **在哪裡** | 合規文件核心：合規資源庫的「匯入 YAML 範本」 |
| **攻擊面位置** | 需要有建立範本權限（`module-frame.create`）的管理員帳號；後果是暫停服務、不是外洩 |
| **信任邊界位置** | **使用者上傳的 YAML ↔ 解析程式**。YAML 支援「引用前面某一段」，解析器在讀檔階段就會把引用全部展開；當時用的安全讀法擋得住執行程式、擋不住引用炸彈（30 層、每層引用 10 次），之後的遞迴展開也沒有層數上限。檔案頂層不是預期格式時還會直接 500 |
| **元件端點位置** | 主專案 `api/module_frame/__init__.py`：`POST /module-frame/import/yaml`（`ModuleFrameYamlImportTemplateRoute`）→ `module_frame_import_service.import_yaml_file()` → `infra/module_frame/adapter/yaml_to_module_frame_parser_adapter.py`（`_NoAliasSafeLoader`、`_parse_workflow()`） |
| **駭客怎麼打** | ① 有建立範本權限的管理員；② 寫一個幾 KB 的 YAML，裡面一層引用前一層十次、疊 30 層；③ 上傳匯入；④ 伺服器展開時 CPU 與記憶體吃滿，一個請求佔住處理程序到逾時；⑤ 連送幾個，所有公司的使用者同時連不上 |
| **得手什麼** | 讓所有公司的使用者同時連不上 |
| **修正的做法** | ① **自訂讀檔器，遇到「引用」直接拒絕**——這是主修法，因為解析器在讀檔階段就展開，事後數層數擋不住；repo 內沒有任何範本用到引用，全拒不影響既有範本；② 遞迴展開加層數上限 10、流程與任務節點總數上限 5,000；③ 頂層不是物件、欄位型別不對一律回 400 格式錯誤，不再 500；④ 新錯誤碼撞號後改為 `MODULE_FRAME_400011`（格式）／`MODULE_FRAME_400012`（含引用或超限） |
| **狀態** | ✅ 已修（FR-114 CM-2190，主專案 commit `374392065`；錯誤碼改號 `3d97fcaf9`，1.21.0 出貨）。驗證：30 層引用炸彈 0 秒回 400；10 層通過、12 層擋下；6,000 節點擋下 |

## M11-35 ⚪ 範本匯入檢核送一格很長的怪字串，佔住處理程序兩分鐘 {#m11-35}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #181；OWASP：A06 不安全的設計） |
| **在哪裡** | 合規文件核心：合規資源庫 Excel 匯入前的「檢核」——檢查條款與要求事項欄位有沒有夾帶網頁標籤 |
| **攻擊面位置** | 需要有建立範本權限（`module-frame.create`）的管理員帳號 |
| **信任邊界位置** | **使用者送的欄位內容 ↔ 比對規則**。檢查網頁標籤的規則應該限制長度；當時規則可以在一格超長、充滿左角括號的字串裡反覆重試 |
| **元件端點位置** | 主專案 `api/module_frame/__init__.py`：`POST /module-frame/import/verify`（`ModuleFrameImportDataVerifyRoute`）→ `app/module_frame/service/module_frame_import_service.py` 的 `verify_import_module_frame_data()`（常數 `HTML_TAG_PATTERN`） |
| **駭客怎麼打** | ① 有建立範本權限的管理員；② 在檢核請求的條款欄送一格一百萬個左角括號的字串；③ 比對規則卡住，一個請求佔住處理程序兩分鐘；④ 四個請求卡住全部處理程序 |
| **得手什麼** | 用四個請求讓整個系統沒回應 |
| **修正的做法** | ① 標籤比對規則改成「中間不得再出現角括號、最長 200 字」，不再反覆重試；② 條款或要求事項超過 2,000 字直接判不合格，沿用同檔的錯誤收集寫法、不丟例外 |
| **狀態** | ✅ 已修（FR-114 CM-2181，主專案 commit `48638cbdb`，1.21.0 出貨）。驗證：送一百萬個左角括號 0.07 秒內回判不合格 |

## M11-36 ⚪ 頁數極多的 CMMC PDF 讓解析時記憶體撐爆 {#m11-36}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #192；OWASP：A06 不安全的設計） |
| **在哪裡** | 合規文件核心：原廠匯入 CMMC 合規框架 PDF |
| **攻擊面位置** | 需要是**平台管理員**（框架是全域資源，服務層 `require_platform_admin()`） |
| **信任邊界位置** | **上傳的 PDF ↔ 解析程式**。開檔後應該先擋總頁數、每頁讀完就釋放；當時頁數不設限，而且每頁讀完不關，記憶體隨頁數直線成長 |
| **元件端點位置** | 主專案 `api/oscal/__init__.py`：`POST /oscal-framework-parse-jobs/parse`（`FrameworkParseJobParseRoute`）→ `app/oscal/service/framework_parse_job_service.py` → 套件 `jedi_oscal_v2/infra/adapter/cmmc/cmmc2_lv2_parser_adapter.py`；頁數檢查 `infra/adapter/base_parser_adapter.py` 的 `_assert_page_count()` |
| **駭客怎麼打** | ① 平台管理員帳號；② 上傳一份 3MB、兩萬頁的 PDF 當框架；③ 解析時記憶體直線成長，吃到 3.4GB；④ 處理程序被砍 |
| **得手什麼** | 讓解析程序記憶體耗盡 |
| **修正的做法** | ① 新增頁數上限 3,000 頁，開檔後先擋，超過回 400（`OSCAL_V2_400006`）；② 兩輪取頁每頁用完就關閉釋放 |
| **狀態** | ✅ 已修（FR-114 CM-2184，套件 commit `ae5ffff5`，1.21.0 出貨）。驗證：兩萬頁 3.26MB 修前 7.1 秒／973MB、修後 1.6 秒拒絕／167MB；2,999 頁照常解析；真實 CMMC 修前修後解析結果逐位元組相同 |

## M11-37 ⚪ PDF 裡一行超長的字讓「是不是目錄」的比對卡住 {#m11-37}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #193；OWASP：A06 不安全的設計） |
| **在哪裡** | 合規文件核心：CMMC PDF 解析時判斷「這一行是不是目錄」 |
| **攻擊面位置** | 需要是**平台管理員**，同 M11-36 |
| **信任邊界位置** | **PDF 內容 ↔ 比對規則**。判斷目錄的規則與取「[SELECT FROM:」標記後文字的規則遇到超長的一行會反覆重試；應該先限長度或改成不回頭的寫法 |
| **元件端點位置** | 同 M11-36 的 `POST /oscal-framework-parse-jobs/parse` → 套件 `jedi_oscal_v2/infra/adapter/cmmc/cmmc2_lv2_parser_adapter.py` 的 `_is_toc_noise()` |
| **駭客怎麼打** | ① 平台管理員帳號；② 做一份 0.04MB、五頁、每頁一行四萬字的 PDF；③ 上傳；④ 解析跑 52 秒，約十頁就超過逾時 |
| **得手什麼** | 佔住處理程序到逾時 |
| **修正的做法** | ① 一行超過 500 字直接當內文、不跑目錄比對；② 取「[SELECT FROM:」標記後文字原本用「前面任意字＋標記」的取代，改成逐一找出標記、取最後一個之後的字，語意相同、時間線性 |
| **狀態** | ✅ 已修（FR-114 CM-2184，套件 commit `ae5ffff5`，1.21.0 出貨）。驗證：五頁每頁四萬字修前 52.3 秒、修後 4.4 秒 |

## M12-8 ⚪ 稽核計畫 Word 塞幾萬個空白，解析比對卡到逾時 {#m12-8}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #196；OWASP：A06 不安全的設計） |
| **在哪裡** | 稽核輪次管理：稽核計畫 Word 解析器的三條比對規則（標題日期、時段、單位） |
| **攻擊面位置** | 需要是某個專案的稽核員或管理者，上傳一份格式像樣的 Word |
| **信任邊界位置** | **檔案內容 ↔ 比對規則**。段落文字進比對之前應該先截長度；當時三條規則遇到大段空白會來回重試，長度加倍、時間變四倍 |
| **元件端點位置** | 主專案 `POST /ap/{ap_uid}/docx-imports/parse`（`ApDocxImportUploadRoute`）→ 套件 `jedi_compliance_audit/app/service/ap_report_parser/airasia_cmmc_l1_v1.py` |
| **駭客怎麼打** | ① 某專案的稽核員或管理者；② 在稽核計畫 Word 的標題或時段那一段塞幾萬個空白；③ 上傳；④ 2 萬字時標題日期規則就跑 20.7 秒，幾十 KB 的檔就能卡到逾時 |
| **得手什麼** | 佔住處理程序到逾時 |
| **修正的做法** | ① 段落先截 2,000 字；② 時間與單位規則比對前把連續空白壓成一個；③ 標題日期只看標題最後 40 字 |
| **狀態** | ✅ 已修（FR-114 CM-2181，套件 commit `2cb9d2cd`、主專案 commit `48638cbdb`，1.21.0 出貨）。驗證：標題與單位段各塞 2 萬空白，修前 5.77 秒、修後 0.01 秒，解析出的單位相同 |

## M24-16 ⚪ 首次安裝精靈設定碼塞中文或特殊字元，伺服器回 500 {#m24-16}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #214；OWASP：A10 例外狀況處理不當） |
| **在哪裡** | 主系統：新機第一次開站的安裝精靈，要求輸入安裝程式印出來的設定碼 |
| **攻擊面位置** | **不需要帳號**，但只在系統還沒開通時打得到（開通後這組入口就封了）。任何連得到這台新機的人都構成攻擊面 |
| **信任邊界位置** | **未登入的外部 ↔ 開站入口**。比對設定碼時應該對任何輸入都安全地回「不對」；當時用的比對函式遇到非英數字（中文、表情符號、網頁伺服器解出的高位字元）會直接拋錯，變成 500 |
| **元件端點位置** | 主專案 `api/setup/__init__.py`：`GET /setup/status`、`POST /setup/provision`（`api/setup/routes/setup_route.py`，`X-Setup-Token` 標頭）→ `common/setup/setup_token.py` 的 `verify_token()` |
| **駭客怎麼打** | ① 在客戶剛裝好、還沒開通的時候連到這台機器，不需要帳號；② 在設定碼標頭裡塞中文或特殊字元送出；③ 比對函式拋錯，伺服器回 500；④ 重複送，錯誤警報被灌滿，真正的錯誤被淹掉。不會放行、也不會洩漏設定碼 |
| **得手什麼** | 用假的錯誤警報淹沒真的錯誤 |
| **修正的做法** | ① 比對前兩邊都先轉成位元組再比（固定時間比對不變）；② 轉不過去（孤立的特殊字元）或型別不對一律回「不符」，維持失敗就拒絕。實際做法與模組頁原建議的「只收英數字」不同：改成「任何字元都能安全比對、不符就拒」，效果相同且不必另外維護字元白名單 |
| **狀態** | ✅ 已修（8-E，CM-2215，commit `822216b6c`，1.21.0 出貨）。驗證：帶「中文」「ÿéü」狀態查詢回 200、開通回 403，日誌 0 個錯誤堆疊 |

## M24-15 ⚪ 雲端硬碟通知說有去重複、實際沒做 {#m24-15}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #219；OWASP：A10 例外狀況處理不當） |
| **在哪裡** | 主系統：Google 雲端硬碟有變動時推通知過來，系統排一個同步工作 |
| **攻擊面位置** | 對外開放的通知接收口，**不帶使用者登入**（是 Google 打進來），靠通道編號＋通道密鑰比對才收。能送出有效通知的只有 Google 或握有密鑰的人 |
| **信任邊界位置** | **Google ↔ 我們後端**。通知通過密鑰比對後，每一則都排一個同步工作；程式說明寫「工作端會依待辦狀態去重複」，實際排工作時沒有檢查是否已有同一租戶的待辦工作 |
| **元件端點位置** | 主專案 `api/cloud_integration/__init__.py`：`POST /webhooks/google-drive/{tenant_id}`（`GoogleDriveWebhookRoute`）→ `app/cloud_integration/service/google_drive_webhook_service.py` 的 `handle_notification()` → `domain/cloud_integration/service/drive_sync_job_domain_service.py` 的 `enqueue()`（直接新增一筆，不查重複） |
| **駭客怎麼打** | ①（多半是 Google 正常行為）雲端硬碟短時間內推來多則通知；② 每一則都通過驗證、各排一個同步工作；③ 同步多跑幾次。同步本身可以重跑、不會出錯，只多耗一點資源 |
| **得手什麼** | 沒有人得手；只是多跑幾次同步 |
| **修正的做法** | **不修的理由**：重複通知只會讓同步多跑一次，同步本身可重跑、不會出錯，屬程式品質問題，不影響資料或權限 |
| **狀態** | 🚫 裁定不修 |

## M24-17 ⚪ 匯出診斷包時一邊帶時區一邊不帶，程式當掉回 500 {#m24-17}

| 欄位 | 內容 |
|---|---|
| **嚴重度** | ⚪ 低（總表 #227；OWASP：A10 例外狀況處理不當） |
| **在哪裡** | 主系統：客戶的平台管理員在畫面上選時間範圍、下載診斷包給原廠排查 |
| **攻擊面位置** | 需要登入且是**平台管理員**（服務層呼叫 `require_platform_admin()`）。多半是正常操作踩到，不是攻擊 |
| **信任邊界位置** | **使用者輸入 ↔ 服務層**。兩個時間在比較前應該先統一格式；當時只帶起始時間（帶時區）、不帶結束時間時，程式自己補的結束時間不帶時區，兩者一比就拋錯變 500 |
| **元件端點位置** | 主專案 `api/support/__init__.py`：`POST /support/diagnostic-bundle`（`DiagnosticBundleRoute`）→ `app/support/service/diag_bundle_export_service.py` 的 `_resolve_window()` |
| **駭客怎麼打** | ①（正常操作就會踩到）平台管理員要匯出診斷包；② 只填起始時間、而且是帶時區的格式，結束時間留空；③ 程式比較兩個時間時拋錯，回 500；④ 診斷包匯不出來，排查被耽誤。不會外洩任何東西 |
| **得手什麼** | 沒有人得手；是維運拿不到診斷包 |
| **修正的做法** | ① 比較前兩個時間都先過打包核心既有的 `_as_naive_local()`，統一成同一種格式（沿用同一支、不另寫）；② 不合法的範圍照舊回 400、不夾成邊界值——靜默夾住會讓人以為抓了七天、實際只有一天 |
| **狀態** | ✅ 已修（8-D，CM-2214，commit `d39181f3b`，1.21.0 出貨；同卡退回補修的 `046553756` 屬同卡另一條主機指令問題、與本條無關）。驗證：起始帶時區＋結束不帶、結束帶 UTC＋起始不帶，都正常回傳 |
