60cf754f61b9180b41aef6feca06179e85a20d88(工作區有未提交改動,屬平行 session 的檔,與本棒無關)claude-security plugin 0.11.0,effort low掃完了,找到 12 個問題,全部三票一致確認,11 條中等、1 條低。最嚴重的一件事是:專案那一側六組附屬資料的權限守得滴水不漏,而範本這一側的同一批六組——程式是從那邊複製過來改的——讀取權限六組全漏、歸屬檢查六組全無。
這不是「六份裡有一份漏了」,是六份一起漏:複製當下就沒帶到,後來補權限時只補了寫入那半邊的角色檢查,讀取整排沒補、「這份範本是不是你家的」一支都沒問。
系統裡有兩個地方長得幾乎一樣:
程式碼是從專案那側複製到範本那側改出來的。專案那側先前已經掃過(FR-113 的 O5 那一棒),結論是六組全部守齊、一支不漏:讀取要「你是這個專案的人」,寫入要「你是這個專案的負責人」。這一棒就是拿那份結論當標準,逐組去比對範本側有沒有跟著做到。
比對結果:範本側六組,讀取全部漏、寫入全部漏。 不是「六份裡有一份漏了」,是六份一起漏——複製當下就沒帶到,之後補權限檢查時也只補了寫入那半邊的角色檢查,讀取整排沒補、歸屬檢查一支都沒有。
現況(2026-10-01):本棒各條後來的處理結果如下(過程紀錄保留,不改)。
- 第一類 F1~F5、F12(六支讀取入口無權限檢查)=M11 第 4、28 條,✅ 已修(CM-2186,commit
aafaa070d,1.21.0 出貨)。- 第二類 F6~F11(六組寫入不查範本歸屬)=M11 第 8 條,✅ 已修(CM-2187,commit
3f15886bb,1.21.0 出貨)。
| # | 這是什麼問題 | 出事會怎樣 | 要先有什麼才打得到 | 在哪裡 | 怎麼修 |
|---|---|---|---|---|---|
| F1 | 查範本的人員名單時,只檢查有沒有登入,沒檢查有沒有資源庫權限 | 聯絡人的姓名、email、電話、住址整批被撈走 | 只要有任何一組能登入的帳號 | api/module_frame/routes/module_frame_party_route.py:35 |
在 get 上加一行 @require_capability("module-frame.read") |
| F2 | 查範本的設備清單時,同上 | 資產編號、IP 位址、主機名稱外洩,等於送對方一張內網地圖 | 同上 | api/module_frame/routes/module_frame_inventory_route.py:27 |
同上 |
| F3 | 查範本的系統元件時,同上 | 元件用途、通訊協定、開放的連接埠範圍外洩 | 同上 | api/module_frame/routes/module_frame_components_route.py:27 |
同上 |
| F4 | 查範本的繼承授權時,同上 | 對外揭露公司向哪些雲端供應商繼承了哪些授權、授權日期 | 同上 | api/module_frame/routes/module_frame_leveraged_route.py:38 |
同上 |
| F5 | 查範本的系統特性時,同上 | 系統名稱、系統編號、資安敏感等級、授權邊界說明外洩 | 同上 | api/module_frame/routes/module_frame_system_characteristic_route.py:37 |
同上 |
| F12 | 查範本的設備/資訊系統對照時,同上 | 除了清單本身,還附帶真實資產主檔的內部編號,那些編號可以再拿去打其他端點 | 同上 | api/module_frame/routes/module_frame_ssp_resources_route.py:42 |
同上 |
| # | 這是什麼問題 | 出事會怎樣 | 要先有什麼才打得到 | 在哪裡 | 怎麼修 |
|---|---|---|---|---|---|
| F6 | 新增/修改/刪除範本人員時,只確認範本存在,沒確認範本屬於你 | 子公司管理員可以改總公司分享下來的範本、甚至原廠公版範本;之後所有依這份範本開的專案全部繼承被改過的內容 | 要有資源庫的編輯權限(通常是租戶管理員),不是任何人都行 | app/module_frame/service/module_frame_party_service.py:116 |
在 _template_ssp_id() 解出範本後呼叫 assert_scope_writable(mf.tenant_id, mf.scope) |
| F7 | 範本元件的寫入,同上 | 同上,改的是元件的協定/連接埠等描述 | 同上 | app/module_frame/service/module_frame_components_service.py:94 |
同上 |
| F8 | 範本設備清單的寫入,同上 | 同上,可以塞假資產或刪掉真資產 | 同上 | app/module_frame/service/module_frame_inventory_service.py:96 |
同上 |
| F9 | 範本繼承授權的寫入,同上 | 同上;而且刪掉後,指向它的元件連結會變成斷掉的孤兒 | 同上 | app/module_frame/service/module_frame_leveraged_service.py:105 |
同上 |
| F10 | 範本系統特性的寫入,同上 | 這是一份資料不是一堆,所以一次請求就整份蓋掉別人的設定 | 同上 | app/module_frame/service/module_frame_system_characteristic_service.py:87 |
同上 |
| F11 | 範本設備/資訊系統對照的寫入,同上 | 同上,連帶動到指向資產主檔的關聯 | 同上 | app/module_frame/service/module_frame_ssp_resources_service.py:63 |
同上 |
出事會怎樣。 合規聯絡人的個人資料——全名、電子郵件、電話號碼、通訊地址,外加對應到系統內哪個使用者——會流到完全沒有資源庫權限的帳號手上。而且資料庫這一層沒有第二道防線:oscal.parties 這張表完全沒有開「每個客戶只能看自己資料」的隔離機制(我逐張查過,五張存放這六類資料的表一張都沒開);而範本主表的規則又明文允許大家都看得到原廠公版(SYSTEM)與上層分享下來(SHARED)的範本(scripts/init/02-schema.sql:24323)。兩件事疊起來,這是跨公司的外洩,不只是跨部門。
在哪裡。 api/module_frame/routes/module_frame_party_route.py:35,ModuleFramePartyListResource.get
怎麼回事。 網址裡的範本編號是呼叫者自己填的,而這支讀取入口上面只掛了「有沒有登入」(@jwt_required()),沒有掛「有沒有資源庫的閱讀權限」。往下一層的 service.list_parties(uid) 也沒有補任何檢查,直接把整份人員資料回出去。同一個檔案裡的新增(第 42 行)、修改(第 64 行)、刪除(第 83 行)三支都有掛權限檢查,只有讀取這支沒有——所以這不是刻意設計,是漏掉。
打得到的具體走法。 一個只被加進某個專案當參與者的使用者(他登入後根本看不到「資源庫」這個選單,因為前端會依權限把它藏起來)拿自己正常的登入憑證去打 GET /api/1.0/module-frames/menu。這支列表入口同樣只驗登入(api/module_frame/routes/module_frame_route.py:27),會把範本編號列給他,包含所有人都看得到的原廠公版。他挑一個編號,再打 GET /api/1.0/module-frame/<編號>/parties,回來的就是完整人員名單。
要先有什麼才打得到。
怎麼修。 在 @jwt_required() 和 @inject 中間插一行 @require_capability("module-frame.read"),寫法照同資料夾的 module_frame_control_default_route.py:51。這個檔案開頭已經 import 過 require_capability 了,加一行就好。
檢查結果。 三位檢查員三票全數確認。
出事會怎樣。 範本裡登錄的資產清單會外洩,包含資產編號、IPv4 位址、主機名稱,以及每台設備對應到哪些系統元件。對攻擊者來說這是現成的踩點資料。存放這些資料的 oscal.ssp_inventory_items 同樣沒有客戶隔離機制,所以原廠公版與分享範本的內容會跨公司流出。
在哪裡。 api/module_frame/routes/module_frame_inventory_route.py:27,ModuleFrameInventoryListResource.get
怎麼回事。 與 F1 同一個形狀:網址上的範本編號由呼叫者決定,入口只驗登入,下層服務不補檢查。同檔案的新增/修改/刪除三支都掛了權限檢查。
打得到的具體走法。 低權限帳號先從選單入口拿到範本編號,再打 GET /api/1.0/module-frame/<編號>/inventory,拿到帶 IP 與主機名稱的設備清單。
要先有什麼才打得到。
怎麼修。 讀取那支加上 @require_capability("module-frame.read")。
檢查結果。 三位檢查員三票全數確認。
出事會怎樣。 元件的名稱、用途說明、使用的通訊協定與開放的連接埠範圍、以及安全認證相關設定會外洩。這等於把受評系統的對外暴露面直接交出去。oscal.ssp_components 沒有客戶隔離機制。
在哪裡。 api/module_frame/routes/module_frame_components_route.py:27,ModuleFrameComponentsListResource.get
怎麼回事。 同上:編號來自網址,入口只驗登入,服務層不補。同檔的三支寫入都有守。
打得到的具體走法。 沒有資源庫權限的帳號打 GET /api/1.0/module-frame/<編號>/components,讀到含協定與連接埠範圍的元件清單。
要先有什麼才打得到。
怎麼修。 讀取那支加上 @require_capability("module-frame.read")。
檢查結果。 三位檢查員三票全數確認。
出事會怎樣。 「我們有哪些安全控制是繼承自外部供應商的」這份清單會外洩,包含供應商名稱、授權日期、用途與狀態說明。對外揭露的是公司的雲端供應鏈結構。oscal.ssp_leveraged_authorizations 沒有客戶隔離機制。
在哪裡。 api/module_frame/routes/module_frame_leveraged_route.py:38,ModuleFrameLeveragedListResource.get
怎麼回事。 同上。同檔的新增(42 行)/修改(62 行)/刪除(79 行)三支都掛了權限檢查,讀取沒有。
打得到的具體走法。 低權限帳號拿範本編號打 GET /api/1.0/module-frame/<編號>/leveraged,看到公司從哪些外部供應商繼承授權。
要先有什麼才打得到。
怎麼修。 讀取那支加上 @require_capability("module-frame.read")。
檢查結果。 三位檢查員三票全數確認。
出事會怎樣。 系統名稱、系統唯一識別碼、資安敏感等級分類、以及授權邊界的描述會外洩。這是整份合規文件的抬頭資料,等於告訴外人「這套系統多重要、範圍到哪裡」。oscal.ssp_system_characteristics 沒有客戶隔離機制。
在哪裡。 api/module_frame/routes/module_frame_system_characteristic_route.py:37,ModuleFrameSystemCharacteristicResource.get
怎麼回事。 這條的證據最直接:同一個檔案裡,緊接在讀取下面的修改(第 45 行)掛著 @require_capability("module-frame.update"),讀取(第 28 行)只有登入檢查。同一份資料,改要權限、看不用——這就是漏掉,不是設計。
打得到的具體走法。 沒有資源庫權限的帳號打 GET /api/1.0/module-frame/<編號>/system-characteristic,讀到另一家公司合規文件的敏感等級與授權邊界。
要先有什麼才打得到。
怎麼修。 讀取那支加上 @require_capability("module-frame.read")。
檢查結果。 三位檢查員三票全數確認。
出事會怎樣。 子公司的管理員可以在原廠公版範本或總公司分享下來的範本裡面新增、修改、刪除人員、元件、設備、繼承授權與系統特性。範本主表的讀取規則刻意讓這些範本對下游可見,而底下五張存實際資料的表一張都沒有客戶隔離機制,所以寫入會真的落到別家公司的資料上。更嚴重的是之後每一個依這份範本開的專案都會繼承被改過的基準——這是稽核證據的完整性問題,不只是資料被動。同樣的缺口在本棒範圍內的另外五支服務都在(F7~F11)。
在哪裡。 app/module_frame/service/module_frame_party_service.py:116,ModuleFramePartyService.add_party
怎麼回事。 網址帶進來的範本編號一路傳到 _template_ssp_id()(第 159 行)→ _require_mf()(第 172 行),而 _require_mf 只做一件事:查得到就過(if mf is None: raise NotFound)。它沒有問「這份範本是不是你家的」。相比之下同專案的 ModuleFrameService.update_module_frame(app/module_frame/service/module_frame_service.py:112)就有呼叫 assert_scope_writable。資料庫那層也補不了,因為 oscal.parties 沒有隔離機制。
打得到的具體走法。 子公司管理員打 GET /module-frames/menu,回來的清單包含原廠公版與總公司分享下來的範本。他挑一個,打 POST /api/1.0/module-frame/<那個編號>/parties。角色檢查會過——因為他在自己公司確實有這個權限;範本查得到——因為讀取規則允許他看到分享範本;寫入落到 oscal.parties——因為那張表沒有隔離機制。總公司與所有兄弟公司之後讀到的都是被改過的範本。
要先有什麼才打得到。
怎麼修。 在 _template_ssp_id() 解出範本物件後,呼叫 assert_scope_writable(mf.tenant_id, mf.scope)(common/authz/sharing.py:25),寫法照 module_frame_service.py:112。同樣的一行要補在元件/設備/繼承授權/系統特性四支服務。長期解是給這五張 OSCAL 子表加上依 SSP 推導的客戶邊界,讓程式忘了檢查時資料庫還能擋——現在這五張表是「程式漏了就全開」。
檢查結果。 三位檢查員三票全數確認。
出事會怎樣。 分享範本或原廠公版範本裡的系統元件紀錄(類型、標題、說明、用途、狀態、協定與連接埠、安全認證設定)可以被下游公司建立、修改或刪除,污染其他公司與所有衍生專案讀到的基準。
在哪裡。 app/module_frame/service/module_frame_components_service.py:94,ModuleFrameComponentsService.add_item
怎麼回事。 與 F6 同一個形狀:_template_ssp_id 只驗存在不驗歸屬,寫入落到沒有隔離機制的 oscal.ssp_components。
打得到的具體走法。 拿一個分享範本的編號,子公司管理員打 DELETE /api/1.0/module-frame/<編號>/components/<元件編號>:角色檢查過、範本查得到、元件在別家公司的範本裡被找到、刪除執行——資料庫沒有任何規則攔下來。
要先有什麼才打得到。
怎麼修。 在 _template_ssp_id 內(或每支對外寫入方法開頭)加 assert_scope_writable(mf.tenant_id, mf.scope)。
檢查結果。 三位檢查員三票全數確認。
出事會怎樣。 分享範本的資產清單(資產編號、IP 位址、主機名稱,以及指向資產主檔的關聯)可以被跨公司新增、竄改或刪除,污染其他公司依賴的合規基準。
在哪裡。 app/module_frame/service/module_frame_inventory_service.py:96,ModuleFrameInventoryService.add_item
怎麼回事。 同 F6 形狀,寫入落到沒有隔離機制的 oscal.ssp_inventory_items。
打得到的具體走法。 子公司管理員打 POST /api/1.0/module-frame/<分享範本編號>/inventory 塞一筆假資產。權限檢查過、範本讀得到、寫入無人攔阻。
要先有什麼才打得到。
怎麼修。 _template_ssp_id 解出範本後加 assert_scope_writable(mf.tenant_id, mf.scope)。
檢查結果。 三位檢查員三票全數確認。
出事會怎樣。 分享範本裡的繼承授權紀錄(標題、授權單位、授權日期、供應商、狀態、分類)可以被下游公司偽造或刪除。而且這筆資料的識別碼是元件用來指向它的關聯依據,刪掉會讓指向它的元件連結悄悄變成斷掉的孤兒,畫面上不會報錯。
在哪裡。 app/module_frame/service/module_frame_leveraged_service.py:105,ModuleFrameLeveragedService.add_item
怎麼回事。 同 F6 形狀,寫入落到沒有隔離機制的 oscal.ssp_leveraged_authorizations。
打得到的具體走法。 子公司管理員打 PUT /api/1.0/module-frame/<分享範本編號>/leveraged/<項目編號>,改掉總公司基準上的授權日期。權限檢查過、無歸屬檢查、寫入落地。
要先有什麼才打得到。
怎麼修。 _require_mf 之後補 assert_scope_writable(mf.tenant_id, mf.scope)。
檢查結果。 三位檢查員三票全數確認。
出事會怎樣。 分享範本或原廠公版範本那份唯一的系統特性紀錄(系統名稱、說明、系統識別碼、資安敏感等級、授權邊界、狀態)可以被下游公司整份蓋掉。這是一對一的紀錄,所以一次請求就換掉整份,不像其他五組是多筆資料要一筆一筆動。這條的破壞速度最快。
在哪裡。 app/module_frame/service/module_frame_system_characteristic_service.py:87,ModuleFrameSystemCharacteristicService.update
怎麼回事。 同 F6 形狀,覆寫落到沒有隔離機制的 oscal.ssp_system_characteristics。
打得到的具體走法。 子公司管理員打 PUT /api/1.0/module-frame/<分享範本編號>/system-characteristic,送 {"security_sensitivity_level": "low", "scope_description": "..."}。角色檢查過、範本可見、既有紀錄被整筆覆蓋。
要先有什麼才打得到。
怎麼修。 解出範本後加 assert_scope_writable(mf.tenant_id, mf.scope)。
檢查結果。 三位檢查員三票全數確認。
出事會怎樣。 掛在分享範本上的設備與資訊系統項目可以被下游公司建立、編輯或刪除,包含指向資產主檔的關聯欄位。
在哪裡。 app/module_frame/service/module_frame_ssp_resources_service.py:63,ModuleFrameSspResourcesService.add_item
怎麼回事。 _require_mf(第 134 行附近)與 _require_ssp(第 91 行)都只確認東西存在,之後就把事情交給下層去做,寫入落到沒有隔離機制的 oscal.ssp_components。
打得到的具體走法。 子公司管理員打 DELETE /api/1.0/module-frame/<分享範本編號>/ssp-resources/items/<項目編號>,刪掉屬於上層公司範本的一筆元件。
要先有什麼才打得到。
怎麼修。 _require_mf 之後,在三支寫入入口都加 assert_scope_writable(mf.tenant_id, mf.scope)。
檢查結果。 三位檢查員三票全數確認。
出事會怎樣。 掛在範本上的設備與資訊系統清單會外洩。比前面幾條多一層:回傳的內容經過加工,帶上了資產主檔裡真實紀錄的名稱與內部編號(SspResourcesContextService._device_to_dict 會補上 matched_device_name / matched_device_uid / matched_info_system_name / matched_info_system_uid),那些編號可以再拿去打其他資產相關的端點。
在哪裡。 api/module_frame/routes/module_frame_ssp_resources_route.py:42,ModuleFrameSspResourcesResource.get
怎麼回事。 同 F1 形狀:編號來自網址,入口只驗登入。同檔的三支寫入都掛了權限檢查。
打得到的具體走法。 低權限帳號打 GET /api/1.0/module-frame/<編號>/ssp-resources,拿到設備/資訊系統清單以及它們對應的內部資產編號。
要先有什麼才打得到。
怎麼修。 讀取那支加上 @require_capability("module-frame.read")。
檢查結果。 三位檢查員三票全數確認。研究員原本評中等,三位檢查員投票後降為低(投出的等級是中、低、低),理由是外洩內容比前五條有限、且要範本先掛過這類資料。報告採用檢查員的最終等級。
掃了什麼。 15 支檔案、1,904 行,全部讀完,沒有任何檔案被跳過或截斷。包含:六類附屬資料的網址入口(六支路由)、範本匯出入口、範本 SSP 查詢入口、一支序列化定義,以及六支商業邏輯服務。
沒往下追什麼(刻意的,範圍界定如此)。
mf_ssp_export_route.py 底下的匯出服務——在 FR-113 O2 那一棒的範圍內,本棒只核對路由本身。核對結果:這支守齊了,同時掛了登入檢查與 @require_capability("module-frame.read")(第 51-52 行),是本棒六支讀取入口裡唯一正確的一支。app/oscal/service/resource_library_app_service.py 本體——已在 O2 掃過。本棒只看 module_frame_template_ssp_route.py 怎麼呼叫它(見下)。這個強度的限制。 low 是單一研究員通掃全範圍加三人投票,不做元件拆分、不做威脅建模、不做廣度複掃。所以「這 12 條存在嗎」可信度高(每條都三票一致,我也逐條開檔核對過行號與說法);「只有這 12 條嗎」則不能保證——這是快速一輪,不是窮盡式閱讀。
卡片特別點名的那支檔案(api/oscal/routes/module_frame_template_ssp_route.py)。 這支 38 行的檔案本來因為「出現在 O2 的清單裡」被當成掃過而排除,但 O2 報告寫的是「掃描沒有針對這支提出發現」——那是「工具沒報」,不是「查過沒事」。本棒當成從沒查過的檔完整讀了。結果:工具沒有對它出報告。 我自己核對的狀況是:它只掛 @jwt_required(),沒有角色檢查,呼叫的 get_template_ssp_ref(mf_uid) 回的是 {ssp_uid, exists}——只有一個識別碼和一個布林值,沒有實質內容。它的形狀確實與 F1~F5、F12 同一類(讀取入口沒有權限檢查),只是回傳的東西沒有敏感資料,所以三人面板沒有讓它成案。我的判斷是這支仍應一併補上權限檢查,理由有二:一是它回的識別碼正是後續 SSP 相關端點的入場券,二是同一批六支讀取入口全部要補,漏掉這一支就又留下一個「六份裡有一份沒跟上」的種子。這一點不是工具的發現,是我核對後的建議,請決策者定奪。
「這些問題存在嗎」——可信度高。 12 條每一條都是三位獨立檢查員三票一致確認,沒有一條是勉強過關的。我另外自己做了三件核對:① 逐支開檔確認引用的行號與程式碼相符(全部對上);② 實際查資料庫結構檔,確認那五張表真的沒有客戶隔離機制(oscal.parties、ssp_components、ssp_inventory_items、ssp_leveraged_authorizations、ssp_system_characteristics,五張全部是 0 筆隔離設定);③ 把專案那一側六支服務的守門逐支列出來比對。
第③項是本棒最有力的證據。 專案那側六支服務,每一支的每一個對外方法開頭都有一行:讀取是 self._perm.require_participant(ssp_uid)(你要是這個專案的人),寫入是 self._perm.require_manager(ssp_uid)(你要是這個專案的負責人)——六支全中、一支不漏。範本這側六支服務,同樣位置一支都沒有,只有 _require_mf() 的「查得到就過」。兩邊放在一起看,這不是「可能有風險」,是同一套邏輯在一側做了、在另一側整排沒做。
另外還有一個形狀上的觀察。 卡片要我特別比對「檢查 A、動手改 B」那個病。專案那側的做法是先用權限檢查回傳的 SSP 編號去撈清單、再比對子物件編號。範本這側的查找函式(各服務的 _resolve)形狀其實是對的——都是先解出範本的 SSP 編號、再在那個範圍內比對子物件編號,動手的對象確實是從前面解出來的東西推出來的,沒有踩到「改使用者另外送來的那個編號」那個坑。問題不在查找的形狀,在於前面那一步「解出範本」時根本沒問歸屬。所以這六條的病因是「該問的沒問」,不是「問了 A 卻動了 B」。
「只有這些嗎」——不保證。 low 強度是一輪快掃,不做威脅建模與廣度複掃。而且本棒只看 15 支檔案,其他入口(例如 Excel 與 Word 匯入那幾條路)不在範圍內。
沒有任何程式被執行過。 全部結論都來自讀程式碼,沒有跑測試、沒有實際發動攻擊、沒有驗證過概念性攻擊程式。
| 項目 | 數值 |
|---|---|
| 掃描版本 | 60cf754f(工作區有其他未提交改動) |
| 範圍 | 15 支檔案、1,904 行 |
| 強度 | low,聚焦正式程式碼(不看測試與第三方複製品) |
| 耗時 | 約 2 小時 47 分 |
| 派出的 agent | 41 個,全部完成,0 個失敗、0 個被跳過、0 個回空 |
| 研究員 | 派出 2 位、回報 2 位 |
| 候選問題 | 21 條,去掉重複後 13 條 |
| 投票 | 13 條各由 3 位檢查員投票,39 票全數投出,沒有漏投 |
| 成案 | 12 條(1 條被三位檢查員一致否決) |
| 等級調整 | 1 條(F12 由中等降為低) |
| 未驗證候選 | 0 |
| 驗證輪次 | 1 輪跑完,沒有待續 |
三人面板完整跑完、票數齊全,所以這份報告帶得動驗證章。