檢查日期 2026-09-23|耗時 1 小時 27 分|對應卡片 CM-2080|範圍 8 檔/925 行|掃的版本
f80b9f682
掃描工具找到 3 個問題(三個檢查員全數確認),其中新的只有 2 個,都屬於「讓伺服器忙到當掉」這一類;我自己開檔另外查出 2 個,最嚴重的是:「整批匯入」可以直接覆蓋一份已經存在的範本,但系統只檢查了「你能不能建新範本」。
另外兩件卡片特別要求單獨回答的事:
yaml.safe_load),檔案內容只會被當成資料,不會被當成程式執行。但它沒有限制檔案的巢狀層數,一個幾 KB 的檔案就能讓伺服器忙到當掉(見 B1c-2)。合規範本是稽核流程的骨架(例如「ISO 27001 要查哪些條款、每條要做哪些事」)。除了在畫面上一條一條建,合規資源庫頁面(ModuleFrame.vue)還有「匯入」按鈕,讓使用者上傳一個檔案,一次建出整份範本連同底下所有條款。
匯入有兩條路:
| 入口 | 畫面上的操作 | 檔案格式 |
|---|---|---|
POST /module-frame/import/yaml |
上傳 YAML 檔(一種純文字的設定檔格式) | YAML |
POST /module-frame/import/verify → POST /module-frame/import |
上傳 Excel,先預覽檢核、確認後存檔 | 前端把 Excel 轉成 JSON 送過來 |
三個入口都只掛了一道檢查:module-frame.create(「你能不能建新範本」),出廠預設只有「管理員」角色有。
這一棒要回答:匯入進來的東西,有沒有跟在畫面上一條一條建時經過一樣的檢查。
現況(2026-10-01):本棒各條後來的處理結果如下(過程紀錄保留,不改)。
- B1c-1=M11 第 5 條,✅ 已修(CM-2178,1.21.0 出貨)。
- B1c-2(YAML 巢狀引用)=M11 第 34 條,✅ 已修(CM-2190,commit
374392065,1.21.0 出貨)。- B1c-3(Excel 檢核比對拖慢)=M11 第 35 條,✅ 已修(CM-2181,commit
48638cbdb,1.21.0 出貨)。- B1c-4(建立權限即可覆蓋既有範本)=M11 第 10 條,✅ 已修(CM-2178,commit
abf1cf8a4,1.21.0 出貨)。- B1c-5(YAML 匯入建不出東西,資訊性):無修正卡,查不到後續處理。
| # | 嚴重度 | 這是什麼問題 | 出事會怎樣 | 要先有什麼才打得到 | 在哪裡(該補檢查的位置) | 來源 |
|---|---|---|---|---|---|---|
| B1c-1 | 🟡 中 | 修改範本時,只有改「分享範圍」才檢查範本是不是你家的,改「適用控制項清單」完全不檢查 | 子公司管理員可以把母公司分享出來的範本的控制項清單清空,順帶刪掉範本裡的實作說明 | 自己公司的管理員+一份母公司分享或原廠內建的範本 | app/module_frame/service/module_frame_service.py:104(update_module_frame 開頭) |
工具,三票確認。與 B1 第 1 條同一個問題,不另開卡 |
| B1c-2 | 🟢 低 | 上傳的 YAML 檔沒有限制「一層包一層」的層數,而 YAML 允許「這裡引用上面那段」 | 一個幾 KB 的檔案可以讓伺服器建出天文數字個物件,那條處理程序卡死或記憶體用盡,其他公司的使用者同時受影響 | 有 module-frame.create 權限的帳號(預設只有管理員) |
infra/module_frame/adapter/yaml_to_module_frame_parser_adapter.py:14(parse 開頭) |
工具,三票確認 |
| B1c-3 | 🟢 低 | Excel 匯入檢核時,用來檢查「有沒有夾帶網頁標籤」的比對寫法,遇到特定內容會越跑越慢,而且欄位沒有長度上限 | 送一筆一百萬個 < 的條款名稱,就能讓一條處理程序忙兩分鐘,幾個請求同時送就把伺服器全部卡住 |
同上 | app/module_frame/service/module_frame_import_service.py:52(verify_import_module_frame_data 開頭) |
工具,三票確認 |
| B1c-4 | 🟡 中(當時待裁;已裁不能覆蓋、已修 CM-2178) | Excel 整批匯入可以覆蓋既有範本,但入口只檢查「你能不能建新範本」,也沒檢查那份範本是不是你家的 | 只有「建立」權限的人可以改掉既有範本的名稱、條款內容與子項 | 有 module-frame.create 權限的帳號+知道既有範本的編號 |
api/module_frame/routes/module_frame_import_route.py:45-47(ModuleFrameImportDataRoute.post 的裝飾器);app/module_frame/service/module_frame_import_service.py:124(save_import_module_frame_data 開頭) |
⚠️ 我自己開檔查出,未經工具投票 |
| B1c-5 | ⚪ 資訊 | YAML 匯入這條路第一步就會出錯,實際上建不出任何範本 | 目前不是安全問題;但哪天有人把那支空殼方法補回來,這條路會變成完全沒守的批次寫入 | — | app/module_frame/service/module_frame_service.py:98(add_module_frame) |
⚠️ 我自己開檔查出,未經工具投票 |
在哪個畫面、誰、做什麼:合規資源庫頁面,子公司的管理員打開一份「母公司分享給旗下子公司」或「原廠內建」的範本,改它的適用控制項清單並存檔(或直接送 API)。
會發生什麼:update_module_frame 這支方法裡有兩段寫入。改「分享範圍」那段有呼叫 assert_scope_writable(檢查「這份範本是不是你家的」),改「適用控制項清單」那段沒有。後者會直接改 OSCAL 的 profile 表並刪除範本 SSP 裡被移除控制項的實作說明——這兩張表沒有開「每個客戶只能看自己資料」的資料庫隔離機制(RLS),所以資料庫那道最後防線也擋不住。
這條不是新的:B1 那一棒(scan-B1.md 第 1 條)已經找到同一個問題、同一個行號。本棒工具再次獨立找到並三票確認,等於多一次佐證,不另開修正卡。
怎麼修:照 B1 報告的修法——在 update_module_frame() 開頭、任何寫入之前,比對 get_user_context().tenant_id == mf.tenant_id 且範本不是原廠內建(SYSTEM),不符就擋下。
這是什麼問題:YAML 格式允許「在這裡貼上前面某一段的內容」(稱為錨點與引用)。解析器 _parse_workflow 是一層一層往下展開子流程的,而且沒有限制層數也沒有限制總數。攻擊者可以寫:第 1 層引用第 0 層 10 次、第 2 層引用第 1 層 10 次……疊 30 層,檔案只有幾 KB,但展開後是 10 的 30 次方個物件。
在哪個畫面、誰、做什麼:合規資源庫頁面的「匯入 YAML」,有建立範本權限的管理員上傳這個特製檔案。
出事會怎樣:處理這個請求的伺服器程序會吃滿一顆 CPU、記憶體一路漲,直到 120 秒逾時被砍或整個容器記憶體耗盡。預設只有 4 條處理程序,連送幾個請求,所有公司的使用者都會同時連不上系統。
為什麼是「低」不是「中」:要先有管理員帳號,而且後果是服務暫停、不是資料外洩或被竄改。
跟「會不會執行程式」的關係:這是兩件事。yaml.safe_load(:16)已經防住「上傳檔案在伺服器上執行程式」這個最嚴重的風險,這點我開檔確認過;B1c-2 是另一種攻擊——不執行程式,只是讓伺服器算不完。
在哪裡:
infra/module_frame/adapter/yaml_to_module_frame_parser_adapter.py:14 ← parse() 開頭,該在這裡擋
infra/module_frame/adapter/yaml_to_module_frame_parser_adapter.py:16 ← safe_load(安全,不用改)
infra/module_frame/adapter/yaml_to_module_frame_parser_adapter.py:35 ← 無上限的遞迴展開
怎麼修:在 parse() 開頭改用自訂的 SafeLoader,遇到引用(alias)就直接拒絕(範本檔沒有正當理由需要引用);另外在 _parse_workflow 加層數上限(例如 10 層)與總節點數上限(例如 5,000 個),超過就丟 BadRequestError。順帶:格式錯的檔案目前 data.get 會直接炸成 500,應一併改回「格式錯誤」訊息。
這是什麼問題:Excel 匯入時,系統會檢查每一條的「條款名稱」與「要求事項」有沒有夾帶網頁標籤,用的比對規則是 <[^>]+>(:23)。這個寫法遇到「很多個 < 但沒有 >」的字串時,每個 < 都會從頭掃到尾,工作量是字串長度的平方。而欄位沒有長度上限。
在哪個畫面、誰、做什麼:合規資源庫頁面的「匯入 Excel」→ 預覽檢核這一步,有建立範本權限的管理員送出一筆條款名稱是一百萬個 < 的資料。
出事會怎樣:一個請求就讓一條處理程序忙到 120 秒逾時,四個請求就把預設的四條全部卡住。後果跟 B1c-2 一樣是全系統暫停。
在哪裡:
app/module_frame/service/module_frame_import_service.py:52 ← verify_import_module_frame_data() 開頭,該在這裡先擋長度
app/module_frame/service/module_frame_import_service.py:23 ← 比對規則本身
怎麼修:在 verify_import_module_frame_data() 開頭,條款名稱與要求事項超過合理長度(例如 2,000 字)就直接判為不合格;比對規則改成有上限的寫法 <[^<>]{1,200}>。
這是什麼問題:Excel 匯入存檔的入口(/module-frame/import)只掛了 module-frame.create。但進到 save_import_module_frame_data() 後,只要送上來的資料帶了既有範本的編號(module_frame.uid),程式就走「覆蓋」這條路(:125-129):
update_module_frame 改範本主檔(名稱、版本等)——這支就是 B1c-1 那支,不改分享範圍就不檢查歸屬update_module_frame_item(:169)——這支就是 B1-item 第 3 條講的「檢查甲、動手改乙」、本身完全沒有權限檢查的那支同一件事在畫面上一條一條改,要有 module-frame.update;走匯入就只要 module-frame.create。這就是卡片預測的「上傳入口只問你能不能建,不問你能不能改既有的」。
出事會怎樣:只被授權「建新範本」、沒被授權「改範本」的人,可以用匯入改掉既有範本的名稱與每一條的內容。
可以打到多遠——同公司確定打得到,跨公司我判斷會被資料庫擋下:範本主表與流程主表有開資料庫隔離,「只准改自己公司的」。這條路每一步都會寫主表(至少會寫「最後修改人」),跨公司時,資料庫的隔離規則會讓主表那一筆「改了等於沒改」(不報錯、只是改到 0 筆),而程式的資料存取層(SQLAlchemy)發現「預期改 1 筆卻改了 0 筆」會丟錯,整批撤銷。這是我讀程式碼推的,沒有實際打過。
跟 B1c-1 的關係:B1c-1 修好之後,主檔那一步會擋下跨公司,但同公司內「只有建立權限就能改」這個落差仍在,所以要分開修。
修法先要決策者裁一件事:「只有建立權限的人能不能透過匯入覆蓋既有範本?」
ModuleFrameImportDataRoute.post 的裝飾器(module_frame_import_route.py:45-47),把 require_capability("module-frame.create") 改成依有沒有帶 uid 分流——或更簡單,在 save_import_module_frame_data() 開頭(:124)遇到 is_update 為真時,再呼叫一次 viewer_has_capability("module-frame.update"),沒有就擋。YAML 匯入第一步呼叫 module_frame_service.add_module_frame()(import_service.py:274),但那支方法目前是空殼,固定回 None(module_frame_service.py:98-101,註解寫「v2 切換後停用,2B 重建」)。下一行 module_frame_dto.template_uid 就會出錯,整個請求 500。
所以 YAML 這條路今天不會建出任何範本或子項——這也是為什麼它「沒有走子項服務」這件事目前不構成漏洞。
要留意的是未來:YAML 匯入後面的步驟是直接呼叫流程套件建範本、改範本(:321、:340、:371、:396),完全繞過子項服務,也沒有任何歸屬檢查。哪天有人把 add_module_frame 補回來,這條路就會變成一條沒守的批次寫入。建議在重建 add_module_frame 的那張卡上註明:YAML 匯入要改成走子項服務,不要直接呼叫流程套件。 B1c-2 的 YAML 解析問題則是今天就存在的——解析發生在建範本之前,不受這支空殼影響。
另外 Excel 匯入的「新建範本」分支(save_import_module_frame_data 的 else,:131)也呼叫同一支空殼,所以 Excel 匯入今天也只有「覆蓋既有範本」這條路能成功——剛好就是 B1c-4 那條。
答案:入口的權限點沒有把範圍限住,建議定為「中」。
module_frame_item_route.py:78-80)掛的是 module-frame.delete,修改子項入口(:63-65)掛的是 module-frame.update。這兩個都是**「你這個角色能不能做這個動作」**的檢查,不看你要動哪一份範本。拿到權限之後,請求內容裡的 moduleFrameUid(刪除)、parentTemplateUid(修改)可以隨便填,程式照填的去改。module_frame_item_service.py:159、:227)也沒有任何權限或歸屬檢查。compliance.workflow_templates)有隔離規則,只准改自己公司的;流程圖內容雖然存在沒有隔離的翻譯表,但每次存檔都會同時更新主表的「最後修改人」。跨公司時資料庫會讓主表那一筆改到 0 筆(不報錯),程式的資料存取層發現「預期改 1 筆卻改了 0 筆」就丟錯,整個交易撤銷,連翻譯表的改動一起還原。所以我判斷跨公司打不到——這是讀程式碼推的,沒有實際打過。delete_workflow_template(uid)(:247)刪的是網址上那個子項,跟前面改的那份範本毫無關聯——可以造成「刪掉的子項」與「流程圖上被移除的方塊」屬於不同範本,資料就此對不起來。為什麼是「中」不是「高」:要先有管理員等級的刪除或修改權限,而且被擋在公司邊界內。但它會讓範本資料悄悄對不上,事後很難查出原因。
怎麼修:在 update_module_frame_item()(:159)與 delete_module_frame_item()(:227)開頭,先用網址上的 uid 撈出子項,反查它所屬的範本,比對跟請求內容裡填的是不是同一份,不是就擋;再確認那份範本是呼叫者公司的。
不會。 yaml_to_module_frame_parser_adapter.py:16 用 yaml.safe_load,只會產出字串、數字、清單這類純資料,不會建出任意物件,也就不會執行程式。整個掃描範圍內沒有其他地方用 yaml.load、yaml.unsafe_load 或 pickle。這條可以結案,剩下的風險是 B1c-2(讓伺服器算不完),另案處理。
「掃到的這些存在嗎」
「只有這些嗎」
dirty),但這 8 支檔在掃描時沒有未 commit 的改動,結果對應的就是 f80b9f682 這個版本。| 項目 | 數字 |
|---|---|
| 掃描範圍 | 8 檔/925 行(入口 95、匯入邏輯 415、範本主體接縫 260、解析器與轉接器 91、資料格式定義 64) |
| 掃的 commit | f80b9f6824fd88987e47af0b7e8744cf46228caa(branch feature/review) |
| 工具 run ID | wf_93ae6f70-4e7 |
| effort | low(加開「只看攻擊者碰得到的正式程式碼」模式) |
| 研究員 | 派出 2、回報 2 |
| 候選發現 | 3 條 |
| 檢查員投票 | 3 條 × 3 人=應投 9 票,9 票全數投出、全數確認,沒有漏投、沒有降級 |
| 驗證章 | verified(CLAUDE-SECURITY-REVISION-f80b9f6824fd-dirty.json) |
| 耗時 | 5,247 秒(約 1 小時 27 分) |
| 工具原始報告 | CLAUDE-SECURITY-20260923-095422/(不入版控) |