Guidant AI 資安檢視總報告 · STRIDE 威脅分類
命中這一類的 44 件,還原成模組頁的 53 條原始條目逐條展開。每一條用白話寫出:在哪裡、攻擊面、信任邊界、元件端點、駭客怎麼打、得手什麼、修正的做法。
拿到超出自己角色的能力。STRIDE 六類裡它破壞的是「授權」——依微軟的定義:沒有權限的人取得較高的權限,足以危害甚至摧毀整個系統。判定時問的是:攻擊者打下這一條之後,能不能拿到超出自己角色的能力? 典型樣子是一般員工把自己改成管理員、子公司管理員改到全站設定、在伺服器上執行自己的程式碼。
對稽核產品來說,權限就是信任的來源:誰能結案、誰能改範本、誰看得到哪家客戶,全靠角色與層級。這次的 53 條在本專案長成六種形狀:
STRIDE 與 OWASP 的關係:OWASP 講「程式哪裡寫錯」,STRIDE 講「攻擊者能拿它做什麼」。同一條問題通常兩邊都命中,所以這一頁的條目會與 OWASP 各頁重複出現,內容相同——兩邊各自完整,讀者從哪一邊進來都看得完。這一類 44 件裡有 32 件以權限提升為主分類,其餘 12 件主要歸在「冒充身分」「資料外洩」「竄改資料」,權限提升是它們的附帶後果;這些條目的嚴重度欄會註明主分類。
為什麼是 53 條不是 44 件:總結問題總表有 8 件是同一個洞被兩三個模組各撈到一次,總表併成一件:#12(M23-1+M16-7+M17-8)、#13(M02-4+M03-13)、#20(M09-2+M21-1)、#37(M03-14+M06-7)、#72(M16-8+M17-11)、#73(M17-10+M15-6)、#118(M06-14+M06-16)、#122(M16-6+M17-6)。本頁拆回模組頁原始條目,每條都列。標題括號內是總表編號,可回去對照。
本頁與總表、模組頁不一致的地方(都以程式碼與 commit 為準寫在下方,總表與模組頁待統籌決定是否更正):① M11-13 總表狀態欄把 CM-2179(commit
94275d72f)列為修正,但那個 commit 修的是同模組第 30 條「草稿版本也能拿去建庫」,它自己寫明第 65 項的入口權限是 CM-2040(commit60015232f)修的,本卡只確認;② M05-14 總表狀態欄只列「填答那半」與「AI 儀表板查問卷」兩個入口,模組頁點名的第三個入口「規劃頁把問卷掛上任務」另由主專案 commit02bc1179c(CM-2194 入口三)修掉,總表沒寫;③ M16-7、M17-8、M16-8、M17-11 模組頁寫「密碼已換發」,但 CM-2048、CM-2049 兩個 commit 都註明資料庫與系統管理員密碼「本輪不換發、排正式上版前統一處理」;④ M04-2 總表狀態欄沒有卡號,修正 commit 由本頁回查補上(FR-094.1 三張卡);⑤ M03-5 總表寫已修,但「代理程式端實際比對指紋」裁定另開小卡,本次只把指紋記進派工資料,未見對應 commit。
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🔴 最嚴重(總表 #1;OWASP:A07 身分驗證失效、A01 存取控制失效;主分類為冒充身分,另涉資料外洩、竄改資料) |
| 在哪裡 | 遠端代理程式模組:裝在客戶機房的代理程式與後端之間的五條通道——報到、領工作、確認收到、回報結果、下載證據檔 |
| 攻擊面位置 | 後端對外開放的「代理程式控制面」API。這一面設計上就不帶使用者登入(代理程式是機器、不是人),任何連得到後端的人都打得到,不需要帳號 |
| 信任邊界位置 | 客戶機房 ↔︎ 我們後端。後端該在入口用「簽過章的身分憑證」確認對方是哪一台;當時外層的雙向憑證已退役,後端只讀請求裡自報的機器編號就相信。竄改的那一面在「回報結果」:結果該只收派給這一台的單,且撤銷的機器一律拒收,當時兩件都沒做 |
| 元件端點位置 | jedi_remote_agent/api/routing.py:POST /api/1.0/agents/register(報到)、/agents/heartbeat(領工作)、/agents/tasks/{uid}/ack(確認收到)、/agents/tasks/{uid}/result(回報結果);處理在 app/service/agent_task_service.py、agent_enrollment_service.py、狀態寫入 AgentTaskDomainService.update_status;證據檔下載路由在主專案 api/remote_agent/routes/agent_file_route.py |
| 駭客怎麼打 | ① 在任何連得到後端的位置,不需要帳號;② 照代理程式平常的格式送「我是代理程式 X」,後端不驗證就把工作與主機帳密交出來;③ 改打回報結果通道,替 X 回報「掃描全部正常」或把真實結果蓋掉——稽核證據被偽造;④ 管理員按「撤銷」後,確認收到與回報結果兩條照樣能用,被撤銷的機器還能繼續送假資料 |
| 得手什麼 | 冒充任一家客戶的機器、拿走主機明文帳密,並偽造或抹除弱點掃描的稽核證據 |
| 修正的做法 | ① 報到時多發一張「控制面通行證」:用後端既有的簽章私鑰簽,內含機器編號、客戶編號、用途標記與效期;② 其餘通道每個請求都要帶這張證:新守門 agent_pass_required+AgentControlAuthService——驗簽、只認證裡的編號、每個請求查一次資料庫確認沒被撤銷或停用;③ 確認收到與回報結果:交易內再驗一次撤銷狀態,並在 AgentTaskDomainService.update_status 比對「這張單是不是派給這台」,不符回「找不到」;「已取消就短路」排在比對之後,不讓別台探出單子狀態;④ 路由表改成管理員/代理程式/報到三級,認不得的等級在啟動時就報錯;⑤ 報到時被撤銷的機器一律拒絕、不再洗回正常;帶舊編號重新登記要附舊私鑰簽的持有證明,且憑證申請檔的名稱必須等於本次機器指紋(驗收退回補上);⑥ 主專案的證據檔下載改用同一張通行證,拿掉自報編號的標頭 |
| 狀態 | ✅ 已修(CM-2052,套件 commit c4acc687+9c049540,主專案 7a3bdf1ec)。驗證:套件 150 測試;DEV 實跑撤銷後心跳/確認/回報/下載全部 403、確認別台的單回 404 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🔴 最嚴重(總表 #2;OWASP:A07 身分驗證失效;主分類為冒充身分) |
| 在哪裡 | 登入與權限模組:登入頁的「忘記密碼」功能 |
| 攻擊面位置 | 不需要登入就能打的公開 API(忘記密碼本來就是給還沒登入的人用的),任何人在登入頁就構成攻擊面 |
| 信任邊界位置 | 匿名網路 ↔︎ 身分系統。忘記密碼的設計是「系統把重設鑰匙寄到本人信箱」,鑰匙只該走信箱這條路;當時 API 回應直接把鑰匙交給了按下送出的人,等於邊界的另一側(信箱)被繞過 |
| 元件端點位置 | jedi_iam/api/routing.py:POST /forget-password(ForgetPasswordRoute.post)→ request_change_password() 回傳的實體含一次性重設憑證 uid,route 整個 dump 進回應;拿到 uid 後可直接打 /user/change-pwd-by-req 改密碼 |
| 駭客怎麼打 | ① 任何人打開登入頁,不需要帳號;② 在「忘記密碼」輸入目標的信箱(例如原廠管理員的信箱)按送出;③ 系統在畫面回應裡直接附上重設密碼的鑰匙——不必收信、不必知道舊密碼;④ 拿鑰匙去打「用請求憑證改密碼」,把目標帳號的密碼改成自己的;⑤ 目標若是原廠最高權限管理員,等於拿到全部客戶的資料 |
| 得手什麼 | 接管任何知道信箱的帳號,包含原廠最高權限管理員 |
| 修正的做法 | ① 回應改成固定空白:忘記密碼成功與失敗一律回同一個空內容,不再把重設實體 dump 出去,鑰匙只留在服務內部組信件連結用;② 同步從「改密碼」回應 schema 拿掉 uid 欄位;③ 新增三條測試:成功不含鑰匙、遮罩開啟時失敗與成功回應逐位元組相同(不讓人靠回應差異探信箱存不存在)、遮罩關閉時仍照舊回 404;用突變測試確認斷言有牙齒;④ 前端已確認沒有讀取回應內容,零影響 |
| 狀態 | ✅ 已修(CM-1575,套件 jedi-iam commit a9edf1d3)。已核對:該功能成功與失敗都回同一個空白結果 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟠 高(總表 #4;OWASP:A01 存取控制失效;主分類為資料外洩) |
| 在哪裡 | 弱點檢測整合模組:「檢測工具設定」頁的「測試連線」按鈕 |
| 攻擊面位置 | 已登入使用者可打的檢測工具 API。只要能登入就按得下去,不需要任何管理權限;連到哪台主機也是請求裡自己填的 |
| 信任邊界位置 | 使用者 ↔︎ 客戶的維運帳密。這顆按鈕會先把加密存著的主機帳密與掃描工具權杖解開再送出去,解密前應該先確認「你有沒有權限改這份設定」。同一支檔案裡的新增、修改、重置都有這道檢查,只有測試連線漏掉——是漏寫,不是設計 |
| 元件端點位置 | POST /api/1.0/detection-tools/configs/{uid}/test-connection(套件 jedi_detection/api/routes/detection_tool_route.py 的 TenantDetectionToolConfigTestConnectionRoute.post)→ app/service/detection_tool_service.py 的測試連線流程,經遠端代理程式對目標主機實連 |
| 駭客怎麼打 | ① 公司裡任何一個能登入的帳號;② 打開檢測工具設定頁,記下某份工具設定的編號;③ 直接送測試連線請求,目標主機填成自己控制的機器;④ 系統只驗登入,把 SSH/Windows 遠端管理密碼與三套掃描工具權杖解密後送過去;⑤ 拿到的帳密可以直接登入客戶正式機房的主機 |
| 得手什麼 | 整家公司用來掃描主機的維運帳密與掃描工具權杖 |
| 修正的做法 | ① 測試連線路由補上 @capability_required(plugin_update_capability),寫法照抄同一支檔案裡新增、更新、重置三處現成的宣告;② 旁邊同樣沒掛權限的「查引用次數」確認是純計數、不解密、不對外連線,維持原樣;③ 「連到哪裡」不設白名單(第 6 條另修:主機數上限、回應收斂、記稽核日誌),所以這道權限檢查就是唯一的防線 |
| 狀態 | ✅ 已修(CM-2042,套件 commit 5a9baf2b) |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟠 高(總表 #12,與 M16-7+M17-8 同一件;OWASP:A07 身分驗證失效、A02 安全設定錯誤;主分類為資料外洩) |
| 在哪裡 | 意見回饋模組檢視時查到:資料庫系統管理帳號(可繞過客戶隔離的那一個)的密碼,散在 249 個進版控的文件裡,其中一份把主機、帳號、密碼湊成可直接複製貼上的連線指令 |
| 攻擊面位置 | 拿得到程式庫的人;再加上連得到資料庫的網路位置。同一組密碼同時通行開發環境、出貨基線資料庫、兩座標示退役但仍活著的舊資料庫,也是快取服務的密碼 |
| 信任邊界位置 | 版控 ↔︎ 祕密存放處,以及一般應用帳號 ↔︎ 系統管理帳號。產品平常用受隔離規則約束的帳號;這組是繞過所有客戶隔離的管理帳號,它的密碼跨進了版控,客戶隔離這條線對拿到它的人就不存在 |
| 元件端點位置 | 外流位置:docs/conversation-history/(112 檔)、docs/features/(67)、docs/features-site/(65)、docs/issues/、docs/claude/memory/、docs/analysis/2026-05-28-poc-db-migration-plan.md(可直接執行的連線指令)。還在寫入的路徑:docs/system-design/scripts/generate_db_schema_docx.py 的 DB_CONN |
| 駭客怎麼打 | ① 拿到程式庫;② 打開那份遷移計畫,複製三行連線指令;③ 在連得到資料庫的位置貼上執行,以系統管理身分登入;④ 每一家客戶的資料可讀可寫,連出貨基線庫也能改——改進去的東西會跟著安裝檔裝到客戶機器上 |
| 得手什麼 | 繞過全部客戶隔離,讀寫每一家客戶的資料,並可汙染出貨基線 |
| 修正的做法 | 三步、順序不能顛倒:① 先堵還在寫入的路:generate_db_schema_docx.py 改從環境變數 DB_SCHEMA_DOCX_DB_PASSWORD 讀,沒設就直接報錯,不退回任何寫死值;② 清掉已寫進去的:250 個檔的密碼字串換成「請查 .env 或部署文件」,可直接執行的連線指令優先處理,記憶檔兩份手動改寫,文件站整站重新產出(含搜尋索引);③ 換密碼:決策者裁定排在正式環境上版前統一換。驗證:全庫含站點產物搜尋密碼值零命中 |
| 狀態 | ✅ 已修(CM-2049,commit 5d4c14221);殘留已清、再寫進去的路已堵,密碼換發排在正式上版前 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟠 高(總表 #12,與 M23-1+M17-8 同一件;模組頁原評 🟡 中;OWASP:A07 身分驗證失效、A02 安全設定錯誤;主分類為資料外洩) |
| 在哪裡 | 通知模組檢視時查到:一支產生資料庫結構文件的腳本,把客戶展示環境(依規定等同正式環境)的完整連線資訊寫在程式碼裡 |
| 攻擊面位置 | 拿得到程式庫的人,加上連得到展示機的內網位置。它是 249 個檔裡唯一一支會被執行的腳本,其餘都是文件,所以也是「會繼續把密碼寫出去」的那個源頭 |
| 信任邊界位置 | 版控 ↔︎ 祕密存放處。連線資訊該從環境設定讀;當時直接寫成程式常數,每次有人改這支腳本就再 commit 一次 |
| 元件端點位置 | docs/system-design/scripts/generate_db_schema_docx.py:模組層級的 DB_CONN = dict(host=..., dbname=..., user=..., password=...) |
| 駭客怎麼打 | ① 拿到程式庫;② 打開這支腳本,主機、資料庫名、帳號、密碼一行全有;③ 在內網直接連進展示環境的資料庫;④ 讀寫客戶試玩會看到的所有資料 |
| 得手什麼 | 直接連進等同正式環境的展示機資料庫 |
| 修正的做法 | ① 腳本改成密碼從環境變數 DB_SCHEMA_DOCX_DB_PASSWORD 讀,沒設就丟錯中止,不退回任何預設值(commit 5d4c14221,CM-2049,與 M23-1 同批);② 版控殘留字串清除(CM-2048/CM-2049);③ 換發:模組頁記為已換發,但 CM-2049 commit 註明「密碼暫不換發、排正式上版前換」——兩處說法不一致,以 M23-1 的狀態為準 |
| 狀態 | ✅ 已修正(腳本改讀環境變數 commit 5d4c14221;殘留清除 CM-2048 commit e8e134a4e) |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟠 高(總表 #12,與 M23-1+M16-7 同一件;模組頁原評 🟡 中;OWASP:A07 身分驗證失效、A02 安全設定錯誤;主分類為資料外洩) |
| 在哪裡 | 公告模組檢視時查到:資料庫系統管理帳號的密碼寫在開發文件裡 |
| 攻擊面位置 | 拿得到程式庫的人,加上連得到開發環境或出貨基線資料庫的位置 |
| 信任邊界位置 | 版控 ↔︎ 祕密存放處,以及一般應用帳號 ↔︎ 可繞過隔離的管理帳號。出貨基線庫是所有安裝檔的來源,這條線一破,寫進去的東西會跟著出貨 |
| 元件端點位置 | 外流位置:docs/claude/memory/、docs/system-design/、docs/features/ 等開發文件(與 M23-1 的 249 檔同批)。受影響的帳號:資料庫的系統管理帳號(cmmgr,可繞過客戶隔離) |
| 駭客怎麼打 | ① 拿到程式庫;② 在文件裡找到管理帳號密碼;③ 連進開發環境或出貨基線資料庫;④ 繞過所有客戶隔離讀寫資料,或在基線庫埋東西等著隨安裝檔出貨 |
| 得手什麼 | 開發環境與出貨基線庫的完整讀寫權 |
| 修正的做法 | ① 與 M23-1 同一批清除:文件裡的密碼字串換成「請查 .env 或部署文件」;② 文件站重新產出;③ 換發:模組頁記為已換發,但 CM-2048、CM-2049 兩個 commit 都註明資料庫管理密碼「本輪不換發、排正式上版前統一處理」——兩處說法不一致,以 M23-1 的狀態為準 |
| 狀態 | ✅ 已修正(殘留由 CM-2048 commit e8e134a4e、CM-2049 commit 5d4c14221 清除) |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟠 高(總表 #13,與 M03-13 同一件;OWASP:A05 注入攻擊、A06 不安全的設計;主分類為冒充身分) |
| 在哪裡 | 檔案上傳下載模組:證據、附件的「預覽」功能 |
| 攻擊面位置 | 已登入使用者可打的檔案上傳與預覽 API。上傳者只需要一個有效帳號;預覽是設計上就開放的日常功能 |
| 信任邊界位置 | 伺服器 ↔︎ 受害者的瀏覽器。檔案內容是上傳者給的、不可信,伺服器把它交給瀏覽器之前應該決定「能不能在我們的網域裡打開、用什麼格式打開」;當時只看上傳者自己填的副檔名,而且允許瀏覽器直接把它畫出來,等於把不可信的內容放進了可信的網域 |
| 元件端點位置 | jedi_file_upload/api/routing.py:POST /file/upload(上傳)、GET /file/pdf-preview/{uid}(UploadFilePreviewAsPDFRoute)→ upload_file_route.py 的 _send_preview() → common/utils/preview_policy.py 的 decide_preview() |
| 駭客怎麼打 | ① 公司內任一有帳號的人,做一個內含程式的網頁檔(或把網頁檔改名成 .png);② 當成證據或附件上傳;③ 稽核人員在證據清單點「預覽」;④ 伺服器照副檔名把它當網頁送出,瀏覽器在我們的網域裡執行那段程式;⑤ 程式用受害者的登入身分讀資料、改資料,或把登入狀態送出去 |
| 得手什麼 | 受害者的登入身分,能用他的名義做他能做的任何事 |
| 修正的做法 | ① 可預覽清單:只有 pdf、html、png、jpg、gif、webp 這幾種可以在瀏覽器裡開,清單外一律強制下載;② 伺服器自己驗檔頭:讀檔案開頭的特徵碼確認內容真的是宣稱的格式,叫 .png 但內容是網頁的也強制下載;③ 網頁預覽加隔離指令:回應帶 CSP sandbox,不給執行程式、不給碰本站,排版與圖片照常顯示;④ 所有回應加「不准瀏覽器自己猜格式」的標頭;⑤ 不再用主機的檔案類型對照表(同一支程式在不同機器會給不同答案),改用套件內固定對照表 |
| 狀態 | ✅ 已修(CM-2062,套件 commit a061438f)。驗證:假 .png 被強制下載、真 png/pdf/html 正常預覽且 html 帶隔離、exe/svg 強制下載 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟠 高(總表 #13,與 M02-4 同一件;模組頁原評 🟡 中;OWASP:A05 注入攻擊、A06 不安全的設計;主分類為冒充身分) |
| 在哪裡 | 弱點檢測整合模組:代理程式把掃描報告回傳給後端、存成證據 |
| 攻擊面位置 | 掃描報告的兩個檔名來源——呼叫端帶的檔名提示、代理程式回應標頭裡的檔名——都在系統外面。網頁格式正是掃描報告的正式格式,這是日常路徑,不是罕見情境 |
| 信任邊界位置 | 客戶機房的代理程式 ↔︎ 我們後端。後端收到報告時應該自己決定檔名與格式;當時直接採用外面給的檔名,沒去掉路徑、也沒查副檔名,接著這份檔案被寫成證據,就進了 M02-4 那條預覽路徑 |
| 元件端點位置 | jedi_remote_agent/api/routing.py:POST /agents/tasks/{uid}/result(回報結果)→ jedi_detection/app/service/detection_result_handler.py 的 _fetch_blob()(取回報告、決定檔名)→ jedi_detection/common/report_filename.py 的 sanitize_report_filename();危險的出口是證據清單的 GET /file/pdf-preview/{uid} |
| 駭客怎麼打 | ① 控制得了代理程式回應或檔名提示的人,回一份網頁格式的報告;② 後端照單全收檔名,存成證據;③ 「執行歷史」那一區點報告是下載(安全),但報告同時寫了一筆證據;④ 稽核人員從證據清單點「預覽」,網頁裡的程式就在他的瀏覽器裡用他的身分跑起來 |
| 得手什麼 | 稽核人員的登入身分 |
| 修正的做法 | ① 兩個檔名來源一律先過 sanitize_report_filename():切掉目錄片段(含 Windows 與 Linux 兩種分隔符)、洗掉控制字元、查副檔名白名單;② 不合規的一律退回系統自產名 detection_report_<任務編號>.html;③ 刻意保留中文——OpenSCAP 報告慣例是中文檔名,濾掉會變一串底線;④ 預覽端由 M02-4 的白名單+驗檔頭+隔離指令一起收口 |
| 狀態 | ✅ 已修(CM-2062,套件 commit a061438f)。驗證:中文 OpenSCAP 檔名原樣保留,../../etc/passwd 類路徑被去掉,.exe/空字串退回預設名 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟠 高(總表 #15;OWASP:A05 注入攻擊、A08 軟體或資料完整性失效;主分類為權限提升,另涉竄改資料) |
| 在哪裡 | 弱點檢測整合模組:「檢測基準」管理——建立或改版基準時上傳一包檢測規則(壓縮檔),系統自動解析出裡面有哪些檢查項目 |
| 攻擊面位置 | 已登入、有建立檢測基準權限的客戶管理員可打的基準管理 API。上傳規則包是設計上就開放給客戶的功能;網址型來源連上傳都不用,填一個自己伺服器的網址即可 |
| 信任邊界位置 | 客戶上傳的檔案 ↔︎ 後端主機上的外部程式(cinc-auditor)。後端把壓縮檔重新打包後交給 cinc-auditor 解析,它讀設定檔 inspec.yml 時會先當成 Ruby 樣板渲染;該守的檢查應在「交給外部程式之前」,當時驗證器只驗結構,沒有一項看設定檔內容 |
| 元件端點位置 | jedi_detection/api/routing.py:POST /api/1.0/detection-tool-profiles、/detection-tool-profiles/{uid}/new-version、/detection-tool-profile-versions/{uid}/source、/detection-tool-profile-versions/{uid}/extraction;解析鏈 DetectionProfileExtractionService → common/profile_extractor/inspec.py:_repack_flat() → cinc-auditor json |
| 駭客怎麼打 | ① 任一客戶的管理員登入;② 做一包檢測規則,在 inspec.yml 寫一段 Ruby 樣板;③ 上傳,結構全部正常、驗證器放行;④ 系統排解析,cinc-auditor 一讀設定檔就以後端程序身分把那段程式跑起來;⑤ 讀出系統密鑰、把資料庫身分提權成超級管理員,任意改寫全平台客戶的資料 |
| 得手什麼 | 整台後端主機的權限,以及改寫全平台客戶資料的能力 |
| 修正的做法 | ① 在「重新打包」那一步本身擋:_repack_flat() 交給 cinc-auditor 前先讀出 inspec.yml,含樣板記號 <% 就拒收並回明確訊息;② 擋點放在上傳與網址兩條路共用的那一層,第三種來源出現時也擋得到;③ 開工前實查隨包 13 筆基準與 8 支內建規則包都沒用樣板語法,拒收不會打壞正常客戶;④ 同批把三道防炸彈上限下沉到共用層 common/archive_limits.py;⑤ 已知打折:只看 inspec.yml 一個檔,若外部工具對 controls/*.rb 也做樣板渲染則擋不住;長期正解「把外部工具關進沙箱」未做 |
| 狀態 | ✅ 已修,1.21.0 出貨(CM-2057,套件 commit c0fe19fd)。驗證:含 <% 的包拒收、只有 controls 含 <% 的包放行(打折範圍);套件 88 測試通過 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟠 高(總表 #20,與 M21-1 同一件;OWASP:A05 注入攻擊;主分類為權限提升,另涉資料外洩) |
| 在哪裡 | 系統日誌模組:管理員在「操作日誌」查詢頁按「匯出」,拿到 Excel 打開 |
| 攻擊面位置 | 系統每一個入口——每次請求都會在任何權限檢查之前被原樣記下來(瀏覽器識別字串、網址、查詢條件、請求內容),所以不需要帳號。匯出的 17 個欄位裡有 6 個吃得到外部輸入 |
| 信任邊界位置 | 我們的資料 ↔︎ 管理員自己的電腦。「先記錄再檢查權限」的順序是對的,不該改;該守的是匯出那一側——交出去之前把會被試算表當公式的開頭字元標成純文字。當時匯出沒有任何處理 |
| 元件端點位置 | 種入:主專案 common/middleware/app_mw.py 的 before_request() 寫 api_log;引爆:jedi_api_log/api_log/api/routing.py 的 GET /log/api-logs/export(ExportApiLogsRoute,平台管理員)→ api_log_service.export_api_log_file() → api_log/common/utils/excel_util.py 的 clean_invalid_characters()/gen_excel_bytes() |
| 駭客怎麼打 | ① 不需要帳號,對系統任一網址送一個請求;② 把瀏覽器識別字串改成一串以 = 開頭的公式;③ 系統照實記進操作日誌;④ 管理員匯出操作日誌、打開 Excel、按掉安全警告;⑤ 公式在管理員電腦上執行,可以是釣魚連結,也可以把整張表送到外部網址 |
| 得手什麼 | 在管理員電腦上執行內容,外洩整份操作日誌 |
| 修正的做法 | ① jedi-common 新建共用函式 neutralize_formula():開頭是 = + - @ tab CR 的字串前面加一撇單引號,其他原樣,一個字都不刪;② 操作日誌匯出的每一格都套它;③ clean_invalid_characters() 先清控制字元、再判公式字元——順序不能反,控制字元清掉後才會露出後面的 = |
| 狀態 | ✅ 已修(CM-2055,套件 commit 38de2886)。驗證:實跑匯出、存檔讀回是純文字 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟠 高(總表 #20,與 M09-2 同一件;模組頁原評 🟡 中;OWASP:A05 注入攻擊;主分類為權限提升,另涉資料外洩) |
| 在哪裡 | 意見回饋模組:管理員匯出意見回饋成 CSV 或 Excel |
| 攻擊面位置 | 送意見回饋的 API,門檻只有登入。使用者可控的欄位有標題、描述、標籤名、建立者暱稱 |
| 信任邊界位置 | 我們的資料 ↔︎ 管理員自己的電腦。同 M09-2,該在匯出那一側中和;當時兩個匯出分支都沒處理 |
| 元件端點位置 | 種入:jedi_issue/api/routing.py 的 POST /feedback(FeedbackRoute);引爆:POST /feedback/export/{export_type}(FeedbackExportRoute)→ jedi_issue/app/feedback/service/feedback_service.py 的 export_feedbacks()(csv 與 excel 兩個分支) |
| 駭客怎麼打 | ① 任一登入帳號送一則意見回饋,標題以 = 開頭寫一段公式;② 有匯出權限的管理員匯出成 CSV 或 Excel;③ 用試算表軟體打開;④ 公式執行,把同一份檔案其他欄位的內容送到外部網址,或在使用者點過安全提示後執行外部程式 |
| 得手什麼 | 在管理員電腦上執行內容,外洩整份回饋清單 |
| 修正的做法 | ① 與 M09-2 共用同一支 neutralize_formula(),不另寫一份;② csv(csv.DictWriter 每列)與 excel(ws.append 每格)兩個分支都套,避免只修一個入口 |
| 狀態 | ✅ 已修(CM-2055,套件 commit 38de2886)。驗證:實跑兩個分支都中和 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟠 高(總表 #21;OWASP:A05 注入攻擊、A06 不安全的設計;主分類為權限提升,另涉資料外洩) |
| 在哪裡 | 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 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟠 高(總表 #22;OWASP:A01 存取控制失效、A07 身分驗證失效;主分類為權限提升,另涉冒充身分) |
| 在哪裡 | 系統設定模組:登入政策(雙因子、鎖定、登入憑證效期)、員工帳號目錄連線(LDAP)、寄信設定、日誌轉送——這幾組設定全系統只有一份 |
| 攻擊面位置 | 任何一家客戶(含子公司)的管理員。這些設定的修改權限是發給各客戶管理員的,開一個新客戶就多一個打得到的人 |
| 信任邊界位置 | 客戶租戶 ↔︎ 全站共用設定。權限只回答「這個人能不能改設定」,沒回答「他站的層級有沒有資格改全公司那一份」;而設定實體只存在最上層一列,子公司管理員按下儲存就改到了全站 |
| 元件端點位置 | 主專案 PUT /system/security-policy(api/system_config/routes/security_policy_route.py → SecurityPolicyAppService.update_policy)、PUT/DELETE /system/config/{group}/{key}(SystemConfigGroupRoute → GuardedSystemConfigService);套件 jedi-system-core POST /system/config、PUT/DELETE /system/config/{uid}(經 core/plugins/system_core.py:assert_config_capability);日誌轉送 PUT /log-forwarding、POST /log-forwarding/test(core/plugins/api_log.py:_forwarding_guard)。守門在 common/authz/tenant_hq.py |
| 駭客怎麼打 | ① 某家子公司的管理員登入;② 打開系統設定的登入政策,關掉雙因子、把帳號鎖定次數設成無限、把登入憑證效期從五分鐘拉到一個月;③ 更狠的:打開員工帳號目錄設定,把目錄伺服器位址改成自己架的機器;④ 之後全公司每個人用公司帳號登入,帳密都送到他的機器,他回什麼「驗證通過」系統就信什麼——等於接管所有人的登入,而且畫面上看不出被改過 |
| 得手什麼 | 接管全公司所有人的登入,並拿走他們輸入的公司帳密 |
| 修正的做法 | ① 新增第七條守門軸「總部層級」(common/authz/tenant_hq.py):全公司共用的三組設定(寄信、帳號目錄、登入政策)列在 common/constant/company_wide_config.py,寫入時除了查權限,還要判「他是不是總部」,子公司一律 403 GRC_403066,讀取不卡;② 每個寫入入口都掛:套件三條設定路由、主專案依群組讀寫那條、登入政策專屬端點、日誌轉送寫入那欄——守門放在服務層而不只在路由,因為寫入入口不只一條,逐條補必漏(commit e90d8e3cb,驗收補 a25933de8 讓日誌轉送先查權限再查層級);③ 後續修正總部的判定(CM-2202,commit 6cfcb116e):原本用「組織樹第二層」認總部,但最上層底下第二層不只一家,任何一家都能改全站;改成落地安裝版只認「安裝精靈建立的那個客戶租戶」、雲端多租戶版只認原廠,平台管理員一律放行,查不到身分往拒絕倒 |
| 狀態 | ✅ 已修(CM-2054,commit e90d8e3cb+a25933de8;判定修正 CM-2202,commit 6cfcb116e)。驗證:平台/總部/子公司 × 五組設定 × 讀寫矩陣實跑,子公司寫入一律 403;判定修正後 DEV 實打兩種部署模式 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟠 高(總表 #23;OWASP:A01 存取控制失效;主分類為權限提升,另涉竄改資料、事後無法追查) |
| 在哪裡 | 授權管理模組:授權檔、授權異動紀錄、停權紀錄這三張資料表的資料庫門禁規則(決定哪個客戶碰得到哪幾筆) |
| 攻擊面位置 | 任何以子單位帳號身分寫到這三張表的路徑。資料庫門禁是最後一道防線——程式層守門只要漏一支,這一道就是唯一擋板。需要被停權客戶手上有任一子單位帳號 |
| 信任邊界位置 | 子單位 ↔︎ 母公司(租戶 ↔︎ 租戶)。子單位「讀」母公司的授權是設計需要(授權只發給最頂層、底下共用);但當時同一條規則把路徑拆成編號陣列逐一比,等於「讀得到=改得到=刪得到」,資料庫這一側沒有另外守寫入 |
| 元件端點位置 | 資料表 config.tenant_licenses、config.tenant_license_events、config.tenant_license_suspensions 的門禁規則(RLS policy);套件 jedi-license-runtime/migrations/002-license-rls.sql、003-tenant-license-suspensions.sql;已裝機修正 scripts/sql/2026-09-28-fr114-rls-tenant-prefix-unify.sql、2026-10-01-cm2369-license-tables-ancestor-read.sql;讀這三張的功能如 GET /api/1.0/license/status(jedi_license_runtime/api/routing.py) |
| 駭客怎麼打 | ① 被停權的客戶用手上任一個子單位帳號登入;② 經任何一條以子單位身分寫到停權紀錄表的路徑送出刪除;③ 資料庫規則把母公司那列算成「這個子單位碰得到的」,連刪除一起放行;④ 停權紀錄「有一列就代表停權中」,刪掉整棵樹當場復權;⑤ 順手改掉授權到期日與模組清單、刪掉異動紀錄(模組頁未點名具體寫入入口,此處依資料庫規則推論) |
| 得手什麼 | 被停權客戶自行復權並改寫授權內容,且刪得掉事後追查用的異動時間線 |
| 修正的做法 | ① 改刪收緊(CM-2271):這三張連同弱點檢測、遠端代理共九張表,門禁規則改成前綴比對,只碰得到自己與底下;已裝機另開主線 migration 重建(套件檔以 DO 區塊包,既有庫會跳過);新增守衛測試斷言全庫不再有拆陣列寫法;② 讀取開回給子單位(CM-2369):第①步讓子單位讀不到上層的照、被判成沒買全面 403;三張表各加一條只管「讀」的規則(重用既有的 app_tenant_is_ancestor_of_session()),改刪仍只吃前綴規則;③ 真庫測試 12 條:子讀上層=1、子改刪上層=0、兄弟互看=0 |
| 狀態 | ✅ 已修(改刪:CM-2271,主專案 6c74ff0e3+套件 6e0d0d4b,1.21.0;讀取:CM-2369,主專案 3574033a6+套件 ba14e128,1.21.1) |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟠 高(總表 #27;OWASP:A01 存取控制失效、A02 安全設定錯誤;主分類為權限提升,另涉資料外洩) |
| 在哪裡 | 共用基礎模組:每次查資料前告訴資料庫「現在是誰在查」的那一段(所有功能共用) |
| 攻擊面位置 | 所有「還沒有身分」的路徑——登入功能本身、雲端硬碟通知入口這類不帶使用者的端點,以及任何漏掛認證的路由或沒還原身分的背景執行緒 |
| 信任邊界位置 | 未認證請求 ↔︎ 資料庫隔離。原本的寫法是「查不到身分就當作最高權限的系統管理員」,另外「身分裡沒有客戶編號」也被當成最高權限。缺值被解讀成特權,任何忘記設值的路徑都會悄悄拿到全部客戶的資料可見性 |
| 元件端點位置 | 套件 jedi_common/session/database/db.py 的 session_scope()(設定資料庫連線變數 app.is_super_admin、app.allowed_tenant_paths);每次都處在無身分狀態的入口:POST /api/1.0/login、POST /api/1.0/webhooks/google-drive/{tenant_id} |
| 駭客怎麼打 | ① 不需要帳號,打一支處在「無身分」狀態的入口(例如雲端硬碟通知入口);② 這段程式裡的每一次查詢與寫入都被資料庫當成最高權限;③ 那段程式只要有任何一個小錯誤(例如照請求內容去查資料),影響就從「一家公司」放大成「全部公司」 |
| 得手什麼 | 那段程式碰得到的全部客戶資料 |
| 修正的做法 | 分三刀:① 具名系統身分:新增 system_context(name),無人登入的排程以具名方式宣告要繞過隔離,每次進入留一行具名警告(CM-1767);② 拔掉「沒有客戶編號=最高權限」:只認兩個明說的訊號(具名系統身分、原廠租戶路徑),簽章通行證改以真實使用者身分組出(CM-1787);③ 完全沒身分改成什麼都看不到,並留一行警告指出是哪個呼叫點沒帶身分進來;新增「只看得到單一客戶」的 tenant_context 給機器端點用、提權唯讀查詢給登入流程用,代理程式與通知入口從「繞過全部隔離」收成「只看自己那家」(CM-1788);④ 主專案排程全數接上具名系統身分(BE 3af065133、1125ceee1) |
| 狀態 | ✅ 已修(FR-094.1:套件 commit 2bd93918 CM-1767、9a0b4893 CM-1787、fc1cadb9 CM-1788)。⚠️ 總表狀態欄未寫卡號,修正 commit 由本頁回查補上 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟠 高(總表 #29;OWASP:A07 身分驗證失效;主分類為冒充身分) |
| 在哪裡 | 登入與權限模組:個人設定 → 綁定外部帳號(公司帳號、Google) |
| 攻擊面位置 | 任何已登入的員工,最低權限即可 |
| 信任邊界位置 | 使用者 ↔︎ 別人的帳號。綁定是在「某個使用者」底下掛一個外部身分,應該先確認「操作的人就是這個使用者,或有管理帳號的權限」;當時只檢查「目標使用者存在」與「這個外部身分沒被綁走」 |
| 元件端點位置 | 套件 jedi-iam POST /auth-provider(UserAuthProviderCreateRoute)、DELETE /auth-provider/{uid}(UserAuthProviderRoute)→ app/service/user_auth_provider_service.py:register_ad_user()/register_ldap_user()/register_google_user()/delete_user_auth_provider() |
| 駭客怎麼打 | ① 一般員工用自己的帳號登入;② 呼叫綁定 API,目標填管理員的使用者編號、外部帳號填自己的公司帳號;③ 系統沒問「你是不是那個人」,綁定成功;④ 登出,改用「公司帳號登入」並輸入自己的公司帳密——系統查到這個外部身分綁在管理員名下,就讓他以管理員身分進來;⑤ 解綁 API 同理,可以把別人既有的綁定拆掉讓他登不進來 |
| 得手什麼 | 任何員工自助升級成管理員 |
| 修正的做法 | ① 新增 _assert_caller_can_manage(user_uid):呼叫者就是那個使用者本人,或持有「修改使用者」權限,才放行,否則 403;② 四個方法各自呼叫,守門放在任何外部驗證與資料庫寫入之前(不讓攻擊者靠回應差異探別人的目錄帳號);解綁先查出紀錄拿到所屬使用者再判;③ 新增 8 條測試(本人放行、有權限放行、無權限 403),突變測試確認拔掉守門會紅 |
| 狀態 | ✅ 已修(CM-1562,套件 jedi-iam commit 76a8c2dd)。已核對:每一支綁定與解綁前都先過本人或管理權限檢查 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟠 高(總表 #30;OWASP:A01 存取控制失效;主分類為權限提升,另涉竄改資料) |
| 在哪裡 | 登入與權限模組:右上角「個人資料」頁的儲存——人人都能開,這是門檻最低的一條升權路 |
| 攻擊面位置 | 已登入使用者可打的自助修改 API。任何一般員工,在自己的個人資料頁就構成攻擊面 |
| 信任邊界位置 | 使用者 ↔︎ 帳號權限資料。自助頁只該收畫面上真的會改的欄位(暱稱、職稱、信箱、電話、密碼),角色、所屬客戶、部門、狀態只能由管理端改。當時自助那支用 UserRequest(unknown=INCLUDE) 收整包,update_user() 再拿送來的 user_roles 整批覆蓋既有角色關聯 |
| 元件端點位置 | jedi_iam/api/routing.py:PUT /user-profile/{uid}(UserProfileRoute.put,api/routes/user_route.py)→ update_user();管理端改角色走的是另一支 PUT /user/{uid}(UserRoute.put,需 user.update 能力點) |
| 駭客怎麼打 | ① 一般員工登入,打開自己的個人資料頁;② 按儲存時攔下請求,多加一個「角色=管理員角色編號」(或把客戶、部門歸屬清空);③ 系統照收,拿這個欄位整批覆蓋他的角色;④ 重新登入就是管理員(開發環境實測:一般帳號送出後角色真的變成原廠管理員角色) |
| 得手什麼 | 一般員工把自己提升為管理員,或抹掉自己的客戶與部門歸屬 |
| 修正的做法 | ① 新增 UserProfileRequest(unknown=EXCLUDE),白名單只收自助頁實際會送的欄位,user_roles、tenants、org_units、status、is_super_admin 一律不收;② 路由內強制用 get_user_by_uid() 查資料庫現況,把這四項重建塞回去再交給 update_user()——防的不只是「拒收未知欄位」,是「已知欄位也不讓呼叫端決定」;③ 管理端那支不動;④ 新增 4 條測試,突變還原成舊寫法核心斷言轉紅 |
| 狀態 | ✅ 已修(CM-1576,套件 jedi-iam commit 7cf3cf2b)。已核對:角色、客戶、部門、狀態四項都從資料庫重新取出覆蓋 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟠 高(總表 #33;OWASP:A06 不安全的設計、A08 軟體或資料完整性失效;主分類為權限提升,另涉竄改資料) |
| 在哪裡 | 授權管理模組:平台把欠費或違約的客戶「停權」(產品轉唯讀),以及客戶上傳、啟用授權檔 |
| 攻擊面位置 | 已登入的客戶帳號可打的授權上傳與啟用 API;其中啟用的幾條只要登入、不需管理員——刻意如此,開通當下客戶常還沒有管理員在場 |
| 信任邊界位置 | 客戶送來的授權檔 ↔︎ 平台的停權決定。停權原本記在「那一張授權檔」的欄位上,而換照是「新增一張、取代舊的」,新的那張停權欄位是空的。停權是平台對「這個客戶」的決定,不該跟著客戶自己能換掉的資料走 |
| 元件端點位置 | jedi_license_runtime/api/routing.py:POST /api/1.0/license/upload、/license/activation/upload、/license/activation/online → LicenseVerificationService._store_verified_license();停權 POST /license/tenants/{uid}/lock → TenantLicenseAdminService.force_lock_tenant();執法 LicenseGuard.snapshot_for() |
| 駭客怎麼打 | ① 平台把某客戶停權,產品轉唯讀;② 該客戶任一登入帳號把手上那張舊授權檔重新上傳;③ 系統建一筆新的授權紀錄取代現行照,新列的停權欄位是空的;④ 唯讀當場解除,不需要平台同意 |
| 得手什麼 | 被停權的客戶自行改寫自己的授權狀態,平台的最後商務手段形同無效 |
| 修正的做法 | ① 把停權從授權檔搬到客戶身上:新表 config.tenant_license_suspensions,一列=該客戶目前被停權,解除就刪那一列,與授權檔完全脫鉤;② force_lock_tenant()/unlock_tenant() 改寫新表,LicenseGuard.snapshot_for() 與 LicenseStatusService.get_my_license_status() 改讀新表,到期推進改批次查新表決定哪些客戶跳過;③ 舊照上的兩個停權欄位保留當歷史;④ 新表一出生帶著寫反的資料庫門禁,子單位可直接刪掉母公司的停權紀錄——另案修正(見 M18-8) |
| 狀態 | ✅ 已修(CM-1579,主專案 commit a039aa287、套件 8f9155a9) |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟠 高(總表 #199;OWASP:A05 注入攻擊;主分類為冒充身分) |
| 在哪裡 | 主系統的雲端硬碟整合:「系統設定 → 雲端硬碟 → 連結 Google 硬碟」,Google 同意後跳回我們系統的那個小頁面 |
| 攻擊面位置 | 回呼網址設計上不需要登入(Google 要能把人導回來),網路上任何人都能做一個指向它的連結 |
| 信任邊界位置 | 網址上的參數 ↔︎ 本站網域裡執行的頁面程式。這個頁面與產品同網址、登入狀態存在瀏覽器裡,所以頁面程式等同本站身分。網址上的錯誤訊息應該只是「代碼」;當時被原樣塞進頁面的 <script> 裡,2026 年 6 月用的 json.dumps 不會跳脫 < > /,擋不住 |
| 元件端點位置 | 主專案 api/cloud_integration/__init__.py:GET /integrations/google-drive/callback(GoogleDriveCallbackRoute)→ api/cloud_integration/routes/google_drive_integration_route.py 的 _callback_response()/_callback_html() |
| 駭客怎麼打 | ① 不需要帳號,做一個回呼網址,error 參數填 </script><script>… 開頭的特製文字;② 寄給一個已登入的使用者;③ 對方一點,頁面被切成兩段,攻擊者的程式在本站網域裡跑;④ 讀走瀏覽器裡的登入權杖送出去;⑤ 用他的名義看、改、刪他看得到的所有稽核資料,受害者是管理員就是整家公司 |
| 得手什麼 | 受害者的登入身分 |
| 修正的做法 | 三道各自有效:① 白名單:網址上的錯誤只認 access_denied,其他情況依錯誤碼對到 invalid_state/exchange_failed 等固定代碼,例外訊息不再回顯、只進後端紀錄;② 跳脫:內嵌值再把 < > & 換成跳脫序列;③ CSP:頁面只准執行帶「本次隨機碼」的那一段程式;④ 前端改用後端給的固定代碼查翻譯顯示 |
| 狀態 | ✅ 已修,1.21.0 出貨(CM-2200,主專案 commit f0a740bd7、前端 9569cf6)。驗證:新增 8 條測試+突變測試,DEV 實打四種情境、瀏覽器無 CSP 違規 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟠 高(總表 #201;OWASP:A01 存取控制失效;主分類為權限提升) |
| 在哪裡 | 系統設定模組:寄信伺服器、員工帳號目錄(LDAP)、登入政策、日誌轉送這幾組全站只有一份的設定;它是 M22-1 修法的漏洞 |
| 攻擊面位置 | 任何一家客戶的總部管理員。M22-1 修好之後子公司改不動了,但「總部」的判定是「組織樹第二層」,而原廠底下的第二層每一家客戶都是,所以每家客戶的總部管理員都打得到 |
| 信任邊界位置 | 客戶 ↔︎ 客戶(租戶 ↔︎ 租戶),以及客戶 ↔︎ 原廠平台。全站那一份設定只該由「整套系統的擁有者」改;當時用路徑深度認總部,深度只分得出上下層,分不出「這家客戶」與「另一家客戶」 |
| 元件端點位置 | 判定在主專案 common/authz/tenant_hq.py 的 viewer_is_tenant_hq()/require_tenant_hq();查「安裝精靈建的那家客戶」在 infra/setup/setup_state_reader.py 的 primary_customer_tenant_id。受保護的寫入入口同 M22-1:PUT /api/1.0/system/security-policy、PUT/DELETE /api/1.0/system/config/{group}/{key}、套件 jedi-system-core POST /system/config、PUT/DELETE /system/config/{uid}、日誌轉送 PUT /log-forwarding |
| 駭客怎麼打 | ① 客戶 B 的總部管理員登入(他的公司與客戶 A 一樣掛在原廠底下第二層);② 打開系統設定的員工帳號目錄或寄信設定;③ 系統問「你是不是總部」——深度符合,放行;④ 他改掉的是全站那一份,客戶 A 與原廠平台本身的登入、寄信從此走他指定的伺服器(統籌者在開發環境用實際公司路徑測過) |
| 得手什麼 | 改掉其他客戶與原廠共用的登入與寄信設定,接管別家的登入流程 |
| 修正的做法 | ① 只改判定、四個呼叫點不動:平台管理員一律可改;雲端多租戶版(DEPLOYMENT_MODE 不是 host)一律不准客戶改;落地安裝版只認「安裝精靈建立的那個客戶租戶」——原廠底下第一個建的那家,用繞過隔離的獨立連線查,因為子公司看不到上層會誤判;② 不用客戶名稱或描述欄認(可被編輯);③ 查不到身分、不在請求裡、查不到客戶租戶一律往拒絕倒;④ 新增 10 條測試,突變改回「深度=2」4 條轉紅 |
| 狀態 | ✅ 已修,1.21.0 出貨(CM-2202,主專案 commit 6cfcb116e)。驗證:開發環境兩種部署模式實打,落地版只有精靈建的客戶與平台管理員 200,另兩家客戶與子公司 403;雲端版只有平台管理員能改;讀取各身分不受影響 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟡 中(總表 #37,與 M06-7 同一件;OWASP:A01 存取控制失效;主分類為權限提升,另涉竄改資料、事後無法追查) |
| 在哪裡 | 弱點檢測整合模組:任務頁上的掃描操作——開始掃描、取消、單台重跑、單台取消、立即開始、整組取消、刪單筆、整組刪,共八個會改狀態的功能 |
| 攻擊面位置 | 已登入、且是該專案參與者的任何角色,包含產品定義上「只能看、沒有待辦」的唯讀角色。需要知道任務或執行組編號(專案頁看得到) |
| 信任邊界位置 | API 層 ↔︎ 任務經手人。該守的是「你是不是這張任務的負責人、或這個專案的管理者」;這八支借用的是稽核流程那支「任一專案參與者都放行」的寬鬆守門 |
| 元件端點位置 | jedi_detection/api/routing.py:POST /api/1.0/detection-tools/jobs/{job_uid}/execute、/jobs/{job_uid}/cancel、/execution-groups/{group_uid}/assignments/{assignment_uid}/rerun、…/cancel、…/start-now、/execution-groups/{group_uid}/cancel、DELETE /execution-groups/{group_uid}、DELETE /executions/{execution_uid};處理在 jedi_detection/app/service/detection_orchestration_service.py 八處,守門呼叫主專案 WorkflowExecutionService.assert_job_operator |
| 駭客怎麼打 | ① 以唯讀角色登入,打開專案的弱點檢測任務;② 直接按或直接打「開始掃描」,系統只確認他是專案成員就放行;③ 掃描帶著維運帳密連進客戶的正式機器;④ 接著反覆取消再重派干擾正常檢測,或刪掉失敗的執行紀錄——掃描紀錄被一個不該動手的人改掉 |
| 得手什麼 | 以唯讀身分發動帶帳密的掃描,並刪改掃描執行紀錄 |
| 修正的做法 | ① 稽核流程那邊先新開嚴格版守門 assert_workflow_job_operator(只放行任務被指派人與專案 manager,見 M06-7);② 主專案加一層包裝:套件手上只有流程實例編號,新增 assert_job_operator_by_workflow_execution_id 反查後委派嚴格守門,WorkflowExecutionService.assert_job_operator(workflow_execution_id, template_job_id) 提供一行呼叫;查無流程實例回 404;③ 套件八處改接:八支寫入功能由 assert_project_participant 換成 assert_job_operator;掃描跑完由代理程式回報觸發的自動完成路徑沒有使用者身分,維持原樣 |
| 狀態 | ✅ 已修,1.21.0 出貨(FR-114.1-2,CM-2022,套件 commit 88e519b4+主專案 e10d10a83) |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟡 中(總表 #37,與 M03-14 同一件;OWASP:A01 存取控制失效;主分類為權限提升,另涉竄改資料、事後無法追查) |
| 在哪裡 | 稽核流程模組:任務的「完成」與「退回上一關」兩個按鈕 |
| 攻擊面位置 | 已登入、是該專案參與者的任何角色,包含唯讀角色(原意是開放給外部顧問或其他單位同事觀看)。需要知道任務編號 |
| 信任邊界位置 | API 層 ↔︎ 任務經手人。權限政策寫明「任何角色的專案參與者都會通過」,另一處又把這個角色定義成「純瀏覽、沒有待辦」——產品承諾與判斷邏輯互相矛盾,守門只守到「是不是專案的人」 |
| 元件端點位置 | 主專案 api/flow_engine/__init__.py:POST /api/1.0/flow-engine/task/complete/{id}(JobCompleteRoute)、POST /api/1.0/flow-engine/task/revert/{id}(JobRevertRoute)→ app/flow_engine/service/workflow_execution_service.py 的 complete_job/revert_job;守門 app/flow_engine/service/job_operator_guard.py、common/authz/workflow.py |
| 駭客怎麼打 | ① 以唯讀角色登入,打開別人負責的稽核任務;② 按「完成」或「退回」,系統只確認他是專案成員就放行;③ 任務、問卷、流程狀態跟著被推進或退回;④ 稽核紀錄上的經手人變成這個只能看的人 |
| 得手什麼 | 以旁觀者身分結案或退回別人的稽核任務,改動流程狀態 |
| 修正的做法 | ① 新開嚴格版守門(FR-114.1-1):common/authz/workflow.py 新增 assert_workflow_job_operator,前段政策與既有那支一致,最後多問「你是這張任務的被指派人、還是專案 manager」;不收嚴既有那支,因為佐證文件的增刪改也吃它;完成與退回各改一行呼叫;新增錯誤碼 GRC_NOT_JOB_OPERATOR;② 保留兩條拿掉會壞的旁路:無使用者身分(掃描自動完成)不守、輪次主流程任務沿用已知專案編號;③ 歸屬判斷改走唯一入口(CM-2119):「任務屬於哪個專案」原本查指派表,未指派的任務查不到會被誤擋,改委派 JobProjectOwnershipQuery(走輪次鏈) |
| 狀態 | ✅ 已修,1.21.0 出貨(FR-114.1-1,CM-2021,主專案 commit 18b2ba06c;誤擋修正 CM-2119,e8445ae16)。驗證:九種角色組合逐條跑過;未指派任務的成員改前 403、改後 200,非成員仍 403 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟡 中(總表 #41;OWASP:A01 存取控制失效;主分類為權限提升,另涉竄改資料) |
| 在哪裡 | 任務與成員管理模組:新增一筆任務指派(把某張任務指派給某位成員)。模組頁沒有逐條表,此條依總表描述與修正 commit 寫成 |
| 攻擊面位置 | 已登入使用者可打的任務指派 API。任何人自己開一個新專案就自動是管理者,門檻等於零 |
| 信任邊界位置 | 專案 ↔︎ 專案(同一客戶內)。系統只查「操作者是不是請求裡那個專案的管理者」+「被指派人是不是那個專案的成員」,從不核對「這張任務本身屬於哪個專案」——兩道檢查問的都是呼叫者自己填的那個專案 |
| 元件端點位置 | jedi_task_platform/participant/api/routing.py:POST /api/1.0/task-assignee(TaskAssigneeRoute.post,api/routes/task_assignee_route.py)→ app/service/task_assignee_service.py 的 add_task_assignee;任務歸屬由主專案 infra/flow_control/task_existence_query.py 的 FlowEngineTaskExistenceQuery.get_project_id 提供 |
| 駭客怎麼打 | ① 小明自己開一個新專案,成為它的管理者;② 送出指派時,專案欄填自己的專案、任務欄填別人專案的任務、被指派人填自己;③ 兩道檢查都問自己的專案,全部通過;④ 別人專案的任務被指派給小明,他就能以經手人身分操作那張任務 |
| 得手什麼 | 把自己塞進別人專案的任務,取得那張任務的經手權 |
| 修正的做法 | ① 套件的任務查詢介面 ITaskExistenceQuery 新增 get_project_id(job_uid);② add_task_assignee 寫入前比對任務真正所屬專案與請求的專案,查不到或不相等一律回「找不到」,不回 403 以免被當探測工具;③ 主專案實作那支查詢,一行委派全系統唯一的 JobProjectOwnershipQuery——刻意不查指派表(那正是這道比對要保護的表);④ 原本要在流程實例表補專案欄位,決策者裁作廢,避免變成兩份真相;⑤ 其他寫入點逐一核過:改與刪只能動既有紀錄、批次指派是恆回空的殘留功能,不套 |
| 狀態 | ✅ 已修,1.21.0 出貨(FR-114 CM-2071,套件 commit ef9a313b、主專案 fccfe4db7)。驗證:同專案 200、專案與任務錯配 404 且沒寫入;DEV 現有 166 筆指派紀錄用新判斷不一致 0 筆 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟡 中(總表 #65;OWASP:A01 存取控制失效;主分類為權限提升) |
| 在哪裡 | 合規文件核心模組:「合規資源庫」的建立——選一個框架版本,系統複製一整套控制項目錄做成公司自己的範本庫 |
| 攻擊面位置 | 已登入使用者可打的建立資源庫 API,公司內任何角色(含唯讀帳號) 都打得到;而隔壁做同樣事情的「新增範本」入口是有要求權限的 |
| 信任邊界位置 | 使用者 ↔︎ 「建立範本」這項功能權限。同一件事有三條路(手動建、用 Excel 建、用 Word 建),應該在三條路共同經過的地方問「你有沒有建立範本的權限」;當時只有其中兩條問了。又因為每建一次就要複製整套目錄、沒有次數上限,等於一般帳號可以用很小的代價把資料庫灌大。不跨客戶 |
| 元件端點位置 | 主專案 api/oscal/__init__.py:POST /api/1.0/oscal/resource-libraries(ResourceLibraryCreateRoute,api/oscal/routes/resource_library_route.py)→ app/oscal/service/resource_library_app_service.py 的 create_resource_library();用 Excel 建新庫那條在 app/oscal/service/ssp_excel_import_app_service.py 的 _verify_source_exists();對照組是手動新增 POST /api/1.0/module-frame(ModuleFrameRoute) |
| 駭客怎麼打 | ① 一個沒被給「建立範本」權限的帳號(例如唯讀角色)登入;② 直接送建立資源庫的請求,選任一已發佈的框架版本;③ 系統只驗登入就複製一整套控制項目錄;④ 寫個迴圈重複送,每次都多一整套目錄,沒有上限 |
| 得手什麼 | 繞過「不給建立權」的設定自建範本庫,並能不受限地灌大資料庫 |
| 修正的做法 | ① 建立資源庫的路由掛上 @require_capability("module-frame.create"),與手動新增、Word 建新庫同一顆權限;② 盤點時發現用 Excel 建新庫那條也沒門,在 _verify_source_exists() 補上同一顆權限檢查(新錯誤碼 GRC_403067)——三條建庫路徑只擋兩條等於沒擋;③ 在三條路共同經過的 create_resource_library() 加每家客戶每小時 10 次上限(assert_within_tenant_limit(),新錯誤碼 GRC_429001),上限綁客戶而不綁人,換帳號繞不過去;快取連不上時放行並記警告 |
| 狀態 | ✅ 已修,1.21.0 出貨(CM-2040,主專案 commit 60015232f)。CM-2179(commit 94275d72f)只確認上述三處權限仍在,它本身修的是同模組第 30 條「草稿版本也能拿去建庫」,見下方「與總表對不上」說明 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟡 中(總表 #68;OWASP:A01 存取控制失效;主分類為權限提升,另涉竄改資料) |
| 在哪裡 | 任務與成員管理模組:「群組層成員」的新增、修改、刪除——這張表被拿來算「哪些專案我看得到」。模組頁沒有逐條表,此條依總表描述與拆除 commit 寫成 |
| 攻擊面位置 | 若有任何呼叫者接上這三支,守門形同虛設;查證時全系統零呼叫(沒有路由、前端、DI 容器在用),屬未爆彈 |
| 信任邊界位置 | 呼叫者 ↔︎ 成員資料(決定誰看得到哪個專案)。這組服務的守門靠注入的角色服務,但系統啟動時組裝它的那一行從沒接上角色服務(同檔其他手足都有接)——檢查程式在,但永遠拿不到判斷依據 |
| 元件端點位置 | 套件 jedi_task_platform/participant/app/service/project_group_participant_service.py 的 add_project_group_participant/update_project_group_participant/delete_project_group_participant;漏接線處在主專案 di_containers/flow_engine/project_participant_containers.py 的 project_group_participant_service;資料表 project_group_participants |
| 駭客怎麼打 | ① 今天沒有入口可打;② 只要哪天有人把這三支接上路由或畫面;③ 任何呼叫者就能把自己加進任一專案的群組層成員;④ 系統算「我看得到哪些專案」時會算進去,等於自己幫自己開了別人專案的門 |
| 得手什麼 | (若被接上)把自己加進任何專案的成員名單 |
| 修正的做法 | 拆除了什麼:① 三支寫入方法整支刪除;② 刻意不補接線——補上會安靜改變行為;③ 同批刪除同性質的流程成員增改刪(其守門找不到紀錄時提早放行、真正檢查永遠跑不到)與三支舊版任務指派查詢;④ 保留查詢方法(AI 儀表板在用)與專案刪除時的清理 |
| 狀態 | 🗑️ 裁定刪除,已刪除,1.21.0 出貨(CM-2044,套件 commit 9c9029fd) |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟡 中(總表 #69;OWASP:A01 存取控制失效;主分類為權限提升) |
| 在哪裡 | 任務與成員管理模組:「流程參與者」的新增、修改、刪除——一整組從沒啟用過的功能。模組頁沒有逐條表,此條依總表描述與拆除 commit 寫成 |
| 攻擊面位置 | 今天沒有:全系統零呼叫(沒有路由、前端、DI 容器在用)。現在擋住它的,是它本身就壞著 |
| 信任邊界位置 | 呼叫者 ↔︎ 流程成員名單。增改刪三支都先呼叫「是不是這個流程的管理者」的檢查,但那支檢查找不到紀錄時提早結束、當作通過,真正比對角色的那一行永遠跑不到——守門的形狀在,判斷不在 |
| 元件端點位置 | 套件 jedi_task_platform/participant/app/service/process_participant_service.py 的 add_process_participant/update_process_participant/delete_process_participant 與守門 _assert_manager_for_process()(皆已刪) |
| 駭客怎麼打 | ① 今天沒有入口可打;② 只要哪天有人把這三支接上路由;③ 任何呼叫者對一個查不到既有成員紀錄的流程送新增,守門提早放行;④ 就把自己加進那個流程的參與者,取得經手權 |
| 得手什麼 | (若被接上)把自己加進任何流程的參與者名單 |
| 修正的做法 | 拆除了什麼:① 三支寫入方法整支刪除;② 已成孤兒的守門 _assert_manager_for_process() 一併刪除;③ 保留查詢方法 get_process_participants() 與專案刪除時的清理;④ 與 M10-7、M10-18 同一批,查證全系統零呼叫後才刪 |
| 狀態 | 🗑️ 裁定刪除,已刪除,1.21.0 出貨(CM-2044,套件 commit 9c9029fd)。驗證:套件成員相關 79 測試、主專案任務指派相關 16 測試通過 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟡 中(總表 #72,與 M17-11 同一件;OWASP:A07 身分驗證失效、A02 安全設定錯誤;主分類為資料外洩) |
| 在哪裡 | 通知模組檢視時查到:三支資料調整/測試腳本寫死系統管理員帳號密碼,其中兩支寫成「環境變數沒設就用這個」 |
| 攻擊面位置 | 拿得到程式庫的人;另一面是執行腳本的人自己——變數忘了設,腳本會靜默用真密碼跑下去,操作者不會被告知 |
| 信任邊界位置 | 版控 ↔︎ 祕密存放處。「沒設就用預設值」本來是方便,但預設值填的是真密碼,等於把祕密當成程式的一部分出貨 |
| 元件端點位置 | scripts/migrate_2026-08-09_fr062_existing_tenant_licenses.py、scripts/seed_2026-08-01_fr059_detection_profiles.py(兩支「沒設就用真密碼」);另有 scripts/smoke_test_ssp_docx_parser.py、scripts/smoke_test_ssp_docx_preselect.py、scripts/e2e_test_module_frame_with_docx.py 三支測試腳本同樣帶密碼 |
| 駭客怎麼打 | ① 拿到程式庫;② 打開任一支腳本,預設值那一行就是管理員密碼;③ 用它登入產品,取得系統管理員身分 |
| 得手什麼 | 一組可用的系統管理員帳密 |
| 修正的做法 | ① 兩支 migration/seed 腳本拿掉寫死的預設值,環境變數沒設就中止並印出設定方式,不再靜默用真密碼;② 三支測試腳本改用環境變數帶入;③ 文件與對話紀錄裡的殘留字串(約 100 多檔)全數清除,改標「密碼請查 .env 或部署文件」;④ 換發:模組頁記為已換發,但 CM-2048 commit 註明「系統管理員密碼本輪不換發,待正式上版前統一處理」——兩處說法不一致 |
| 狀態 | ✅ 已修正(CM-2048,commit e8e134a4e)。手測:兩支腳本未設環境變數時確實中止並提示 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟡 中(總表 #72,與 M16-8 同一件;OWASP:A07 身分驗證失效、A02 安全設定錯誤;主分類為資料外洩) |
| 在哪裡 | 公告模組檢視時查到:一支資料庫調整腳本寫「環境變數沒設就用這個密碼」,那個密碼是真的管理員密碼 |
| 攻擊面位置 | 拿得到程式庫的人;以及忘了設變數就執行的維運人員 |
| 信任邊界位置 | 版控 ↔︎ 祕密存放處。同 M16-8 |
| 元件端點位置 | scripts/migrate_2026-08-09_fr062_existing_tenant_licenses.py(環境變數讀取那一行的預設值) |
| 駭客怎麼打 | ① 拿到程式庫;② 打開這支腳本,讀出預設值;③ 用它以管理員身分登入 |
| 得手什麼 | 一組可用的系統管理員帳密 |
| 修正的做法 | ① 拿掉寫死的預設值,環境變數沒設就中止並提示(與 M16-8 同一個 commit);② 版控殘留字串清除;③ 換發時程同 M16-8(commit 註明排正式上版前) |
| 狀態 | ✅ 已修正(CM-2048,commit e8e134a4e) |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟡 中(總表 #73,與 M15-6 同一件;OWASP:A03 軟體供應鏈失效、A07 身分驗證失效;主分類為竄改資料) |
| 在哪裡 | 公告模組檢視時順手做的密碼掃描撈到的(與公告功能無關):一份分散式檔案代理程式的開發交接文件,明文寫著公司內部套件倉庫與檔案儲存服務的帳密 |
| 攻擊面位置 | 主專案的程式碼倉庫——這份文件與它產生的網頁、文件站鏡像都進了版本控制。任何讀得到程式碼的人都構成攻擊面,不需要系統帳號;要真的用上帳密還需要連得到公司內網 |
| 信任邊界位置 | 「讀得到程式碼的人」↔︎「有權往套件倉庫發布元件的人」。發布權只該在打包機與發版的人手上(帳密放在打包機不進版控的設定檔);交接文件把帳密原文抄了進去,還寫著「已加入 gitignore」——被忽略的是那個設定檔,不是這份文件 |
| 元件端點位置 | 外流處:docs/features/FR-039-2606-distributed-file-agent/handoff/2026-06-18-FR039-handoff.md(+同目錄 index.html、docs/features-site/ 鏡像);被打穿的發布口:套件 monorepo 各套件 publish.sh 用的 poetry publish --build -r nexus;吃這個倉庫的安裝端:主專案 pyproject.toml 的 [[tool.poetry.source]] nexus |
| 駭客怎麼打 | ① 任何能讀主專案程式碼的人打開這份交接文件;② 照抄套件倉庫的管理員帳密,在內網登入;③ 上傳一個看起來正常、但夾帶後門的內部套件新版本;④ 下次打包機重建時,這個版本被當成公司自己的元件裝進產品,跟著安裝包送到客戶機房;⑤ 同一份文件裡的檔案儲存金鑰則可直接改寫存放在那裡的稽核證據 |
| 得手什麼 | 往客戶安裝包裡塞自己程式碼的位置,以及開發環境檔案儲存的讀寫權 |
| 修正的做法 | ① 換發:套件倉庫與檔案儲存的帳密都已換發,舊值失效(環境操作,不在 commit 裡);② 清出版控:先改來源 md,帳密改寫成「請查部署文件」,同時改掉「已加入 gitignore」的誤導敘述;③ 依序重建網頁、同步文件站鏡像,最後補清對話紀錄殘留——依較寬的搜尋多清出 7 個卡片沒列到的檔;④ 全 repo 交叉搜尋確認殘留樣式 0 命中 |
| 狀態 | ✅ 已修(CM-2048,主專案 commit e8e134a4e)。歷史 commit 仍有舊值,因已換發不改寫歷史 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟡 中(總表 #73,與 M17-10 同一件;OWASP:A03 軟體供應鏈失效、A07 身分驗證失效;主分類為竄改資料) |
| 在哪裡 | AI 儀表板模組檢視範圍外另外撈到的(與儀表板功能無關):與 M17-10 同一份交接文件、同一段,一共明文寫了三樣——套件倉庫管理員帳密、檔案儲存服務的金鑰、一組測試客戶的畫面登入帳密 |
| 攻擊面位置 | 同 M17-10:主專案程式碼倉庫,任何讀得到程式碼的人;三樣都只在公司內網有效,外人拿到用不上——這是評中而非高的理由 |
| 信任邊界位置 | 三條邊界被同一份文件同時打穿:讀程式碼的人 ↔︎ 套件倉庫發布權、讀程式碼的人 ↔︎ 檔案儲存(稽核證據)、讀程式碼的人 ↔︎ 測試環境登入畫面。三樣都該只在部署文件或各機器不進版控的設定裡 |
| 元件端點位置 | 外流處:docs/features/FR-039-2606-distributed-file-agent/handoff/2026-06-18-FR039-handoff.md(套件倉庫帳密那行、UPDATE public.system_configs 設定檔案儲存那段、測試客戶登入帳號兩處);對應服務:主專案 pyproject.toml 的套件倉庫來源、檔案儲存的連線設定存在 system_configs 表 |
| 駭客怎麼打 | ① 讀得到程式碼的人打開交接文件;② 拿套件倉庫管理員帳密推一個帶後門的元件,等下次打包裝進客戶手上的程式;③ 拿檔案儲存金鑰直接改寫存放的稽核證據,繞過系統所有權限檢查;④ 拿那組畫面帳密直接登入測試環境 |
| 得手什麼 | 往安裝包塞東西的位置、稽核證據的讀寫權、一個可直接登入的帳號 |
| 修正的做法 | ① 三樣都換發,舊值失效(環境操作);② CM-2048 清掉交接文件裡三處明文,全部改寫成「請查 .env 或部署文件」,並重建網頁與文件站鏡像;③ 同一組套件倉庫密碼還散在打包腳本與前端三支設定檔,另由 M23-6 收——只清那四個檔、漏了這份交接文件等於沒清乾淨 |
| 狀態 | ✅ 已修(CM-2048,主專案 commit e8e134a4e)。三樣已換發,殘留字串已清 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟡 中(總表 #78;OWASP:A08 軟體或資料完整性失效、A04 加密機制失效;主分類為竄改資料) |
| 在哪裡 | 弱點檢測整合模組:用「網址」建立檢測基準——平台下載解析一次,派工時把網址交給客戶機房的代理程式自己去下載並執行 |
| 攻擊面位置 | 平台與代理程式下載規則包的那段網路路徑。不需要我們系統的帳號,需要的是站在傳輸路徑上能動手腳的位置(同網段、DNS、代理伺服器) |
| 信任邊界位置 | 外部網址下載回來的內容 ↔︎ 客戶機房的代理程式。上傳型規則包有指紋、代理程式會比對;網址型當時連 http:// 也收、而且不記指紋——代理程式拿到什麼就跑什麼。後端自己的下載器本來只准 https,同一系統兩套標準 |
| 元件端點位置 | jedi_detection/api/routing.py:POST /api/1.0/detection-tool-profiles、PATCH /detection-tool-profile-versions/{uid}/source → DetectionProfileService._resolve_source();平台下載與落庫 DetectionProfileExtractionService;派工 common/detection_profile_ref.py 的 build_payload() |
| 駭客怎麼打 | ① 客戶管理員用一個 http 網址建了檢測基準;② 攻擊者站在代理程式下載的網路路徑上;③ 代理程式領到掃描工作去下載時,回一包換過的規則;④ 代理程式手上有客戶主機帳密,直接執行那包規則,掃描報告也一起造假;⑤ 因為不加密,連破解都不用 |
| 得手什麼 | 在客戶內網的代理程式上執行任意程式碼,並偽造掃描結果 |
| 修正的做法 | ① 新登記只收 https:_resolve_source() 的白名單直接取既有的 safe_http_fetch.ALLOWED_SCHEMES,不另立一份;不合格回新錯誤碼 DETECTION_TOOLS_400013;② 既有 http 基準不強制失效(避免客戶既有基準一次全滅),但抽取時寫一筆醒目警告;③ 平台第一次抽取成功時,把下載到的壓縮檔 sha256 落庫,並由 build_payload() 隨派工下發給代理程式;④ 打折:代理程式端實際比對指紋已裁定另開小卡,本次只把指紋記進派工資料 |
| 狀態 | ✅ 已修,1.21.0 出貨(CM-2057,套件 commit c0fe19fd)。代理端比對指紋屬後續小卡,本次未見對應 commit |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟡 中(總表 #98;OWASP:A07 身分驗證失效、A02 安全設定錯誤;主分類為權限提升) |
| 在哪裡 | 安裝程式:每裝一套都會建立同一組原廠最高權限帳號(原廠系統殼帳號),不強制首次登入就改密碼 |
| 攻擊面位置 | 知道或破解出那組密碼的人,加上連得到任一套安裝的登入頁。帳號刻意設計成刪不掉、停不掉 |
| 信任邊界位置 | 原廠 ↔︎ 客戶環境。這個帳號是原廠進入客戶環境支援用的鑰匙;每套都同一把,等於一把鑰匙開所有客戶的門 |
| 元件端點位置 | scripts/init/06-admin.sql(只存 bcrypt 雜湊,建立 tenant 1 底下的最高權限帳號);刪除/停用/降權的守門 app/auth/service/user_app_service.py:assert_root_admin_mutable();登入 POST /login |
| 駭客怎麼打 | ① 從一套安裝裡取得那組帳號的雜湊(例如進得了某台主機的資料庫)或從原廠內部文件外流;② 離線破解出密碼;③ 拿這組帳密登入任何一套裝出去的系統,都是最高權限 |
| 得手什麼 | 每一套已出貨安裝的最高權限帳號 |
| 修正的做法 | 裁定不修。理由:① 這是原廠系統殼帳號,密碼只有原廠知道、不交給客戶,用於原廠支援;② 客戶日常使用的管理員帳號由客戶在設定精靈自行建立、自行設密碼,安裝程式不經手、不印出任何密碼;③ 版控只存 bcrypt 雜湊,明文不入版控、不進手冊、不進安裝紀錄 |
| 狀態 | 🚫 裁定不修 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟡 中(總表 #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 複查確認兩支都以具名系統身分執行 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟡 中(總表 #151;OWASP:A05 注入攻擊;主分類為權限提升) |
| 在哪裡 | 合規文件核心模組:控制項實作現況(SoA)批次匯出成 Excel,改完再匯回 |
| 攻擊面位置 | 能寫控制項現況描述的使用者。描述欄是自由文字 |
| 信任邊界位置 | 我們的資料 ↔︎ 複核者的電腦。E 欄現況描述、H 欄檢查項目描述原樣寫進儲存格,= 開頭就被判成公式;應該在寫出時標成純文字 |
| 元件端點位置 | 主專案 api/oscal/__init__.py:GET /ssp/{ssp_uid}/control-implementations/export(SspControlImplExportRoute)→ app/oscal/service/ssp_control_impl_import_service.py 的 export_excel()(兩個寫入點) |
| 駭客怎麼打 | ① 能寫現況描述的人填一段 = 開頭的公式;② 複核的人匯出 Excel 打開;③ 按下「啟用內容」,公式在複核者自己的電腦上執行 |
| 得手什麼 | 在複核者電腦上執行內容 |
| 修正的做法 | ① 兩個寫入點改走 jedi-common 的 set_text_cell():把儲存格標「強制文字」旗標(quotePrefix),值一個字不改;② 不用加單引號那種做法——這份檔會被匯回,單引號會被讀成內容,每來回一趟多一撇 |
| 狀態 | ✅ 已修,1.21.0 出貨(FR-114 CM-2055,主專案 commit f85a82edb,共用函式在套件 38de2886)。驗證:存檔讀回值原樣、無多餘單引號 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟡 中(總表 #166;OWASP:A05 注入攻擊;主分類為權限提升) |
| 在哪裡 | 合規文件核心模組:下載 SSP 匯入範本 Excel(空白或已填)、框架版本空白範本、帳號匯入範本 |
| 攻擊面位置 | 能編輯計畫內容(元件名、系統名、範本名等)的使用者 |
| 信任邊界位置 | 我們的資料 ↔︎ 下載者的電腦。資料從進系統到變成 Excel,六個關卡(上傳解析、存檔、讀出、組下拉選單、寫儲存格、檔案設定)可以擋、實際擋了零個;產生的檔案還設成「打開時重新計算」 |
| 元件端點位置 | 主專案 api/module_frame/__init__.py:GET /module-frame/{uid}/ssp-import-template、GET /ssp/{ssp_uid}/excel-template、GET /ssp-import-template;jedi-iam GET /user/download/sample(UserUploadSampleDownloadRoute)→ app/module_frame/excel_template/generator.py、lookup_builder.py、app/auth/service/user_import_template_app_service.py;上傳端 app/oscal/service/excel_parser/sheet_handlers.py |
| 駭客怎麼打 | ① 能編輯計畫的人把元件名或系統名改成 =HYPERLINK(...) 之類的公式;② 別人下載這份範本 Excel;③ 打開時檔案自動重算,公式在下載者電腦上執行 |
| 得手什麼 | 在下載者電腦上執行內容 |
| 修正的做法 | ① 七個寫出點全改用 set_text_cell()(標純文字、值不改):下拉選單值、輔助欄、逐列表、直式表值欄、說明頁兩欄(說明頁含使用者命名的範本名)、帳號匯入範本的下拉值;② 刻意不動系統自己寫的自動帶入公式與「打開時重算」設定;③ 上傳端讀格時遇到 = + - @ 開頭只記一筆警告(表名、格位、前 20 字),內容不改、不擋(負數、條列會誤報) |
| 狀態 | ✅ 已修,1.21.0 出貨(FR-114 CM-2189,主專案 commit 895db0ef8)。驗證:DEV 四種範本下載 15/15 項通過。打折:以 openpyxl 讀回型別判定,沒用真 Excel 開檔看畫面 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟡 中(總表 #167;OWASP:A05 注入攻擊;主分類為權限提升) |
| 在哪裡 | 合規文件核心模組:所有範本 Excel 都帶一個隱藏的「使用者」下拉分頁,內容是暱稱 |
| 攻擊面位置 | 修改自己個人資料的 API,任何登入帳號都能改自己的暱稱,門檻比 M11-20 低得多 |
| 信任邊界位置 | 使用者自填的暱稱 ↔︎ 全公司下載者的電腦。暱稱被寫進每份範本的下拉清單,寫出時應標純文字;當時原樣寫入 |
| 元件端點位置 | 種入:jedi-iam PUT /user-profile/{uid}(UserProfileRoute);引爆:同 M11-20 的四支範本下載 → app/module_frame/excel_template/lookup_builder.py 的 build_lookup_sheet()(_lookup_users 分頁) |
| 駭客怎麼打 | ① 任一登入帳號把自己的暱稱改成一段公式;② 之後公司裡任何人下載任何一份範本(連空白範本)都帶著它;③ 顧問把範本帶到客戶那裡打開,公式在他的電腦上執行 |
| 得手什麼 | 在全公司任何下載者電腦上執行內容 |
| 修正的做法 | 與 M11-20 同一批檔案一次改完:lookup_builder.py 組下拉值與輔助欄時改用 set_text_cell(),暱稱存成純文字、值一字不改 |
| 狀態 | ✅ 已修,1.21.0 出貨(FR-114 CM-2189,主專案 commit 895db0ef8)。驗證:DEV 暱稱暫改 =1+1,三種範本的使用者分頁都是純文字(測完已還原) |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟡 中(總表 #168;OWASP:A01 存取控制失效;主分類為權限提升,另涉竄改資料) |
| 在哪裡 | 合規文件核心模組:範本的「整批匯入」——帶既有範本編號時是覆蓋,不帶時是建新範本。範圍是同一家公司內 |
| 攻擊面位置 | 已登入、只被授權「建立範本」、沒被授權「修改範本」的人 |
| 信任邊界位置 | 建立權 ↔︎ 修改權。同一件事畫面上一條條改要修改權限,走匯入只要建立權限——兩個入口、兩套門 |
| 元件端點位置 | 主專案 api/module_frame/__init__.py:POST /api/1.0/module-frame/import(ModuleFrameImportDataRoute,routes/module_frame_import_route.py)→ app/module_frame/service/module_frame_import_service.py 的 save_import_module_frame_data |
| 駭客怎麼打 | ① 只有建立權的人(被刻意不給修改權);② 準備一份整批匯入檔,帶上某份既有範本的編號;③ 系統只查建立權就走覆蓋;④ 既有範本被整份改掉 |
| 得手什麼 | 繞過「不給修改權」的設定,改掉既有範本 |
| 修正的做法 | 決策者裁定只有建立權不能覆蓋既有範本:save_import_module_frame_data 帶既有範本編號時另查 module-frame.update 能力點,錯誤碼沿用既有的 GRC_CAPABILITY_REQUIRED;同一入口也走歸屬檢查 |
| 狀態 | ✅ 已修,1.21.0 出貨(FR-114 CM-2178,主專案 commit abf1cf8a4)。驗證:只有建立權帶編號 403;租戶管理員對子公司範本 403 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟡 中(總表 #202;OWASP:A06 不安全的設計、A02 安全設定錯誤;主分類為權限提升) |
| 在哪裡 | 授權管理模組:判斷「這家客戶買了哪些模組、授權過期沒、子公司數有沒有超過」的總開關 |
| 攻擊面位置 | 客戶自己的主機管理員。落地版裝在客戶機房,主機權限在客戶手上,設定檔他改得到;設計上這一面就是暴露的 |
| 信任邊界位置 | 客戶主機 ↔︎ 原廠的商務規則。授權執法全靠兩個環境變數(LICENSE_ENFORCEMENT_ENABLED、LICENSE_READONLY_GATE_ENABLED),設成 false 就整套放行;防竄改只核對程式檔、不核對設定值,改了不會觸發任何警示。商務規則的開關不該放在被管的人手上 |
| 元件端點位置 | 主專案 common/authz/license.py 的 enforcement_enabled()/readonly_gate_enabled()(所有模組守門與唯讀攔截都經這兩支);設定讀取 config/config.py;唯讀攔截 common/middleware/license_readonly_mw.py;安裝程式 scripts/installer/install.sh;診斷包 app/support/service/diag_bundle_app_service.py |
| 駭客怎麼打 | ① 客戶的主機管理員打開系統設定檔;② 把授權執法開關改成 false,重啟服務;③ 沒買的模組全部開放、過期照常寫資料、子公司數不設限;④ 原廠端看不到任何警示 |
| 得手什麼 | 不付錢使用沒買的模組、授權過期照寫,受損的是原廠商務收入 |
| 修正的做法 | 決策者裁定落地版拿掉這兩個開關:① enforcement_enabled()/readonly_gate_enabled() 在 DEPLOYMENT_MODE=host 時一律回「要執法」、忽略設定;雲端版(原廠自己管機器)保留當保險絲;② 新增 disabled_switches_ignored_on_host(),開機時若發現落地版被設成 false 就記警告,診斷包也回報「曾嘗試關閉」留痕給原廠;③ 安裝程式不再寫這兩行;④ 真要暫時放行,改由原廠補發測試用授權;⑤ 新增 14 條測試,突變拿掉落地版判斷即轉紅 |
| 狀態 | ✅ 已修,1.21.0 出貨(CM-2205,主專案 commit c8aa7d026)。驗證:開發環境以落地版+開關 false 起服務,開機兩筆警告、沒買的模組回 403;雲端版+false 照舊放行 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | 🟡 中(總表 #210;OWASP:A01 存取控制失效;主分類為權限提升,另涉竄改資料) |
| 在哪裡 | 主系統的維運指令:客戶主機上用最高權限跑的「匯出診斷包」,會在容器與主機共用的目錄建一個交換資料夾,讓容器把預先收集的紀錄放進來 |
| 攻擊面位置 | 容器裡只有一般權限的人(例如取得應用程式執行身分的人),能寫那個共用目錄 |
| 信任邊界位置 | 容器一般權限 ↔︎ 主機最高權限。主機那一側用猜得到的名字(.diag-stage-<程序編號>)建交換資料夾、也不檢查是不是捷徑;最高權限帳號後續照路徑寫檔,等於讓一般權限的人決定最高權限要寫到哪裡 |
| 元件端點位置 | 主專案 scripts/installer/guidantai 的診斷段(mktemp -d、_diag_stage_cleanup);容器端讀檔 infra/support/collector/staged_collector.py 的 _read_staged;匯出 API GET /api/1.0/support/diagnostic-bundle(DiagnosticBundleRoute) |
| 駭客怎麼打 | ① 容器裡的一般權限者預先在共用目錄放一個捷徑,名字取成下次會用到的交換資料夾名;② 維運人員在主機上以最高權限執行「匯出診斷包」;③ 指令照路徑把設定副本、紀錄寫進「交換資料夾」——其實順著捷徑寫到攻擊者指定的地方;④ 等於借最高權限改主機上的檔案(執行者已在自己機器上重現) |
| 得手什麼 | 借主機最高權限改寫主機上的任意檔案 |
| 修正的做法 | ① 交換資料夾改用 mktemp -d 隨機名;建立前確認上層存在且不是捷徑;給容器寫的子目錄從 777 改成指定擁有者+700;找產出檔時只收一般檔、不收捷徑;② 容器端 _read_staged 讀檔前解析真實路徑,必須直接落在交換資料夾內,../、絕對路徑、指向外面的捷徑一律拒讀;③ 驗收退回再補:上層目錄可被一般權限改名,對方能把隨機名資料夾改名再換成捷徑——改成建好就 cd 進去、之後全用相對路徑寫檔,交給容器前確認原路徑仍指向同一個資料夾,清理只在資料夾編號相符時才刪,不再用完整路徑 rm -rf |
| 狀態 | ✅ 已修,1.21.0 出貨(8-D,CM-2214,主專案 commit d39181f3b/046553756)。打折:主機端只在本機模擬攻擊驗過,真機實跑等出包後在 190 驗 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | ⚪ 低(總表 #110;OWASP:A10 例外狀況處理不當、A01 存取控制失效;主分類為權限提升) |
| 在哪裡 | 任務與成員管理模組:修改某個任務的被指派人 |
| 攻擊面位置 | 已登入使用者可打的任務指派 API。今天打不穿:後面還有一道「這筆指派是否已存在」的檢查頂著,實際結果是「查無資料」而不是「未經授權的寫入」 |
| 信任邊界位置 | 使用者 ↔︎ 任務指派服務。「是不是這個專案的管理者」應該在任何寫入前無條件檢查;當時寫成「撈得到既有紀錄才檢查」,撈不到時整段檢查被跳過、繼續往下走,等於把守門交給後面那道剛好擋得住的檢查 |
| 元件端點位置 | jedi-task-platform/jedi_task_platform/participant/api/routing.py:PUT /task-assignee(TaskAssigneeRoute.put)→ participant/app/service/task_assignee_service.py 的 update_task_assignee() |
| 駭客怎麼打 | ① 任一登入帳號;② 送一個「更新任務指派」的請求,指向一筆根本不存在的指派;③ 程式撈不到既有紀錄,管理者檢查整段跳過;④ 今天靠後面那道檢查回「找不到」;但哪天有人把那支改成「查不到就當新增」,這個請求就會直接寫進去,而且沒有任何守門 |
| 得手什麼 | 今天什麼都得不到;是一道會靜默消失的把關 |
| 修正的做法 | ① 撈不到既有紀錄就明確拒絕(回「找不到」,錯誤碼 GRC_TASK_ASSIGNEE_NOT_FOUND),不再跳過守門往下走;② 形狀照抄同一支檔案「刪除指派」已驗證過的寫法;③ 程式旁留警語「撈不到就拒絕、不可跳過守門」,防日後改成新增時把把關一起拿掉 |
| 狀態 | ✅ 已修(FR-114.1-3/CM-2023,套件 commit 50c6e0f0,1.21.0 出貨)。驗證:實跑「更新撈不到被擋、正常更新放行」 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | ⚪ 低(總表 #117;OWASP:A06 不安全的設計;主分類為權限提升) |
| 在哪裡 | 共用基礎:每次開資料庫交易時,告訴資料庫「這個人是誰、能看什麼」的那一段(所有功能共用) |
| 攻擊面位置 | 今天沒有:這個開關完全沒有效果。它的風險在讀程式的人——誤以為已經有一道部門層級的檢查,於是不去補真正需要的那道 |
| 信任邊界位置 | 使用者 ↔︎ 部門層級的資料隔離。開關名叫「能不能管部門」,每次有身分時都無條件設定,但資料庫 259 條隔離規則沒有一條讀它;真正被讀的是旁邊那個「有條件才設」的手足開關。看起來有守,實際上這條線上沒有守衛 |
| 元件端點位置 | 套件 jedi-common jedi_common/session/database/db.py 的 session_scope():SET LOCAL app.can_manage_orgs 三行(已刪);對照的活手足 set_can_read_all_orgs() 被出貨基線 scripts/init/02-schema.sql 5 條規則讀取 |
| 駭客怎麼打 | ① 今天什麼都打不到;② 風險在之後:有人要做部門層級的權限,看到這個開關以為已經有了;③ 沒去補真正的檢查;④ 部門之間的資料就一直沒有隔開,而大家以為有 |
| 得手什麼 | 今天什麼都得不到;是一個讓人誤判「已經有防護」的假象 |
| 修正的做法 | 拆除了什麼:① session_scope() 裡設定這個開關的三行刪除,旁邊的使用者編號與超級管理員設定不動;② 四份提到它的說明文件同步改掉,避免文件描述不存在的東西;③ 刪除前查證:21 支套件、主專案、前端、測試、代理程式、授權中心、開發環境隔離規則、資料庫自寫程式、出貨基線、全部資料庫異動腳本皆零命中 |
| 狀態 | 🗑️ 裁定刪除,已刪除,1.21.0 出貨(FR-114.3-3,CM-2045,套件 commit 599ab7c5;文件同步主專案 2b4e172b3)。驗證:session 相關 41 條測試通過 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | ⚪ 低(總表 #118,與 M06-16 同一件;OWASP:A01 存取控制失效;主分類為竄改資料,另涉資料外洩) |
| 在哪裡 | 稽核流程模組(套件):流程圖產生器裡「存流程圖到檔案」「匯出 XML」「從 XML 匯入」三支,現在完全沒人呼叫 |
| 攻擊面位置 | 今天沒有——全系統零呼叫。現在無害的唯一理由是沒人用它 |
| 信任邊界位置 | 呼叫者給的路徑 ↔︎ 主機檔案系統。三支都直接拿傳入的路徑開檔讀寫,沒有任何限制;只要哪天有人把使用者輸入接到參數上,就變成任意檔案讀寫 |
| 元件端點位置 | 套件 jedi_flow_engine/common/utils/bpmn_generator.py 的 save_bpmn_to_file、export_xml、import_xml(已刪);同檔保留的 export_xml_string/import_xml_string 不碰檔案 |
| 駭客怎麼打 | ① 今天打不到;② 若有人日後把某個 API 的參數接到這三支;③ 送一個指向系統設定檔的路徑;④ 讀走或覆寫主機上的任意檔案 |
| 得手什麼 | (若被接上)主機上任意檔案的讀寫 |
| 修正的做法 | 拆除了什麼:三支死碼整支刪除,連帶移除只給匯入那支用的 os 引入;有人在用的「字串版」匯出匯入保留。刪除前查證全 repo 零呼叫 |
| 狀態 | 🗑️ 裁定刪除,已刪除,1.21.0 出貨(隨 CM-2059,套件 jedi-flow-engine commit 4a416ece) |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | ⚪ 低(總表 #118,與 M06-14 同一件;OWASP:A01 存取控制失效;主分類為竄改資料,另涉資料外洩) |
| 在哪裡 | 稽核流程模組:套件提供的「任務新增刪改」服務,主系統組裝好放在那裡備用,整個程式庫沒有任何地方呼叫它 |
| 攻擊面位置 | 今天沒有。與 M06-14 同一種:只要哪天有人接上去,就憑空多出一組沒有檢查的入口 |
| 信任邊界位置 | 呼叫者 ↔︎ 任務資料。套件本身零守門是設計(套件不知道誰有權限,那是用它的人要決定的),防線只能在主系統這層補——而這組服務沒經過任何主系統的守門 |
| 元件端點位置 | 主專案 di_containers/flow_engine/workflow_excution_containers.py 註冊的 job_execution_service(jedi_flow_engine.app.service.job_execution_service.JobExecutionService,已拆) |
| 駭客怎麼打 | ① 今天打不到;② 若日後有人在路由上直接注入這支服務;③ 任何登入者就能新增、修改、刪除任意任務執行資料,沒有歸屬也沒有權限檢查 |
| 得手什麼 | (若被接上)任意改寫任務執行資料 |
| 修正的做法 | 拆除了什麼:拆掉沒人注入的 job_execution_service 註冊與其引入(全庫只剩容器自己引用);它底下的 domain service 仍被主系統的流程服務用到,保留 |
| 狀態 | 🗑️ 已拆除,1.21.0 出貨(8-E,CM-2215,主專案 commit 822216b6c)。驗證:後端起得來、各端點正常 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | ⚪ 低(總表 #120;OWASP:A01 存取控制失效;主分類為權限提升) |
| 在哪裡 | 任務與成員管理模組:任務指派服務裡幾支查詢與配套功能。模組頁沒有逐條表,此條依總表描述與拆除 commit 寫成 |
| 攻擊面位置 | 今天沒有:沒有任何路由、前端、DI 容器呼叫。與 M10-7、M10-8 同一種——只要哪天有人接上去,就多一組沒有把關的入口 |
| 信任邊界位置 | 呼叫者 ↔︎ 任務指派資料。這幾支直接回某專案、某使用者的指派清單與待辦佇列,不問呼叫者是誰;守門只能由接上它的那一層補,而它們從沒被接過——其中一支是被主專案新版取代後留下的舊版,不是「還沒接」 |
| 元件端點位置 | 套件 jedi_task_platform/participant/app/service/task_assignee_service.py 的 get_task_assignee_menu()、get_task_assignees_with_inherits()、get_user_task_queue()(舊版),連同下游 TaskAssigneeDomainService.get_user_task_queue()、ITaskAssigneeRepo.get_user_task_queue()、TaskAssigneeRepoImpl.get_user_task_queue()(皆已刪);新版是主專案 infra/readmodel/tasks/my_grc_jobs_query.py 的 get_user_task_queue_by_sp() |
| 駭客怎麼打 | ① 今天打不到;② 若日後有人把這幾支接上 API;③ 任何登入者帶別人的專案或使用者編號呼叫;④ 拿到那個專案的指派清單或那個人的待辦佇列 |
| 得手什麼 | (若被接上)別人專案的任務指派與別人的待辦清單 |
| 修正的做法 | 拆除了什麼:① 三支零呼叫的查詢整支刪除,連同下游整條孤兒鏈;② 留下第四支「依專案刪掉所有指派」delete_by_project_id()——六層成員表都有同名方法,專案刪除時逐層清理要用到;③ 前端快速配置在用的批次指派與四支活路由方法原樣保留 |
| 狀態 | 🗑️ 裁定刪除,已刪除,1.21.0 出貨(CM-2044,套件 commit 9c9029fd) |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | ⚪ 低(總表 #122,與 M17-6 同一件;OWASP:A07 身分驗證失效、A04 加密機制失效、A02 安全設定錯誤;主分類為冒充身分) |
| 在哪裡 | 通知模組檢視時查到:同一個測試設定檔 .env.test 裡還有三樣——簽發登入憑證的金鑰、資料庫密碼、檔案儲存服務金鑰 |
| 攻擊面位置 | 拿得到程式庫的人,而且只影響我們自己的開發環境:安裝程式每裝一套都會各自隨機產生新的一份,這三樣不會跟著出貨 |
| 信任邊界位置 | 版控 ↔︎ 祕密存放處。登入憑證的簽章金鑰是「誰簽的憑證系統就信」的那一把,它一跨進版控,拿到的人就能自己簽任何人的身分 |
| 元件端點位置 | 外流檔案 .env.test(已撤出版控,commit 5746cef10);金鑰用途 JWT_SECRET_KEY(登入憑證簽章);安裝時隨機產生在 scripts/installer/install.sh(gen_key) |
| 駭客怎麼打 | ① 拿到程式庫,打開測試設定檔;② 用簽章金鑰自己簽一張「我是管理員」的登入憑證;③ 帶著它打開發環境的任何 API,不需要密碼也不需要雙因子;④ 資料庫密碼與儲存金鑰則可直接讀寫開發環境資料 |
| 得手什麼 | 開發環境的任意身分、資料庫與證據檔讀寫權 |
| 修正的做法 | ① 測試設定檔撤出版控、移除 .gitignore 例外(commit 5746cef10);② 換發:模組頁記三樣都已撤銷換發,但 CM-2048 commit 註明簽章金鑰屬開發機專用、安裝時每套重新產生、不需換發(同 M17-6 的裁定);③ 版控殘留字串清除 |
| 狀態 | ✅ 已修正(CM-2048,commit e8e134a4e;撤出版控 5746cef10) |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | ⚪ 低(總表 #122,與 M16-6 同一件;OWASP:A07 身分驗證失效、A04 加密機制失效、A02 安全設定錯誤;主分類為冒充身分) |
| 在哪裡 | 公告模組檢視時查到:簽發登入憑證用的金鑰明文出現在版控的對話紀錄裡(34 處) |
| 攻擊面位置 | 拿得到程式庫的人;影響範圍只有我們的開發環境,因為安裝程式每套各自隨機產生一把新的 |
| 信任邊界位置 | 版控 ↔︎ 祕密存放處。同 M16-6。這條也是決策者定下判嚴重度原則的案例:先問「客戶端用的是不是同一把」,不是先問「這把還活著嗎」或「散在幾個檔」 |
| 元件端點位置 | 外流位置:docs/conversation-history/ 下 30 個檔;金鑰用途 JWT_SECRET_KEY;安裝時隨機產生 scripts/installer/install.sh(gen_key) |
| 駭客怎麼打 | ① 拿到程式庫,在對話紀錄裡找到簽章金鑰;② 自己簽任何人、任何客戶、任何角色的登入憑證;③ 拿去打開發環境 |
| 得手什麼 | 開發環境裡任意身分 |
| 修正的做法 | ① 30 個對話紀錄檔清除金鑰字串,純字串清理、無程式改動;② 決策者裁定開發環境那把不需更換(每套安裝各自隨機產生,不會出貨);③ 同批另清出 47 檔已過期的登入憑證字串(commit 8497acee3) |
| 狀態 | ✅ 已修正(CM-2048,commit e8e134a4e) |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | ⚪ 低(總表 #141;OWASP:A05 注入攻擊;主分類為權限提升) |
| 在哪裡 | 共用基礎:每個資料庫交易開始時,把「這個人是誰、能看哪些客戶與部門」寫進資料庫的隔離設定 |
| 攻擊面位置 | 目前沒有外部可控的輸入走到這裡。三個值(使用者編號、客戶路徑、部門路徑)都由系統自己產生,部門路徑根本沒人填 |
| 信任邊界位置 | 我們的程式 ↔︎ 資料庫的隔離規則。安全完全依賴「沒有人把外部輸入帶進這條路」這個當下的事實;哪天有人加一條新路徑把外部輸入帶進來,漏洞就成立,而且加的人不會知道 |
| 元件端點位置 | jedi-common jedi_common/session/database/db.py 的 session_scope():SET LOCAL app.user_id/app.allowed_tenant_paths/app.allowed_org_paths 三句 f-string;值來自 jedi_iam/middleware/context.py(登入身分的客戶路徑)與 jedi_common/session/auth/tenant_context.py(機器情境) |
| 駭客怎麼打 | 現況打不進來。成立條件是:① 未來某條新路徑讓外部輸入影響客戶或部門路徑的值;② 攻擊者在那個值裡放一個單引號與額外語句;③ 隔離設定被改寫,例如把可見範圍改成全部客戶 |
| 得手什麼 | 現況無;條件成立時是跨客戶讀寫 |
| 修正的做法 | 不修的理由:三個值目前都是乾淨的,部門路徑沒人填。而且這不是順手能改的——這個資料庫指令不支援把語句和值分開傳,真正的改法是改呼叫資料庫內建的設定函式,並確認隔離規則仍讀得到值,這正是當初這段改很久的原因 |
| 狀態 | 🚫 裁定不修(無工單) |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | ⚪ 低(總表 #186;OWASP:A01 存取控制失效;主分類為權限提升) |
| 在哪裡 | 問卷模組:任務問卷的「填答」那一半——列任務問卷、讀寫答案、看與還原歷史版本、匯入答案;另有兩個入口同病:AI 儀表板查問卷、規劃頁把問卷掛上任務 |
| 攻擊面位置 | 沒買問卷模組的客戶的任何登入帳號。選單會反灰,但網址本身打得到 |
| 信任邊界位置 | 客戶 ↔︎ 原廠的商務授權。「設計問卷」那半 33 個入口有 32 個問了「這家有沒有買問卷」,「填答」那半 14 個一個都沒問。每個入口仍有問卷歸屬檢查,所以看不到別家的東西——這是商務授權被繞過,不是資料外洩 |
| 元件端點位置 | 套件 jedi-survey api/routing.py 的 mount_answer_routes():POST /api/1.0/task-surveys、GET/PUT /task-survey/{uid}、/task-survey/import-template/{task_uid}、/task-survey/import-answers、/task-survey/configured/{uid}、GET/PUT /project-survey/answers/{uid}、…/patch、…/checkpoint、/project-survey/histories、/histories/menu/{uid}、/history-details、/history/revert;規劃頁建任務 POST /api/1.0/grc/project/{project_uid}/assessment-object/{ao_uid}/jobs、改任務 PUT /api/1.0/grc/project/{project_uid}/job/{job_uid}(app/flow_control/service/job_service.py);AI 儀表板查問卷 di_containers/dashboard_apis/survey.py |
| 駭客怎麼打 | ① 沒買問卷的客戶任一帳號登入;② 照有買的客戶畫面上的網址,直接送列任務問卷、填答、匯入答案的請求;③ 系統只驗登入與問卷歸屬就放行;④ 或在規劃頁建任務時把任務類型填成「問卷」,或透過 AI 儀表板查問卷——沒買的功能照用 |
| 得手什麼 | 不付錢使用加購的問卷填答功能,受損的是原廠商務收入 |
| 修正的做法 | 三個入口分三刀:① 填答那半:14 支方法各在「必須登入」下一行加 @require_license("survey"),只加裝飾器不動內容;② 規劃頁建/改任務:job_service 新增 _assert_job_type_licensed(),任務類型必須在這家客戶已買的範圍內,清單與匯入那條共用 licensed_job_types();③ AI 儀表板查問卷:套件讓每支查詢能申報 required_license,問卷兩支申報 survey,主專案注入 viewer_module_licensed 檢查,沒買就拒絕 |
| 狀態 | ✅ 已修,1.21.0 出貨(CM-2194:套件 commit 0ced8e15、主專案 02bc1179c;AI 儀表板入口 CM-2220:主專案 80dedfbb5、套件 fb271590)。驗證:以替身拿掉授權快照裡的問卷,14 支全 403 LICENSE_403001 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | ⚪ 低(總表 #213;OWASP:A06 不安全的設計;主分類為權限提升) |
| 在哪裡 | 授權管理模組:授權到期後產品轉唯讀 |
| 攻擊面位置 | 授權已過期的客戶的任何登入帳號,在排程下一次跑完之前;若部署方式讓排程沒在跑,就一直有效 |
| 信任邊界位置 | 時間 ↔︎ 寫入權限。「過期即唯讀」應該在每次判斷寫入權限時當場算;當時讀的是資料庫裡存的狀態,而那個狀態要等每天一次的排程(台灣時間早上 10 點)才更新 |
| 元件端點位置 | 主專案 common/authz/license.py 的 LicenseSnapshot 與 viewer_is_readonly();唯讀攔截 common/middleware/license_readonly_mw.py;排程 core/scheduler.py 的 _license_expiry_state_machine_tick();到期判斷純函式在套件 jedi-license-runtime domain/service/license_expiry_state_machine.py 的 compute_target_status() |
| 駭客怎麼打 | ① 客戶的授權在某天午夜過期;② 排程還沒跑,資料庫裡的狀態仍是「正常」;③ 客戶照常新增、修改資料,最長將近一天;④ 若排程沒在跑,就一直可寫 |
| 得手什麼 | 授權過期後繼續寫入資料 |
| 修正的做法 | 決策者裁定「過期即唯讀」:① LicenseSnapshot 多帶到期日與寬限規則,新增 effective_status(),用排程同一支 compute_target_status() 當場按時間重算;② 與資料庫存的狀態取較嚴的那個——存的已是唯讀就仍唯讀,不因重算放寬;③ viewer_is_readonly() 改看它;④ 排程照留,負責把狀態寫進資料庫給清單、報表與寄通知用 |
| 狀態 | ✅ 已修,1.21.0 出貨(8-G,CM-2217,主專案 commit 3ed47c7a9)。驗證:新增「剛過期 5 分鐘、資料庫仍正常 → 唯讀」等 3 條測試,突變轉紅;開發環境以實際租戶授權把時間推到到期後驗(未改授權資料,屬打折) |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | ⚪ 低(總表 #216;OWASP:A01 存取控制失效;主分類為權限提升) |
| 在哪裡 | 授權管理模組:過期轉唯讀後,全域擋寫入的那道攔截 |
| 攻擊面位置 | 授權已過期的客戶:管理員還能產生新的代理程式註冊碼;任何能填問卷的人還能透過即時填答通道寫入答案 |
| 信任邊界位置 | 唯讀攔截 ↔︎ 兩個例外通道。代理程式回報結果是機器對機器,該放行;但當時把整段 /agents/ 都放行,連管理員產註冊碼也一起放掉。問卷即時填答走另一種連線(SocketIO),本來就不經過網頁請求那道攔截,套件裡也沒有自己的唯讀檢查 |
| 元件端點位置 | 主專案 common/middleware/license_readonly_mw.py 的 _EXEMPT_PREFIXES/_should_block();被誤放行的是 jedi-remote-agent POST /api/1.0/agents/enroll-token、/agents/enroll-token/revoke;問卷即時通道是套件 jedi-survey app/handler/fill_survey_socketio_handler.py(命名空間 /socket/fill-survey)的 on_update,主專案接線在 core/plugins/survey.py |
| 駭客怎麼打 | ① 客戶授權過期,產品轉唯讀;② 管理員照常按「產生註冊碼」,因為整段代理程式網址被放行;③ 填問卷的人打開問卷,透過即時共編通道送答案更新,不經過唯讀攔截就寫進資料庫 |
| 得手什麼 | 唯讀期間仍能註冊新代理程式、寫入問卷答案 |
| 修正的做法 | ① 放行範圍收窄成代理程式自己打的三段(/agents/register、/agents/heartbeat、/agents/tasks/),產生與撤銷註冊碼不再放行;② 套件 jedi-survey 新增選填接口 readonly_check,on_update 在寫入前問它,唯讀就不寫、只回一個錯誤事件(LICENSE_403002,與網頁唯讀攔截同碼);只做共編狀態廣播、不寫資料庫的事件不擋;③ 主專案把 viewer_is_readonly 接到這個接口 |
| 狀態 | ✅ 已修,1.21.0 出貨(8-G,CM-2217,主專案 commit 3ed47c7a9、套件 8ccfac73)。驗證:產/撤註冊碼唯讀下被擋,代理程式三段照放;問卷唯讀擋、可寫放行,突變轉紅 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | ⚪ 低(總表 #218;OWASP:A01 存取控制失效;主分類為權限提升) |
| 在哪裡 | 主系統的雲端硬碟整合:系統設定 → 雲端硬碟 → 「驗證應用程式憑證」按鈕 |
| 攻擊面位置 | 平台管理員群組裡、沒被分到「儲存設定」權限的人。門檻是平台管理員身分 |
| 信任邊界位置 | 平台管理員 ↔︎ 「儲存設定」這項功能權限。設定頁本身要求儲存設定權限;驗證按鈕只問「是不是平台管理員」,沒問這一項,兩個入口兩套門 |
| 元件端點位置 | 主專案 api/cloud_integration/__init__.py:POST /api/1.0/integrations/google-drive/verify-credentials(DriveAppCredentialVerifyRoute)→ app/cloud_integration/service/drive_app_credential_verify_service.py 的 verify() |
| 駭客怎麼打 | ① 平台管理員群組裡一個沒被分到儲存設定權限的人;② 直接送驗證請求,帶上任意一組 Google 應用程式憑證;③ 系統只確認他是平台管理員,就拿去向 Google 驗證並回報對錯;④ 他可以拿這支功能反覆試憑證 |
| 得手什麼 | 不該碰儲存設定的人能用系統替他試憑證對錯 |
| 修正的做法 | ① verify() 在平台管理員檢查之後再疊一道 viewer_has_capability("storage-config.read"),與設定頁同一顆權限;② 沒有就回 403 GRC_403022,擋在打 Google 之前;③ 新增「平台管理員但沒有這項權限 → 403 且不打 Google」測試,突變拿掉檢查即轉紅 |
| 狀態 | ✅ 已修,1.21.0 出貨(8-A,CM-2211,主專案 commit fe928859f)。驗證:租戶管理員 403、持權限的平台管理員 200 照常 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | ⚪ 低(總表 #222;OWASP:A05 注入攻擊;主分類為權限提升) |
| 在哪裡 | 稽核流程模組:稽核計畫底下的任務清單匯出成 Excel,改完再匯回 |
| 攻擊面位置 | 能編輯任務欄位(名稱、說明、負責人等)的專案成員 |
| 信任邊界位置 | 我們的資料 ↔︎ 下載者的電腦。十個欄位原樣寫進儲存格,= + - @ 開頭就被當公式;應該在寫出時標純文字 |
| 元件端點位置 | jedi-task-platform task/api/routing.py:GET /project/{project_uid}/ap/{ap_uid}/jobs/export(JobExportRoute)→ 主專案 app/flow_control/service/job_import_service.py 的 export_excel() |
| 駭客怎麼打 | ① 專案成員在任務欄位填一段公式;② 有人匯出任務 Excel 打開;③ 按下「啟用內容」,那段指令在他自己的電腦上執行 |
| 得手什麼 | 在下載者電腦上執行內容 |
| 修正的做法 | ① 十欄改走 jedi-common 的 set_text_cell()(沿用 CM-2189 那支,不另寫);② 這份檔會被匯回,用「強制文字」旗標而不加單引號,值不變;③ 欄位順序抽成 _EXPORT_COLUMNS,與匯入依位置讀取對齊 |
| 狀態 | ✅ 已修,1.21.0 出貨(8-C,CM-2213,主專案 commit a5804a990/bde8f9d16)。驗證:DEV 真實專案匯出 60 列 0 個公式格 |
| 欄位 | 內容 |
|---|---|
| 嚴重度 | ⚪ 低(總表 #234;OWASP:A01 存取控制失效;主分類為權限提升) |
| 在哪裡 | AI 儀表板模組:「生成儀表板」功能 |
| 攻擊面位置 | 已買 AI 儀表板模組的客戶裡,被客戶管理員刻意沒開這項權限的角色(例如稽核人員)。選單上看不到,網址照樣打得到 |
| 信任邊界位置 | 使用者 ↔︎ 「AI 儀表板」這項功能權限。生成端點只問「這家有沒有買」,沒問「這個人的角色有沒有這項權限」——客戶的權限設定在這裡形同虛設,而且每用一次要付兩次外部 AI 費用,由原廠吸收 |
| 元件端點位置 | 套件 jedi-ai-dashboard:POST /api/1.0/ai-dashboard/auto-generate(api/routes/ai_dashboard_route.py,AUTO_GENERATE_URL)、守門 api/guards.py 的 require_capability()、接線契約 plugin/contract.py;主專案接線 core/plugins/ai_dashboard.py |
| 駭客怎麼打 | ① 一個角色沒有「AI 儀表板」權限的帳號(例如稽核人員)登入;② 選單上看不到這個功能,但直接送生成請求;③ 系統只確認這家有買就開始生成,連打兩次外部 AI;④ 反覆送,費用由原廠吸收(已實測) |
| 得手什麼 | 繞過客戶的角色權限設定使用 AI 儀表板,並讓原廠持續付 AI 費用 |
| 修正的做法 | ① 套件新增接線欄位 capability_required,列為必填——主系統不接就拒絕掛載,不會有人忘了接;② 生成端點在「有沒有買」之後疊 @require_capability("ai-dashboard.read"),權限名沿用既有那一顆、不另造;③ 主專案把 common.authz.require_capability 接上;④ 突變拿掉路由上的權限檢查即轉紅 |
| 狀態 | ✅ 已修,1.21.0 出貨(8-J,CM-2220,主專案 commit 80dedfbb5、套件 fb271590)。驗證:沒有權限的稽核人員帳號 403 GRC_403022,有權限的帳號過守門(本機無 AI 金鑰,未走到真正生成,屬打折) |