掃描當下版本:commit
1c9be8d84(branchws-CM-3711-CM-3720)。行號以這個版本為準。
沒有急件,但有一條要排進修正。 這次找到 2 個問題,兩條都是同一個缺口:落地版裡,更新中心沒有檢查操作的人是不是「總部」。
兩條都經過三個獨立檢查員投票,結果都是 3 比 0 成立。我也自己開檔核對過。
更新中心頁面背後的 API:查目前版本與更新紀錄、檢查新版、下載新版、離線分塊上傳更新包、立即或排程更新、取消與重試、產生並下載診斷包、下載升級紀錄,以及這次新加的 Agent 區(查 Agent 新版、下載、推送給各台 Agent)。
這些動作影響的是整台機器,不是某一家公司。落地版也可以開子公司(子租戶),所以重點是「子公司的管理員是不是碰得到整台機器的東西」。
範圍:4 個檔、約 1135 行。為了查證,我翻了範圍外的 core/host_capabilities.py:214-215、upgrade_order.py:424-463、product_update_flow.py 的上傳段、common/util/chunked_upload.py、resources/roles/shipped-roles.json。這些檔只是拿來查證,本身不算發現。
| # | 這是什麼問題 | 出事會怎樣 | 要先有什麼才打得到 | 在哪裡 | 怎麼修 | 嚴重度 |
|---|---|---|---|---|---|---|
| F1 | 診斷包和升級紀錄包裝的是整台機器的資料,但只要有更新中心的權限就能下載,沒有檢查是不是總部 | 子公司管理員拿得到總部和其他子公司最近 24 小時的操作紀錄:帳號、來源 IP、網址、訊息、遮罩後的請求內容 | ① 落地版 ② 機器上開了子公司 ③ 攻擊者是子公司管理員(出廠角色預設就有這兩個權限);或者只有「更新中心讀取」權限,就能拿到升級紀錄 | app/product_update/service/product_update_app_service.py:228-235(diag_file、upgrade_log_file);判斷在 :104-107 |
這兩支在落地版改呼叫 require_tenant_hq()(「只有總部可以」的共用檢查);upgrade_log_file 不能只靠讀取權限就放行 |
中 |
| F2 | 更新中心的所有寫入動作,在落地版都沒有檢查是不是總部 | 子公司管理員可以讓整台機器馬上升級,造成所有公司一起停機;也可以排程升級、關掉每日自動檢查 | ① 落地版 ② 攻擊者是子公司管理員 | app/product_update/service/product_update_app_service.py:104-107(_assert_may_write) |
_assert_may_write 在落地版也呼叫 require_tenant_hq() |
低 |
為什麼 F1 是中、不是高:要先滿足三個條件:客戶在落地版上開了子公司、攻擊者本身就是那家子公司的管理員、拿到的資料已經遮過密碼。外部的人打不到,拿到的也是紀錄,不是帳密。
為什麼 F2 是低:升級只能升到原廠簽過章的新版,換不成惡意程式,最壞就是挑時間讓整台停機。
白話:診斷包是主機上的 guidantai diag 產生的。它在「系統身分」下執行,會跳過「每家公司只能看自己資料」的資料庫隔離機制(RLS),所以包裡是全部公司的 API 紀錄、系統紀錄、Agent 紀錄,再加上 docker 紀錄和遮過的設定檔。「支援頁」也能匯出同一種診斷資料,但那條路只給呼叫者自己公司的資料;更新中心這條路沒有。
_assert_may_write() 只在 SaaS 時要求平台管理員,落地版直接放行。更新中心的兩個權限(system.product-update.read/.update)也沒有標「只限平台」(is_platform)。出廠的「系統管理員」角色是萬用字元 *.*.*,子公司的管理員因此會自動拿到這兩個權限。
出事會怎樣:子公司管理員先按「產生診斷包」(POST /system/product-update/diag),再下載(GET /system/product-update/diag/<diag_id>),就拿到總部和其他子公司的操作紀錄。只有讀取權限的人,可以從狀態 API 拿到 job_id,再打 GET /system/product-update/upgrade-log/<job_id> 下載升級紀錄包。
要先有什麼才打得到:落地版、開了子公司、子公司管理員(或者擁有更新中心讀取權限的人)。
在哪裡:
app/product_update/service/product_update_app_service.py:104-107:_assert_may_write 只在非落地版擋app/product_update/service/product_update_app_service.py:228-230:diag_fileapp/product_update/service/product_update_app_service.py:232-235:upgrade_log_file(route 只掛讀取權限,見 api/product_update/routes/product_update_route.py:205)core/host_capabilities.py:214-215:兩個權限都沒有 is_platform怎麼修:在 diag_file、request_diag、upgrade_log_file 開頭,落地版加 require_tenant_hq()(common/authz/tenant_hq.py);或者比照支援頁匯出,改用 require_log_viewer('platform.support.export') 再加總部檢查。另一個做法是讓這條路產生的診斷包只含呼叫者自己公司的資料。
小弟開檔核對:成立。 _assert_may_write 的判斷、兩個權限沒有 is_platform、出廠角色是 *.*.*,三點都開檔看過,和描述一致。
白話:更新中心的每個寫入動作(檢查新版、自動檢查開關、下載、離線上傳、立即或排程更新、取消、重試、Agent 推送)都只過 _assert_may_write,這一關在落地版什麼都不擋。系統裡其他「全站共用設定」,例如登入規則、LDAP、日誌轉送,都有過 require_tenant_hq(),只有更新中心漏掉。
出事會怎樣:子公司管理員送 POST /system/product-update/apply、mode=now,整台機器馬上升級,所有公司在升級期間都用不了。他也可以排程更新、關掉每日自動檢查(PUT /auto-check)。有一位檢查員指出,「取消總部排好的升級」這一點說得有點誇大,但「自己發起整台升級」這一點成立。
要先有什麼才打得到:落地版、子公司管理員。
在哪裡:app/product_update/service/product_update_app_service.py:104-107
怎麼修:_assert_may_write 改成:SaaS 照舊要求平台管理員,落地版加呼叫 require_tenant_hq()(平台管理員在那支函式裡本來就會放行)。F1 和 F2 可以合成一張修正卡。
小弟開檔核對:成立(同 F1 的核對)。
system.product-update.update 能不能升級整台:成立,就是 F2。core/host_capabilities.py:214-215 兩個權限都沒有 is_platform,也沒有「只限總部」的宣告。出廠角色 *.*.* 在子公司只排除 is_platform 的權限,所以子公司管理員一定有這個權限。upload_id 要符合 32 碼十六進位格式(common/util/chunked_upload.py:24,55)。index 在路由宣告成整數,而且會檢查 0 <= index < total_chunks(:97)。每塊最多 50MB,邊讀邊算,超過就中止(:109)。總大小最多 8GB,塊數也有上限(product_update_flow.py:581-584),開始上傳前會先檢查磁碟空間,而且同一時間有下載或更新在跑就回 409。驗章的順序也對:單一離線檔是先讀檔內的清單、驗過簽章,才把包抽出來(product_update_flow.py:674-683);舊的兩檔送法是清單一開始就要驗過才收包(:592-598)。小缺口:上傳中的檔案不受「同一時間只能一個」的限制,同一個人可以開很多個上傳把磁碟塞滿,但能這樣做的人本來就有更新權限,所以不列為發現。diag_file/upgrade_log_file:路徑本身沒問題。id 要符合 job_id 格式,會跳過符號連結,也會檢查是不是一般檔案(upgrade_order.py:426-431)。權限面有問題,就是 F1。ProductUpdateNoticeRoute 只驗登入:沒問題。 回傳的只有 scheduled_at、version 和固定的提醒分鐘數(product_update_app_service.py:245-246)。package_downloader.py Agent 包路徑:沒問題。 agent_bundle_dir 先用 parse_version 檢查版號格式,不符就丟例外(package_downloader.py:87-89),檔名用的也是同一個版號。@transaction 或權限檢查:Agent 寫入方法都呼叫了 _assert_may_write(:264-311),下載完自動推送的 push_after_download 也有(:328)。這些方法沒有 @transaction,檔案裡的註解說明是刻意的:流程交給下一層各自包交易,不是漏掉。權限檢查本身的缺口和 F2 是同一件事。product_update_flow.py、upgrade_order.py、agent_rollout_service 只讀了查證需要的那幾段,沒有完整掃。| 項目 | 值 |
|---|---|
| run ID | wf_78b66801-823 |
| 報告目錄 | CLAUDE-SECURITY-20261108-191322/ |
| verification.status | verified |
| reason_kind | null |
| candidates(候選發現) | 2 |
| panel_votes(檢查員投票數) | 6(三個檢查員 × 2 條,沒有漏投) |
| researchers_dispatched/returned | 2/2 |
| 耗時 | 約 44 分鐘 |
| effort/focus | low/attack-surface |
歸檔:docs/features/security-scan-consolidated/runs/be/CLAUDE-SECURITY-20261108-191322/