Guidant AI 資安檢視總報告 · OWASP Top 10(2025)
命中這一類的 46 件,還原成模組頁的 48 條原始條目。每一條用白話寫出:在哪裡、攻擊面、信任邊界、元件端點、駭客怎麼打、得手什麼、修正的做法。
設計上就少了一道限制——設計錯了,實作再完美也補不回來。 OWASP 官方涵蓋的代表弱點有:頻率控制不當(CWE-799)、攻擊面過大(CWE-1125)、流程順序沒強制(CWE-841)、同時送兩次的競態(CWE-362)、信任邊界混淆(CWE-501)、無限制上傳(CWE-434)、只靠前端把關(CWE-602)(定義見兩套分類是什麼)。官方 2025 版沒有收「資源耗盡」(CWE-400/770),本報告依精神歸在這一類。
這次掃到的 48 條,在本專案長成三種形狀:
這一類多數條目同時屬於 STRIDE 的「讓服務停擺」,修法也幾乎一個模式:在入口定上限,超過就拒絕;上限值查過現況最大用量再定,不留可繞過的參數。
條目編號:標題用模組頁編號(例如 M06-3 是稽核流程模組頁第 3 條),括號內是總結問題總表編號。總表把同一套修法的條目併成一件(例如 #13 併了 M02-4 與 M03-13、#88 併了 M06-9 與 M06-10),本頁拆回模組頁原始條目,所以是 48 條、總表是 46 件。嚴重度一律用總表燈號。
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟠 高(總表 #13,與 M03-13 併為一件;STRIDE:冒充身分、權限提升) |
| 在哪裡 | 檔案上傳下載模組:證據、附件的「預覽」功能 |
| 攻擊面位置 | 已登入使用者可打的檔案上傳與預覽 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 強制下載 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟠 高(總表 #13,與 M02-4 併為一件;STRIDE:冒充身分、權限提升) |
| 在哪裡 | 弱點檢測整合模組:代理程式把掃描報告回傳給後端、存成證據 |
| 攻擊面位置 | 掃描報告的兩個檔名來源——呼叫端帶的檔名提示、代理程式回應標頭裡的檔名——都在系統外面。網頁格式正是掃描報告的正式格式,這是日常路徑,不是罕見情境 |
| 信任邊界位置 | 客戶機房的代理程式 ↔︎ 我們後端。後端收到報告時應該自己決定檔名與格式;當時直接採用外面給的檔名,沒去掉路徑、也沒查副檔名,接著這份檔案被寫成證據,就進了 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/空字串退回預設名 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟠 高(總表 #16;STRIDE:讓服務停擺) |
| 在哪裡 | 稽核流程模組:系統推算「下一步輪到誰」的那段程式——打開稽核輪次頁面、完成或退回任務、啟動一輪稽核時都會跑 |
| 攻擊面位置 | 需要登入且有「流程範本新增/修改」權限,把一張特製的流程圖存進去。存進去之後攻擊者什麼都不用做——任何看得到那個專案的人打開稽核輪次頁面,就替他觸發了。也不一定要有人存心:流程畫得太複雜的客戶可能無意間做出同樣的效果 |
| 信任邊界位置 | 使用者畫的流程圖 ↔︎ 伺服器的推算程式。流程圖是使用者提供的資料,推算程式應該把它當不可信輸入,走訪時記住走過哪裡、設深度上限;當時兩支函式互相呼叫、不記走過的連線,每多一層分岔下游就整段重算一次,工作量一層層相乘。而存檔時的兩道檢查擋的是「繞回自己的圈」與「分岔沒寫條件」,這張圖兩道都過得了 |
| 元件端點位置 | 推算核心在套件 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:起輪次三條取範本路徑的匯合處檢查範本狀態,未發布一律擋下) |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟠 高(總表 #21;STRIDE:權限提升、資料外洩) |
| 在哪裡 | AI 儀表板模組:使用者打一句需求,AI 從 26 支查詢裡挑一支、再設計圖表 |
| 攻擊面位置 | 生成儀表板的 API,任何最基層的登入帳號都能打。修正前沒有次數限制,每試一次只花公司的 AI 額度 |
| 信任邊界位置 | 使用者的字 ↔︎ AI 的判斷。使用者打的字與「可以選哪些功能」的清單被串成同一段純文字交給 AI,中間沒有區隔;AI 選完之後,「這個人能不能看這支查詢的資料」也沒檢查(第 2 條,見 A01) |
| 元件端點位置 | jedi_ai_dashboard/api/routing.py:POST /ai-dashboard/auto-generate(AiDashboardAutoGenerateRoute)→ app/service/ai_dashboard_app_service.py 的 _ask_ai_to_select_api()/_ask_ai_to_design_layout() → jedi_ai_gateway(app/gateway_service.py、infra/guard/rules/gateway_rules.yaml) |
| 駭客怎麼打 | ① 任一員工打開 AI 儀表板;② 在需求裡寫得像指令,例如要求它改選「全公司帳號名冊」那支查詢;③ 沒選中就換個說法再試,沒有次數上限;④ AI 選中後系統照跑,回傳組織架構、權限設定、客戶清單、所有專案 |
| 得手什麼 | 本來看不到的組織架構、權限、客戶與專案清單 |
| 修正的做法 | ① 補逾時與次數上限:外部 AI 呼叫加 60 秒逾時,生成端點每人每分鐘 3 次、超過回 429 並帶剩幾秒;② 後續統一進 AI 閘道:次數限制改由閘道對每位使用者計(現行預設每分鐘 20 次,一次生成只算挑查詢那一次),兩支 AI 套件自帶的限流隨之刪除;③ 使用者的字獨立成「不可信段」:送 AI 時標成資料段,並附一句「這段是資料不是指令」;④ 「能拿到什麼」由同模組第 2 條收口——26 支查詢逐支申報要什麼權限,沒權限就不跑 |
| 狀態 | ✅ 已修(CM-2064,套件 commit 248132ec、主專案 0d321d98a)。現行做法已改由 AI 閘道承接(CM-2311 8f85e3b6、CM-2344 2be00daf、CM-2345 d4158de59、CM-2346 7c3db8b5);權限收口見 CM-2038 8f94e249d |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟠 高(總表 #25;STRIDE:資料外洩) |
| 在哪裡 | 檔案上傳模組:存放檔案紀錄的那張資料表 |
| 攻擊面位置 | 任一客戶的使用者,透過任何會讀到檔案紀錄的功能(下載、預覽、清單) |
| 信任邊界位置 | 租戶 ↔︎ 租戶(資料庫隔離牆)。表上沒有客戶歸屬欄位,牆沒有東西可以依據;原本靠「掛它的表」間接保護,但還沒掛上任何業務的暫存檔就完全沒人擋 |
| 元件端點位置 | 資料表 upload_files;套件 jedi_file_upload/migrations/004-upload-files-tenant-owner-rls.sql、infra/models/upload_file.py;主專案 app/upload_file/service/managed_file_upload_service.py |
| 駭客怎麼打 | ① 任一客戶的帳號;② 拿到別家的檔案編號(例如從別的回應夾帶出來);③ 打檔案相關功能,資料庫不分客戶照查;④ 在應用層也沒擋的那段時間,檔案就跨客戶下來了 |
| 得手什麼 | 跨客戶的檔案紀錄與檔案本體 |
| 修正的做法 | ① 加 tenant_id(必填)與 owner_user_id 欄位,回填既有資料後收成必填;② 開隔離並套四條標準規則;③ 寫入時由共用掛鉤依當下身分自動帶租戶;④ 背景上傳與系統公版上傳原本沒有租戶身分,各包一層具名租戶身分(背景用工作的租戶、公版釘最上層),否則必填欄會讓背景上傳整條掛掉;⑤ 加守衛測試。同客戶內跨專案另由 M02-1 的歸屬檢查擋 |
| 狀態 | ✅ 已修(CM-1850,套件 651e33c2/BE a48b41cc0;2,718 筆全數補上客戶歸屬) |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟠 高(總表 #33;STRIDE:權限提升、竄改資料) |
| 在哪裡 | 授權管理模組:平台把欠費或違約的客戶「停權」(產品轉唯讀),以及客戶上傳/啟用授權檔 |
| 攻擊面位置 | 已登入的客戶帳號可打的授權上傳與啟用 API。其中啟用的幾條只要登入、不需管理員——這是刻意的,開通當下客戶常還沒有管理員在場 |
| 信任邊界位置 | 客戶送來的授權檔 ↔︎ 平台的停權決定。停權原本記在「那一張授權檔」的欄位上,而換照是「新增一張、取代舊的」,新的那一張停權欄位是空的。停權該是平台對「這個客戶」的決定,不該跟著客戶自己能換掉的資料走 |
| 元件端點位置 | jedi_license_runtime/api/routing.py:POST /api/1.0/license/upload、/license/activation/upload、/license/activation/online → LicenseVerificationService._store_verified_license();停權 POST /license/tenants/{uid}/lock → TenantLicenseAdminService.force_lock_tenant();執法 common/authz/license.py:LicenseGuard.snapshot_for() |
| 駭客怎麼打 | ① 平台把某客戶停權,產品轉唯讀;② 該客戶任一登入帳號把手上那張舊授權檔(或任何一張驗章會過的照)重新上傳;③ 系統建一筆新的授權紀錄取代現行照,新列的停權欄位是空的;④ 唯讀當場解除,不需要平台同意 |
| 得手什麼 | 被停權的客戶自行復權,平台的最後商務手段形同無效 |
| 修正的做法 | ① 把停權從授權檔搬到客戶身上:新表 config.tenant_license_suspensions,一列=該客戶目前被停權,解除就刪那一列,與授權檔完全脫鉤,換幾次照都動不到它;② force_lock_tenant()/unlock_tenant() 改寫新表;LicenseGuard.snapshot_for() 與 LicenseStatusService.get_my_license_status() 改讀新表;到期推進改批次查新表決定哪些客戶整列跳過;③ 舊照上的兩個停權欄位保留不刪,當「當初那一份被停權過」的歷史紀錄;④ 新表一出生帶著寫反的資料庫門禁,子單位可直接刪掉母公司的停權紀錄——另案修正(見授權管理第 8 條,CM-2271/CM-2369) |
| 狀態 | ✅ 已修(CM-1579,BE commit a039aa287、套件 8f9155a9)。驗證:對 DEV 實跑停權 → 換照 → 停權仍在 → 解除 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟠 高(總表 #34;STRIDE:讓服務停擺) |
| 在哪裡 | 授權管理模組:離線開通——把原廠回寄的授權檔上傳進來 |
| 攻擊面位置 | 任何一個登入帳號都打得到:這支是刻意設計成「任何登入者可用」(授權過期的客戶唯一自救路徑),而且可以無限重複送 |
| 信任邊界位置 | 使用者上傳的檔案 ↔︎ 驗章程式。授權檔要先解壓才能驗章(簽章簽的是解開後的原文,順序不能對調),所以「還沒驗證過的資料」會先進到解壓這一步;這一步應該邊解邊設上限,當時完全沒有上限,在簽章來得及發揮保護之前記憶體就吃光了 |
| 元件端點位置 | 套件 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 條測試(含記憶體量測),突變拿掉修法兩條轉紅 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟠 高(總表 #149;STRIDE:讓服務停擺)。全案唯一不必登入就能讓產品停擺的一條 |
| 在哪裡 | 共用基礎:每個請求進來時,系統先把請求內容「遮密碼」再記進操作日誌——所有網址都經過這一段,包含不存在的網址 |
| 攻擊面位置 | 網路上任何人,不需要帳號。這一段跑在檢查身分之前,所以連登入都不必,打一個不存在的網址也會中 |
| 信任邊界位置 | 匿名網路 ↔︎ 後端入口。在還不知道對方是誰之前,系統應該只做便宜、有上限的事;當時卻把整份請求內容丟進一段比對規則去遮密碼,而且一個請求遮兩遍。那段規則把欄位名寫成「前後各一段可任意長、夾著關鍵字」,遇到特定形狀的內容耗時隨長度平方成長 |
| 元件端點位置 | 任何路徑(例如不存在的網址)。主專案 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 秒內的效能測試,換回舊規則即轉紅 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟡 中(總表 #40;STRIDE:讓服務停擺) |
| 在哪裡 | 稽核流程模組:流程範本編輯器的「檢查流程圖」 |
| 攻擊面位置 | 已登入使用者可打的流程範本 API。任一登入帳號 |
| 信任邊界位置 | 使用者 ↔︎ 流程服務。同檔其他寫入都掛能力點,只有這支沒有;送進來的流程圖內容也沒有長度、節點數限制 |
| 元件端點位置 | 主專案 api/flow_engine/routes/flow_template_route.py:POST /api/1.0/flow-engine/flow-templates/validate(FlowTemplateValidateRoute.post)→ flow_template_app_service.validate |
| 駭客怎麼打 | ① 任一登入帳號;② 不必有範本權限,直接呼叫檢查流程圖;③ 丟一份很大、節點很多的流程圖;④ 系統照收照解析(實測一千節點 0.027 秒,打不到全站停擺,但沒權限的人能用、能塞大東西) |
| 得手什麼 | 無權限使用該功能並送入超大內容 |
| 修正的做法 | ① 路由補 flow_template.update 能力點;② 請求內容加 35 萬字上限(依現有最大範本放大推得);③ 服務層新增節點數 100、分岔深度 8 的上限,走訪時記錄造訪過的節點,遇環不無限遞迴;④ 超限仍回 200+警告清單,維持前端既有的顯示方式 |
| 狀態 | ✅ 已修(FR-114.1-8,CM-2031,BE aa04ca1a7) |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟡 中(總表 #59;STRIDE:資料外洩) |
| 在哪裡 | 共用取資料底層:所有套件共用的資料存取基底 |
| 攻擊面位置 | 任一登入帳號,對任一支把請求條件原樣丟給底層的查詢 |
| 信任邊界位置 | API 層 ↔︎ 資料存取層。底層把「全空」解讀成「不過濾」,API 層沒有一支擋空查詢 |
| 元件端點位置 | jedi_common/session/database/repository/base_repository_impl.py(if not filter_dict 那段);受影響的端點見 M05-1、M05-3~M05-5、M10-1、M10-5 |
| 駭客怎麼打 | ① 任一登入帳號(資訊不足,僅依總表描述展開);② 挑一支查詢送空白條件;③ 底層看到條件全空就不加過濾;④ 回來整張表 |
| 得手什麼 | 整張表的資料(視接的是哪支查詢) |
| 修正的做法 | 決策者裁定「逐支補、底層不動」:① 有洞的查詢各自把關鍵編號改必填並補歸屬檢查(問卷六組 CM-2034、成員與指派三支 CM-2036);② 底層規則維持原樣;③ 反悔條件:新功能第四次撞到同一件事就改底層 |
| 狀態 | ✅ 已修(逐支補:CM-2034 套件 83fc34e5、CM-2036 套件 3ed750cd) |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟡 中(總表 #76;STRIDE:讓服務停擺) |
| 在哪裡 | 弱點檢測整合模組:建立檢測基準(規則包)——可以上傳壓縮檔,也可以填一個網址讓系統去下載 |
| 攻擊面位置 | 需要登入且有「建立檢測基準」權限的帳號(客戶的管理員層級)。選「填網址」這條路就能把一個惡意壓縮檔放在自己的網站上給系統抓 |
| 信任邊界位置 | 外部網址的內容 ↔︎ 解析程式。防壓縮炸彈的三道上限(檔案數/解開總量/膨脹倍數)應該放在兩種來源都會經過的那一層;當時只接在「上傳檔案」那條路,網址下載回來直接解開,三道一道都不會碰到 |
| 元件端點位置 | 套件 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 被擋 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟡 中(總表 #77;STRIDE:讓服務停擺) |
| 在哪裡 | 弱點檢測整合模組:上傳檢測規則包時的「最多一萬個檔」檢查 |
| 攻擊面位置 | 需要登入且有「建立檢測基準」權限的帳號,上傳一個特製的 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 逐筆計數擋下 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟡 中(總表 #80;STRIDE:讓服務停擺) |
| 在哪裡 | 弱點檢測整合模組:檢測基準的「手動重新解析」按鈕(解析失敗後的補救途徑) |
| 攻擊面位置 | 需要登入且有「建立檢測基準」權限的帳號。按鈕每按一次就是一個請求,可以用程式連打 |
| 信任邊界位置 | 使用者請求 ↔︎ 背景工作。開背景工作之前應該先問「這一版是不是已經在跑」與「現在總共跑了幾條」;當時每收一次請求就開一條,沒有任何上限。而且這支會先把狀態壓回「等待中」再排工作——如果只在後面加判斷,判斷永遠看到「等待中」、擋不到任何東西 |
| 元件端點位置 | 套件 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 後拒收 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟡 中(總表 #81;STRIDE:讓服務停擺) |
| 在哪裡 | 弱點檢測整合模組:任務綁定檢測工具時的「要掃哪些機器」,以及按下執行時把網段逐台展開 |
| 攻擊面位置 | 需要登入且能新增或修改專案任務的帳號(專案管理者層級) |
| 信任邊界位置 | 使用者送的部分更新 ↔︎ 已存的綁定。台數上限應該用「這次更新完成後實際會生效的值」重新檢查;當時只看「這次有沒有送範圍」,沒送就沿用舊值、不重驗。而展開網段的那一端註明「這裡刻意不擋」,完全相信前面已經檢查過 |
| 元件端點位置 | 主專案 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 台被擋 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟡 中(總表 #83;STRIDE:讓服務停擺) |
| 在哪裡 | 共用基礎:全站所有清單畫面共用的「第幾頁、一頁幾筆」設定——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 出貨)。驗證:新增測試,突變拿掉上限即轉紅 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟡 中(總表 #85;STRIDE:事後無法追查、冒充身分) |
| 在哪裡 | 問卷模組:多人即時協作填答——一人改一題,同房間其他人即時看到 |
| 攻擊面位置 | 即時通訊(SocketIO)的填答通道。需要是這份問卷的合法填答者(連線時驗登入、每個事件驗房間成員);不是越權,是自己人造假 |
| 信任邊界位置 | 使用者瀏覽器送來的訊息內容 ↔︎ 伺服器寫入的稽核欄位。誰改的應該由伺服器從登入身分決定;當時直接拿訊息裡的 user 欄位寫進 created_user/updated_user |
| 元件端點位置 | SocketIO namespace /socket/fill-survey(config/socketio_namespaces.py 註冊)事件 update → jedi_survey/app/handler/fill_survey_socketio_handler.py:FillSurveySocketioHandler.on_update() → question_answer_service.patch_task_survey_answer() |
| 駭客怎麼打 | ① 合法填答者 A 進入多人填答房間;② 改一題答案時,在送出的訊息裡把 user 改成同事 B 的帳號;③ 伺服器照收、寫成「B 改的」,並廣播給房間裡所有人;④ 事後追查「誰改了這一題」,紀錄指向 B |
| 得手什麼 | 把自己的修改署成別人的名字,填答稽核軌跡失真 |
| 修正的做法 | ① on_update() 的署名改讀 get_user_context().login_name——AuthenticatedNamespace 每個事件都已注入登入身分,REST 的單題更新本來就是這樣取;② 廣播出去的 user 也一併變成真實帳號 |
| 狀態 | ✅ 已修,1.21.0 出貨(CM-2060,套件 commit 54ac36f7)。驗證:問卷套件 354 測試通過 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟡 中(總表 #86;STRIDE:竄改資料) |
| 在哪裡 | 問卷模組:問卷資料夾的修改與刪除 |
| 攻擊面位置 | 已登入、可管理問卷資料夾的帳號可打的資料夾 API |
| 信任邊界位置 | 使用者送來的修改內容 ↔︎ 資料夾的刪除狀態。「修改名稱/說明」與「改變刪除狀態」是兩件事;刪除走專用入口、有「夾內還有問卷就不准刪」的守門。修改入口當時把欄位檢查關掉(apply=False)、把整包請求展開進服務層,程式裡也沒補驗——宣告的格式等於裝飾品 |
| 元件端點位置 | jedi_survey/api/routing.py:PUT /api/1.0/survey-folder/{uid}(SurveyFolderRoute.put)→ jedi_survey/app/service/survey_folder.py:update_folder();對照的守門在 DELETE /survey-folder/{uid} |
| 駭客怎麼打 | ① 可管理問卷資料夾的使用者;② 對一個裡面還有問卷的資料夾送「修改」請求,內容除了名稱再多加一個「已刪除=是」欄位;③ 修改入口照單全收寫進資料庫;④ 資料夾被軟刪除,刪除入口的「夾內非空不可刪」守門完全沒被碰到 |
| 得手什麼 | 造出「資料夾已刪、問卷還掛在底下」的孤兒資料 |
| 修正的做法 | ① 新增嚴格格式 SurveyFolderUpdateRequest:只收名稱與說明,未知欄位直接丟棄;② 路由改由框架解析後具名帶進服務層,不再展開整包請求;③ update_folder() 由「收任意欄位」改成具名參數,沒送的欄位沿用現值——否則未帶的「已刪除」「上層資料夾」會被預設值覆寫成 0/空 |
| 狀態 | ✅ 已修,1.21.0 出貨(CM-2060,套件 commit 54ac36f7)。驗證:問卷套件 354 測試通過 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟡 中(總表 #88,與 M06-10 同一件;STRIDE:竄改資料) |
| 在哪裡 | 稽核流程模組:流程範本(決定一輪稽核走哪些步驟的流程圖)的新增與修改 |
| 攻擊面位置 | 已登入、有流程範本建立或修改權限的人 |
| 信任邊界位置 | 使用者送來的流程圖 ↔︎ 資料庫裡的範本。只有「發布」那一支會驗流程圖(結構、拓撲),新增與修改都不驗——沒檢查過的內容存得進資料庫;能讓伺服器永遠算不完的惡意流程圖(M06-3)從這個門進來不會被攔。它本身不是洞,是讓別的檢查失效的放大器 |
| 元件端點位置 | 主專案 api/flow_engine/__init__.py:POST /api/1.0/flow-engine/flow-templates(FlowTemplatesRoute)、PUT /api/1.0/flow-engine/flow-templates/{uid}(FlowTemplateRoute)→ app/flow_engine/service/flow_template_app_service.py 的 create()/update();對照的 publish() |
| 駭客怎麼打 | ① 有範本修改權限的人;② 畫一張分岔點繞回自己、或串幾十個分岔點的流程圖,存成草稿;③ 新增或修改都不驗,照存;④ 之後任何讀到這份範本、要算「下一步是什麼」的地方都卡死——或者再搭配 M06-10,直接拿這份草稿起一輪真實稽核 |
| 得手什麼 | 把沒檢查過、甚至惡意的流程圖存進資料庫,繞過發布時的檢查 |
| 修正的做法 | ① create()/update() 補上 publish() 已有的兩支驗證 _validate_bpmn+_validate_bpmn_topology,先查再寫;② 規模上限 _check_size_limits() 原本只掛在公開的「檢查」端點,一併掛進拓撲驗證,所有寫入路徑都受節點數與分岔深度上限管;③ 新增 _find_self_loop() 擋「節點連回自己」——不擋所有環,正常的送審退回本來就走回上一節點,出貨內建範本就有;④ 原本明文守「新增不准驗」的測試反過來改成「新增會拒絕壞圖」,代價是畫到一半的流程圖存不了草稿 |
| 狀態 | ✅ 已修,1.21.0 出貨(CM-2059,主專案 commit 8edbaf944)。驗證:DEV 27 份範本修前修後走訪結果完全一致、0 份被新上限擋下;攻擊圖修前永不返回、修後在寫入端被擋 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟡 中(總表 #88,與 M06-9 同一件;STRIDE:竄改資料) |
| 在哪裡 | 稽核流程模組:開一輪稽核時,系統複製流程範本當成這一輪的流程 |
| 攻擊面位置 | 已登入、能建立稽核輪次的人(專案管理者) |
| 信任邊界位置 | 草稿範本 ↔︎ 真實運作的稽核流程。取範本的查詢只過濾「啟用中」、沒過濾「已發布」,一份從沒檢查過的草稿也能被複製去啟動真實流程——與 M06-9 合起來,等於前面講的檢查連「一定會被跑到」都做不到 |
| 元件端點位置 | 主專案 api/project/__init__.py:POST /api/1.0/projects/{project_uid}/audit-rounds(AuditRoundsRoute)→ 套件 jedi_compliance_audit/app/service/audit_round_app_service.py 的 _resolve_flow_template_uid()(三條來源:顯式指定、沿用前一輪、資源庫預設)+_assert_template_published() |
| 駭客怎麼打 | ① 專案管理者手上有一份從沒發布過(也就沒被檢查過)的草稿範本;② 建立新輪次時指定它;③ 系統複製草稿、啟動一輪真實稽核;④ 壞掉或惡意的流程圖就此上線,所有參與者的任務流轉都跑在它上面 |
| 得手什麼 | 讓沒檢查過的流程直接驅動真實稽核 |
| 修正的做法 | ① _resolve_flow_template_uid() 三條來源的匯合處補一次狀態檢查 _assert_template_published():非 published 一律回 GRC_FLOW_TEMPLATE_NOT_PUBLISHED(沿用既有錯誤碼);② 查無範本(回空)維持既有容錯不擋,交給呼叫端原本的「沒有可用範本」處理。與模組頁不同處:模組頁與 CM-2059 的 commit 都寫「後半待裁」,但這一半已由連帶卡 CM-2083 在隔天補上 |
| 狀態 | ✅ 已修(前半 CM-2059,主專案 commit 8edbaf944;後半 CM-2083,套件 jedi-compliance-audit commit d3cc1bc8) |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟡 中(總表 #94;STRIDE:讓服務停擺) |
| 在哪裡 | 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 帶等待秒數、計次器故障時放行 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟡 中(總表 #95;STRIDE:資料外洩) |
| 在哪裡 | AI 儀表板:查到的資料轉成回覆內容 |
| 攻擊面位置 | 能用 AI 儀表板的帳號 |
| 信任邊界位置 | 後端 ↔︎ 瀏覽器/外部 AI 服務。共用的序列化工具把物件上有的欄位全吐出,畫面只顯示三欄但整包已送出,送給外部 AI 的樣本也沒遮 |
| 元件端點位置 | POST /api/1.0/ai-dashboard/auto-generate;序列化工具 jedi_common/utils/serialization_util.py 的 to_serializable;日誌側 jedi_iam/app/service/*_service.py |
| 駭客怎麼打 | ① 能用儀表板的帳號;② 問一句「列出使用者」;③ 回應裡每個帳號都附著密碼加密鹽值與「是不是超級管理員」旗標;④ 這些內容同樣送進外部 AI 的提示 |
| 得手什麼 | 帳號密碼加密材料與最高權限帳號名單 |
| 修正的做法 | ① 序列化工具新增敏感欄位不輸出名單(鹽值、密碼、超管旗標、各類權杖),物件與字典兩個分支都擋;② 日誌側:身分套件查詢的記錄只寫筆數,不再把整筆帳號印進日誌(CM-2269) |
| 狀態 | ✅ 已修(CM-2064,套件 248132ec;日誌側 CM-2269 套件 f679363d) |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟡 中(總表 #150;STRIDE:讓服務停擺) |
| 在哪裡 | 合規文件核心:合規資源庫與專案的系統安全計畫(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)。驗證:解開 400MB 的 Excel 炸彈 0.37 秒回失敗、處理程序記憶體不變;新舊程式對真實範本解析結果逐位元組相同 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟡 中(總表 #152;STRIDE:讓服務停擺) |
| 在哪裡 | 合規文件核心:專案的系統安全計畫 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 照常進入待審 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟡 中(總表 #154;STRIDE:讓服務停擺) |
| 在哪裡 | 合規文件核心:系統安全計畫 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 秒) |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟡 中(總表 #156;STRIDE:竄改資料) |
| 在哪裡 | 合規文件核心:平台管理員刪除框架、刪除版本、編輯公版目錄、匯入覆蓋 |
| 攻擊面位置 | 平台管理員(屬誤操作釀災) |
| 信任邊界位置 | 平台 ↔︎ 客戶資源庫。後端不檢查引用,把關交給前端藏按鈕;原以為能照抄的「引用檢查」查錯欄位,永遠是零筆 |
| 元件端點位置 | 主專案 api/oscal/__init__.py:DELETE /api/1.0/oscal-framework/{uid}、DELETE /api/1.0/oscal-framework-version/{uid}、/oscal-catalog-* 編輯、POST /api/1.0/oscal-framework-parse-jobs/{uid}/confirm;新查詢 infra/readmodel/oscal/framework_version_usage_query.py |
| 駭客怎麼打 | ① 平台管理員;② 刪掉一個版本或改公版目錄;③ 後端不檢查有沒有客戶資源庫在用;④ 客戶資源庫指向不存在的版本,列表框架名稱空掉、用 Word 更新時找不到目錄 |
| 得手什麼 | (誤操作)客戶資源庫失去框架來源 |
| 修正的做法 | ① 新增查詢:數「從這個版本建立、未刪除的資源庫」,用獨立唯讀身分跨客戶查,查不了就報錯而不是回 0;② 刪版本、刪框架、改公版目錄、匯入覆蓋四處,有引用就 409(新碼 GRC_409038);③ 前端補三語訊息 |
| 狀態 | ✅ 已修(FR-114 CM-2180,BE 7262b74c8,1.21.0 出貨) |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟡 中(總表 #175;STRIDE:讓服務停擺) |
| 在哪裡 | 合規文件核心:系統安全計畫 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 秒回應 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟡 中(總表 #176;STRIDE:讓服務停擺) |
| 在哪裡 | 稽核輪次管理:稽核計畫頁匯入稽核計畫 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 秒回失敗、處理程序記憶體不變 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟡 中(總表 #177;STRIDE:讓服務停擺) |
| 在哪裡 | 稽核輪次管理:匯入稽核結果(稽核紀錄 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;真實稽核表新舊解析結果逐位元組相同 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟡 中(總表 #202;STRIDE:權限提升) |
| 在哪裡 | 授權管理模組:落地版(裝在客戶機器上的版本)判斷「這家客戶買了哪些模組、授權過期要不要轉唯讀、子公司數量上限」的總開關 |
| 攻擊面位置 | 客戶機器上的環境設定檔 .env。需要客戶那台主機的管理權限——對落地版來說,這個人就是客戶自己的維運人員,設計上本來就拿得到 |
| 信任邊界位置 | 原廠的商務規則 ↔︎ 客戶的主機管理員。授權檔有原廠簽章、防竄改會核對程式檔,但開關是一個環境變數,防竄改不核對設定值——簽章保護的那條線,從設定檔這一側被繞過 |
| 元件端點位置 | 唯一判斷點主專案 common/authz/license.py 的 enforcement_enabled()/readonly_gate_enabled()(四個執法路徑與唯讀守門 common/middleware/license_readonly_mw.py:init_license_readonly_gate 全經這兩支);開關本身是 config/config.py 的 LICENSE_ENFORCEMENT_ENABLED、LICENSE_READONLY_GATE_ENABLED |
| 駭客怎麼打 | ① 客戶的主機管理員打開 .env;② 把 LICENSE_ENFORCEMENT_ENABLED 改成 false、重啟;③ 系統讀到開關就整套放行,不留任何警示;④ 沒買的模組照用、授權過期照常寫資料、子公司數量上限失效 |
| 得手什麼 | 不付錢使用沒買的模組與過期授權(受損的是原廠商務收入,不是客戶資料) |
| 修正的做法 | ① 決策者裁定:落地版拿掉這兩個開關、一律執法;真的要暫時放行,改由原廠補發測試用授權。原廠自己管機器的 SaaS 保留開關當保險絲;② 兩支判斷函式在 DEPLOYMENT_MODE=host 時一律回 True、忽略設定,呼叫點不動;③ 新增 disabled_switches_ignored_on_host(),開機時對「落地版被設成 false、已被忽略」的開關逐一記警告(不擋啟動),診斷包同步回報「落地版固定執法」並留下 attempted_disable_ignored 痕跡給原廠;④ 安裝程式不再寫這兩行 |
| 狀態 | ✅ 已修(CM-2205,commit c8aa7d026,1.21.0 出貨)。驗證:14 條單元測試+突變;DEV 實測落地版關開關仍回 403、SaaS 關開關回 200 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟡 中(總表 #207;STRIDE:讓服務停擺) |
| 在哪裡 | 稽核流程:專案規劃頁的「匯入任務 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 兩份真實匯出檔新舊回應逐字相同;突變拿掉檢查即放行 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | ⚪ 低(總表 #114;STRIDE:資料外洩) |
| 在哪裡 | 共用基礎:「資料轉成回覆內容」的序列化工具 |
| 攻擊面位置 | 任何呼叫這支工具卻忘了先篩欄位的功能 |
| 信任邊界位置 | 後端 ↔︎ 瀏覽器。工具把物件上有的欄位全吐出,篩選責任全在呼叫端 |
| 元件端點位置 | jedi_common/utils/serialization_util.py 的 to_serializable;已出事的呼叫端是 AI 儀表板(見 M15-4) |
| 駭客怎麼打 | ① 任一能打到使用這支工具的功能的帳號;② 正常呼叫;③ 呼叫端沒先篩;④ 回應夾帶不該看的內部欄位(AI 儀表板真的吐過密碼鹽值) |
| 得手什麼 | 內部欄位,如密碼加密材料 |
| 修正的做法 | 工具本身加敏感欄位不輸出名單(鹽值、密碼、超管旗標、權杖類),物件與字典兩個分支都擋,不再只靠呼叫端自律 |
| 狀態 | ✅ 已修(CM-2064,套件 248132ec)。輸入檔標 CM-2038,實際修這支工具的 commit 標的是 CM-2064 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | ⚪ 低(總表 #117;STRIDE:權限提升) |
| 在哪裡 | 共用基礎:每次開資料庫交易時,告訴資料庫「這個人是誰、能看什麼」的那一段(所有功能共用) |
| 攻擊面位置 | 今天沒有:這個開關完全沒有效果。它的風險在讀程式的人——誤以為已經有一道部門層級的檢查,於是不去補真正需要的那道 |
| 信任邊界位置 | 使用者 ↔︎ 部門層級的資料隔離。開關名叫「能不能管部門」,每次有身分時都無條件設定,但資料庫 259 條隔離規則沒有一條讀它;真正被讀的是旁邊那個「有條件才設」的手足開關。看起來有守,實際上這條線上沒有守衛 |
| 元件端點位置 | 套件 jedi-common jedi_common/session/database/db.py 的 session_scope():SET LOCAL app.can_manage_orgs 三行(已刪);對照的活手足 set_can_read_all_orgs() 被出貨基線 scripts/init/02-schema.sql 5 條規則讀取 |
| 駭客怎麼打 | ① 今天什麼都打不到;② 風險在之後:有人要做部門層級的權限,看到這個開關以為已經有了;③ 沒去補真正的檢查;④ 部門之間的資料就一直沒有隔開,而大家以為有 |
| 得手什麼 | 今天什麼都得不到;是一個讓人誤判「已經有防護」的假象 |
| 修正的做法 | 拆除了什麼:① session_scope() 裡設定這個開關的三行刪除,旁邊的使用者編號與超級管理員設定不動;② 四份提到它的說明文件同步改掉,避免文件描述不存在的東西;③ 刪除前查證:21 支套件、主專案、前端、測試、代理程式、授權中心、開發環境隔離規則、資料庫自寫程式、出貨基線、全部資料庫異動腳本皆零命中 |
| 狀態 | 🗑️ 裁定刪除,已刪除,1.21.0 出貨(FR-114.3-3,CM-2045,套件 commit 599ab7c5;文件同步主專案 2b4e172b3)。驗證:session 相關 41 條測試通過 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | ⚪ 低(總表 #119;STRIDE:竄改資料) |
| 在哪裡 | 任務與成員管理:一個獨立的「快速設定」畫面(批次指派任務),兩個入口網址都沒有任何按鈕導過去,只有手打網址才進得去。模組頁沒有逐條表,此條依總表描述與拆除 commit 寫成 |
| 攻擊面位置 | 已登入、知道網址的人。不是攻擊面,是資料可信度的問題 |
| 信任邊界位置 | 畫面告訴使用者的 ↔︎ 實際存進去的。頁面按下儲存會顯示成功,但背後打的批次指派是恆回空清單的殘留功能(舊的綁定已停用),一筆都沒存——使用者以為指派好了 |
| 元件端點位置 | 前端 src/views/project/TaskSetupView.vue(已刪),路由 /project/projects/{id}/task-setup、/project/projects/{id}/ap/{apUid}/task-setup;它呼叫套件 jedi_task_platform/participant/api/routing.py 的 POST /api/1.0/task-assignees/batch → task_assignee_service.py 的 batch_add_task_assignees(恆回空) |
| 駭客怎麼打 | ① 不是攻擊,是誤導:使用者手打網址進到快速設定頁;② 選好人、按儲存;③ 畫面顯示「快速配置成功」;④ 實際一筆都沒存,任務沒有負責人,後續稽核流程卡住而沒人知道為什麼 |
| 得手什麼 | 沒有人得手;是使用者以為的設定與實際資料不一致 |
| 修正的做法 | 拆除了什麼(決策者裁定整頁拆除、兩個入口都拆):① 刪除兩個路由(共用同一元件,兩者都刪才能刪元件,避免只刪一邊撞錯);② 刪除 1,421 行的 TaskSetupView.vue;③ 刪掉對應的選單字串;共用的翻譯檔有 131/144 個字串被 4 支活頁面用到,整支保留 |
| 狀態 | 🗑️ 裁定拆除,已拆除,1.21.0 出貨(CM-2047,前端 commit 627f92d)。驗證:前端建置通過、原始碼搜尋該頁與路由名零命中 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | ⚪ 低(總表 #123;STRIDE:資料外洩) |
| 在哪裡 | 檔案上傳下載模組:本機儲存模式下刪除檔案 |
| 攻擊面位置 | 不是對外的攻擊面;是「以為刪了、其實還在」——拿到主機檔案系統的人(維運、備份、主機被入侵)看得到 |
| 信任邊界位置 | 使用者的刪除意圖 ↔︎ 主機上的實體檔案。預覽時轉出的快取 PDF 是衍生資料,刪主檔時應一併刪;物件儲存那條有做,本機儲存那條漏了 |
| 元件端點位置 | 刪除入口 DELETE /api/1.0/file/upload/{uid}、/file/uploads(套件 jedi_file_upload/api/routing.py)→ infra/adapter/local/local_file_adapter.py 的 delete_file()、delete_files_by_uids();衍生檔清理 infra/adapter/derived_files.py |
| 駭客怎麼打 | ① 使用者刪掉一份敏感檔案;② 本機儲存只刪了主檔;③ 先前預覽時轉出的 PDF 還留在硬碟上;④ 能讀主機檔案或備份的人照樣拿到內容 |
| 得手什麼 | 使用者以為已刪除的檔案內容(PDF 版) |
| 修正的做法 | ① 把物件儲存那條的衍生檔清理抽成共用函式 delete_derived_files(吃資料存取元件與一個刪實體的函式);② 本機與物件儲存兩種模式共用;③ 維持寬容行為:刪衍生檔失敗只記日誌、不擋主檔刪除 |
| 狀態 | ✅ 已修(CM-2062,套件 commit a061438f) |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | ⚪ 低(總表 #128;STRIDE:事後無法追查) |
| 在哪裡 | 稽核流程模組:稽核輪次的「推進階段」與「退回階段」,結果顯示在階段歷程時間軸與流程圖留言的作者欄 |
| 攻擊面位置 | 已登入、有推進該階段權限的使用者可打的階段 API——等於自己人 |
| 信任邊界位置 | 呼叫端送來的 ctx ↔︎ 伺服器寫入的歷程紀錄。代表身分的帳號欄位由伺服器填;顯示名稱卻用 ctx.setdefault("user_nickname", …)——呼叫端自己帶了就不覆寫 |
| 元件端點位置 | api/flow_engine/__init__.py:POST /api/1.0/project/{project_uid}/audit-round/{round_uid}/stage/advance(StageAdvanceResource)、…/stage/rollback(StageRollbackResource);白名單 api/flow_engine/serializers/stage_advance.py:strip_unknown_ctx() |
| 駭客怎麼打 | ① 有推進權限的使用者;② 推進或退回階段時,在請求的 ctx 裡自己填 user_nickname=主管的名字;③ 伺服器保留他填的值;④ 歷程時間軸顯示「主管推進了這一階段」(帳號欄仍是他自己) |
| 得手什麼 | 偽造畫面上顯示的操作者名稱 |
| 修正的做法 | ① 兩支入口都改成直接指派 ctx["user_nickname"] = user_ctx.nickname,一律以伺服器的登入身分覆寫;② 新增 strip_unknown_ctx():ctx 只放行處理程式真的會讀的 loopback_decision,其餘一律丟掉;兩支路由共用同一支,避免兩入口各長一套 |
| 狀態 | ✅ 已修,1.21.0 出貨(CM-2059,BE commit 8edbaf944) |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | ⚪ 低(總表 #129;STRIDE:資料外洩) |
| 在哪裡 | 證據自動分類模組:分類工作目錄(每次分類的輸入檔、判定結果、容器紀錄) |
| 攻擊面位置 | 要有主機的登入權限才看得到這些殘檔,所以不是越權存取;問題是「使用者以為刪乾淨了」,而合規客戶常有資料保留期限要求 |
| 信任邊界位置 | 使用者的刪除意圖 ↔︎ 主機上的工作目錄。分類跑完只清了上傳檔子目錄,判定結果與紀錄留在工作目錄;刪整批只刪資料庫列,工作目錄原封不動,磁碟也無限成長 |
| 元件端點位置 | DELETE /api/1.0/evidence-batches/{batch_uid}(套件 jedi_evidence_classification/api/routes/evidence_batch_route.py 的 EvidenceBatchDetailRoute.delete)→ app/service/evidence_batch_service.py 的 delete_batch();分類 POST /evidence-batches/{batch_uid}/classify;清理函式 infra/job_dir_cleanup.py |
| 駭客怎麼打 | ① 使用者上傳一批證據跑分類,之後按「刪除整批」;② 畫面上消失;③ 主機的工作目錄裡判定結果與紀錄都還在;④ 拿到主機或備份的人翻得到 |
| 得手什麼 | 使用者以為已刪除的分類結果與證據相關紀錄 |
| 修正的做法 | ① 每次分類跑完(不論成功失敗),結果與紀錄先存進資料庫,再把整個工作目錄刪掉;② 刪整批時再把這一批留下的所有工作目錄清一次;③ 清理只刪位於工作目錄底下、本身不是捷徑的目錄,不會跟著捷徑刪到別處;④ 清理失敗只記日誌、不回滾刪除;⑤ 同卡另補開發機容器的記憶體、CPU、程序數上限與逾時真的停掉容器 |
| 狀態 | ✅ 已修,1.21.0 出貨(CM-2227,套件 commit ecddb72c/85370a17) |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | ⚪ 低(總表 #131;STRIDE:讓服務停擺) |
| 在哪裡 | 證據自動分類模組:系統開一個獨立的分類程式(容器)去拆解證據檔、交給 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 秒逾時後容器確實已不在 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | ⚪ 低(總表 #135;STRIDE:竄改資料) |
| 在哪裡 | 意見回饋模組:刪除回饋附件(本機儲存那一型) |
| 攻擊面位置 | 目前打不到——現在的操作流程一次只傳一個檔案編號。未來做了批次刪除才會出事 |
| 信任邊界位置 | 呼叫者給的檔案清單 ↔︎ 這張回饋真正擁有的附件。程式已經算出「傳進來的檔案」與「這張回饋的附件」的交集,刪關聯表時用了交集,刪實體檔時卻用回沒過濾的原清單 |
| 元件端點位置 | jedi_issue/api/routing.py:DELETE /api/1.0/feedback/file/{uid}/{file_uid}(FeedbackFileRoute.delete)→ app/feedback/service/feedback_issue_service.py → infra/issue_upload_files/local/local_issue_attachment.py 的 delete_attachment |
| 駭客怎麼打 | ① 今天打不到;② 若日後做了批次刪除附件;③ 使用者一次送多個檔案編號,其中混了不屬於這張回饋的檔;④ 關聯表只刪屬於的那幾筆,實體檔卻全部刪掉——別人的附件被誤刪、而且不會報錯 |
| 得手什麼 | (未來)誤刪不屬於這張回饋的附件檔 |
| 修正的做法 | delete_attachment 刪實體檔時改用已算好的 matched_file_uids(過濾後結果),比照上方刪關聯表的寫法;同層 GitLab、GitHub 兩種附件的刪除逐一核過,本來就只刪符合的 |
| 狀態 | ✅ 已修(CM-2066,套件 commit 4f54c9b4) |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | ⚪ 低(總表 #136;STRIDE:讓服務停擺) |
| 在哪裡 | 設備與資訊系統清冊:設備清單頁的「一頁顯示幾筆」 |
| 攻擊面位置 | 任何一個登入帳號,在設備清單的請求裡改一個數字 |
| 信任邊界位置 | 使用者 ↔︎ 資料庫。根源不在這一塊,在所有模組共用的分頁設定(同 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);設備清單的請求格式繼承它,不必另改 |
| 狀態 | ✅ 已修(CM-2065,套件 commit a9562dd0,1.21.0 出貨) |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | ⚪ 低(總表 #144;STRIDE:竄改資料) |
| 在哪裡 | 授權管理模組:匯入或啟用授權檔時的「這張照是否已被別家客戶使用」檢查 |
| 攻擊面位置 | 已登入、能上傳授權檔的客戶帳號;需要在極短的時間窗內兩家同時送出,實際發生機率很低 |
| 信任邊界位置 | 客戶 ↔︎ 客戶(授權唯一性)。跨客戶重複檢查與實際寫入不在同一個交易——查詢走獨立連線、查完就結束,兩家同時上傳同一張照時都會在對方寫入前查到「沒人用」 |
| 元件端點位置 | jedi_license_runtime/api/routing.py:POST /api/1.0/license/upload、/license/activation/upload、/license/activation/online → app/service/license_verification_service.py 的 _store_verified_license();資料庫索引 idx_tenant_licenses_license_id_current(scripts/sql/2026-09-06-cm1580-tenant-licenses-license-id-partial-unique.sql) |
| 駭客怎麼打 | ① 兩家客戶手上有同一張授權檔;② 在同一瞬間各自上傳;③ 兩邊的檢查都查到「沒人用」;④ 兩筆都寫入成功,一張授權被兩家同時生效 |
| 得手什麼 | 一張授權被兩家客戶同時使用 |
| 修正的做法 | ① 資料庫加一道兜底:部分唯一索引「同一張照同一時刻只能有一份生效」(不是全表唯一——換照制下「換回舊照」需要同一張照有多列歷史);② 寫入改走 add_atomic():用交易保存點包住新增,撞到索引時呼叫端能接住錯誤、不會把外層交易整個毒掉;③ 撞索引轉成既有的「已被其他客戶使用」錯誤,預查詢擋下與索引擋下共用同一個出口 |
| 狀態 | ✅ 已修(CM-1580,套件 commit 6cf1879a、主專案 migration f3c586961)。驗證:突變拿掉錯誤轉換重跑會紅 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | ⚪ 低(總表 #180;STRIDE:讓服務停擺) |
| 在哪裡 | 合規文件核心:合規資源庫的「匯入 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 節點擋下 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | ⚪ 低(總表 #181;STRIDE:讓服務停擺) |
| 在哪裡 | 合規文件核心:合規資源庫 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 秒內回判不合格 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | ⚪ 低(總表 #191;STRIDE:竄改資料) |
| 在哪裡 | 合規文件核心模組:稽核輪次的「重新產生稽核計畫草稿」 |
| 攻擊面位置 | 已登入、是該輪次的稽核員或管理者 |
| 信任邊界位置 | 輪次階段 ↔︎ 稽核計畫的可寫狀態。同一支檔案其他四個改計畫的入口都有檢查階段,這一支沒有——稽核中甚至已結案的輪次按一下,輪次就改指向一份空白的新計畫,原本那份還在、但跟輪次脫鉤 |
| 元件端點位置 | 當時:主專案 api/project/__init__.py 的 POST /api/1.0/audit-round/{round_uid}/ap/generate-draft(AuditRoundApGenerateDraftRoute)→ app/flow_control/service/assessment_plan_app_service.py 的 generate_draft_for_round(兩者已拆) |
| 駭客怎麼打 | ① 稽核員或管理者在一個已進入稽核、甚至已結案的輪次;② 打「重新產生草稿」;③ 系統不看階段就產一份空白計畫並把輪次指過去;④ 已定稿的稽核計畫從輪次上消失 |
| 得手什麼 | 把進行中或已結案輪次的稽核計畫換成空白 |
| 修正的做法 | 拆除了什麼(決策者裁「拆網址,不補守門」,前端零呼叫):① 拆掉這支網址、它專用的 generate_draft_for_round;② 同批拆掉空殼的「更新稽核計畫」與三支直打推進網址(只改輪次狀態、不推流程引擎,直打會讓畫面階段與輪次狀態對不上);③ 背後的推進方法保留,階段推進仍呼叫它們 |
| 狀態 | 🗑️ 已拆除(FR-114 CM-2177,主專案 commit 1e0879815)。驗證:五支網址實打皆 404;以 API 走完整稽核流程證明推進方法沒被誤刪 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | ⚪ 低(總表 #192;STRIDE:讓服務停擺) |
| 在哪裡 | 合規文件核心:原廠匯入 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 修前修後解析結果逐位元組相同 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | ⚪ 低(總表 #193;STRIDE:讓服務停擺) |
| 在哪裡 | 合規文件核心: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 秒 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | ⚪ 低(總表 #196;STRIDE:讓服務停擺) |
| 在哪裡 | 稽核輪次管理:稽核計畫 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 秒,解析出的單位相同 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | ⚪ 低(總表 #213;STRIDE:權限提升) |
| 在哪裡 | 授權管理模組:授權到期後產品轉唯讀 |
| 攻擊面位置 | 授權已過期的客戶的任何登入帳號,在排程下一次跑完之前;若部署方式讓排程沒在跑,就一直有效 |
| 信任邊界位置 | 時間 ↔︎ 寫入權限。「過期即唯讀」應該在每次判斷寫入權限時當場算;當時讀的是資料庫裡存的狀態,而那個狀態要等每天一次的排程(台灣時間早上 10 點)才更新 |
| 元件端點位置 | 主專案 common/authz/license.py 的 LicenseSnapshot 與 viewer_is_readonly();唯讀攔截 common/middleware/license_readonly_mw.py;排程 core/scheduler.py 的 _license_expiry_state_machine_tick();到期判斷純函式在套件 jedi-license-runtime domain/service/license_expiry_state_machine.py 的 compute_target_status() |
| 駭客怎麼打 | ① 客戶的授權在某天午夜過期;② 排程還沒跑,資料庫裡的狀態仍是「正常」;③ 客戶照常新增、修改資料,最長將近一天;④ 若排程沒在跑,就一直可寫 |
| 得手什麼 | 授權過期後繼續寫入資料 |
| 修正的做法 | 決策者裁定「過期即唯讀」:① LicenseSnapshot 多帶到期日與寬限規則,新增 effective_status(),用排程同一支 compute_target_status() 當場按時間重算;② 與資料庫存的狀態取較嚴的那個——存的已是唯讀就仍唯讀,不因重算放寬;③ viewer_is_readonly() 改看它;④ 排程照留,負責把狀態寫進資料庫給清單、報表與寄通知用 |
| 狀態 | ✅ 已修,1.21.0 出貨(8-G,CM-2217,主專案 commit 3ed47c7a9)。驗證:新增「剛過期 5 分鐘、資料庫仍正常 → 唯讀」等 3 條測試,突變轉紅;開發環境以實際租戶授權把時間推到到期後驗(未改授權資料,屬打折) |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | ⚪ 低(總表 #215;STRIDE:竄改資料) |
| 在哪裡 | 主系統的首次開通精靈:新裝好的系統第一次建立公司與管理員帳號 |
| 攻擊面位置 | 開通期間能打開通 API 的人(要帶開通設定碼)。實務上最可能是裝機人員自己連點兩次 |
| 信任邊界位置 | 兩個同時進來的開通請求。程式註解說「資料庫會擋住」,但資料庫根本沒有那條限制——而且不同帳號名的兩個請求本來就撞不到唯一約束 |
| 元件端點位置 | 主專案 api/setup/__init__.py:POST /api/1.0/setup/provision(SetupProvisionRoute)→ app/setup/service/setup_wizard_service.py 的 _provision_in_transaction();新增 infra/setup/setup_provision_lock.py |
| 駭客怎麼打 | ① 裝機人員在開通畫面按「建立」;② 網路慢,又按一次(或兩個人同時操作);③ 兩個請求都判定「還沒開通」,各自往下跑;④ 系統建出兩家公司、兩個管理員,事後要人工清掉 |
| 得手什麼 | 沒有人得手;是開通資料重複、需要人工清理 |
| 修正的做法 | ① SetupProvisionLock.try_acquire() 在呼叫端當下的交易內執行 pg_try_advisory_xact_lock:交易層級鎖、提交或失敗都自動放,不會漏放鎖死;非阻塞,第二下立刻得到結果;② _provision_in_transaction() 第一步搶鎖,搶不到回 409 SETUP_409001;③ 搶到後再判定一次是否已開通——前一個請求可能剛好在鎖外判定之後才提交;④ 沿用既有錯誤碼,前端既有的「已開通」畫面直接接得住 |
| 狀態 | ✅ 已修,1.21.0 出貨(8-F,CM-2216,主專案 commit a02409392)。驗證:兩條真資料庫連線同時搶,第二條 0.019 秒內回失敗;打折:DEV 已開通,沒用兩個真的 HTTP 請求並發打 |