B1c 檢查結果:用檔案整批匯入合規範本

B1c 檢查結果:用檔案整批匯入合規範本

檢查日期 2026-09-23|耗時 1 小時 27 分|對應卡片 CM-2080|範圍 8 檔/925 行|掃的版本 f80b9f682


§1

🔴 一句話結論

掃描工具找到 3 個問題(三個檢查員全數確認),其中新的只有 2 個,都屬於「讓伺服器忙到當掉」這一類;我自己開檔另外查出 2 個,最嚴重的是:「整批匯入」可以直接覆蓋一份已經存在的範本,但系統只檢查了「你能不能建新範本」。

另外兩件卡片特別要求單獨回答的事:

  • 上傳 YAML 檔能不能在伺服器上執行程式?不能。 系統用的是安全的讀法(yaml.safe_load),檔案內容只會被當成資料,不會被當成程式執行。但它沒有限制檔案的巢狀層數,一個幾 KB 的檔案就能讓伺服器忙到當掉(見 B1c-2)。
  • YAML 整批匯入建子項,走的是有守門的那支方法,還是沒守的?兩邊都不是——它完全繞過子項服務,而且這條路目前根本建不出任何東西。 真正會呼叫子項服務的是另一個入口「Excel 整批匯入」,而它走的正是 B1-item 講的那支沒守的方法(見 B1c-4)。

§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 匯入建不出東西,資訊性):無修正卡,查不到後續處理。
§3

找到什麼

# 嚴重度 這是什麼問題 出事會怎樣 要先有什麼才打得到 在哪裡(該補檢查的位置) 來源
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) ⚠️ 我自己開檔查出,未經工具投票

§4

每條發現的詳述

B1c-1:改控制項清單不檢查範本歸屬(與 B1 第 1 條相同)

在哪個畫面、誰、做什麼:合規資源庫頁面,子公司的管理員打開一份「母公司分享給旗下子公司」或「原廠內建」的範本,改它的適用控制項清單並存檔(或直接送 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),不符就擋下。


B1c-2:YAML 檔可以用「引用」讓伺服器爆掉

這是什麼問題: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,應一併改回「格式錯誤」訊息。


B1c-3:Excel 匯入檢核的比對寫法會被拖慢

這是什麼問題: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}>。


B1c-4:「建立」權限就能覆蓋既有範本(⚠️ 我自己查出,未經投票)

這是什麼問題:Excel 匯入存檔的入口(/module-frame/import)只掛了 module-frame.create。但進到 save_import_module_frame_data() 後,只要送上來的資料帶了既有範本的編號(module_frame.uid),程式就走「覆蓋」這條路(:125-129):

  1. 呼叫 update_module_frame 改範本主檔(名稱、版本等)——這支就是 B1c-1 那支,不改分享範圍就不檢查歸屬
  2. 對既有的每一條,呼叫子項服務的 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"),沒有就擋。
  • 無論哪個答案,都要確認這份範本是呼叫者公司的(B1c-1 修好就涵蓋)。

B1c-5:YAML 匯入目前建不出東西(⚠️ 我自己查出,資訊性)

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 那條。


§5

首腦交辦的兩件事

一、B1-item 那條「待評」:刪/改子項「檢查甲、動手改乙」

答案:入口的權限點沒有把範圍限住,建議定為「中」。

  • 刪除子項入口(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 筆」就丟錯,整個交易撤銷,連翻譯表的改動一起還原。所以我判斷跨公司打不到——這是讀程式碼推的,沒有實際打過。
  • 同公司內則確定打得到:有刪除權限的人,可以指定刪「另一份範本」裡的某一條,或把 A 範本的子項掛到 B 範本的流程圖上改。
  • 另外注意:刪除那支最後一行 delete_workflow_template(uid)(:247)刪的是網址上那個子項,跟前面改的那份範本毫無關聯——可以造成「刪掉的子項」與「流程圖上被移除的方塊」屬於不同範本,資料就此對不起來。

為什麼是「中」不是「高」:要先有管理員等級的刪除或修改權限,而且被擋在公司邊界內。但它會讓範本資料悄悄對不上,事後很難查出原因。

怎麼修:在 update_module_frame_item()(:159)與 delete_module_frame_item()(:227)開頭,先用網址上的 uid 撈出子項,反查它所屬的範本,比對跟請求內容裡填的是不是同一份,不是就擋;再確認那份範本是呼叫者公司的。

二、YAML 解析會不會讓上傳者執行程式

不會。 yaml_to_module_frame_parser_adapter.py:16 用 yaml.safe_load,只會產出字串、數字、清單這類純資料,不會建出任意物件,也就不會執行程式。整個掃描範圍內沒有其他地方用 yaml.load、yaml.unsafe_load 或 pickle。這條可以結案,剩下的風險是 B1c-2(讓伺服器算不完),另案處理。


§6

這份結果可信到什麼程度

「掃到的這些存在嗎」

  • B1c-1~B1c-3:可信。 三個檢查員各自獨立投票(分別從「打不打得到」「後果多大」「有沒有別的防線擋住」三個角度看),9 票全數確認成立。我也開檔逐行核對過行號。
  • B1c-4、B1c-5:事實可信,影響判斷未經驗證。 程式碼確實如此,我開檔核對過;但「跨公司會被資料庫擋下」是我讀程式碼推的,沒有實際打過,也沒經過檢查員投票。
  • B1-item 待評那條的「中」:同上,是我的判斷,首腦可推翻。

「只有這些嗎」

  • 這一棒用的是最省的掃法(effort low:一個研究員讀完整個範圍,沒有先畫威脅地圖,也沒有第二輪廣掃),研究員讀完後三個檢查員驗證。不是逐行窮舉。
  • 驗證章:verified(工具驗證流程完整跑完)。
  • 掃描時工作區有別的 session 的未 commit 改動(驗證章標了 dirty),但這 8 支檔在掃描時沒有未 commit 的改動,結果對應的就是 f80b9f682 這個版本。

§7

執行概況

項目 數字
掃描範圍 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/(不入版控)