掃到 3 個低風險問題,沒有高風險。三條都要先控制一台已經註冊的 Agent(或是持有更新權限的子公司管理員)才打得到。最值得修的是第 1 條:一個客戶的 Agent 可以讓另一個客戶的 Agent 升級卡住或被暫停。
主系統下載好 Agent 新版之後,怎麼推到各台 Agent:
推送等於「讓遠端機器以高權限換掉自己的程式」,所以重點看誰能推、推給誰、推哪一版。
範圍是 7 支檔案、共 1181 行,見文末執行概況。
| # | 這是什麼問題 | 出事會怎樣 | 要先有什麼才打得到 | 在哪裡 | 怎麼修 | 嚴重度 |
|---|---|---|---|---|---|---|
| 1 | 甲客戶的 Agent 自己要求更新時,會被排進乙客戶那次推送的同一批 | 乙客戶其餘 Agent 的升級永遠卡住,或被自動暫停。資安修正版可能因此推不出去 | 控制甲客戶一台 Agent;乙客戶同一版的推送還有後面的批次在等 | app/product_update/service/agent_rollout_service.py:483 |
批次編號改在「看得到全部客戶」的身分下計算,或分批時把客戶別一起當分組條件 | 低 |
| 2 | Agent 上傳升級紀錄時,沒有限制能傳幾次 | 每換一個工單編號就多存一份 10 MB 的檔案,最後把主機硬碟塞滿、服務停擺;存進去的紀錄刪不掉 | 控制一台 Agent | app/product_update/service/agent_update_event_handlers.py:243(由 api/remote_agent/routes/agent_update_log_route.py:26 呼叫) |
只收「這台真的被派過的工單」的紀錄,並加每台次數上限與總容量上限 | 低 |
| 3 | Agent 主控台按「立即檢查」時,會拿到全機共用的上次檢查結果 | 任何一台 Agent 都看得到「哪個管理員帳號剛按過檢查」,以及所有客戶裡最舊的 Agent 版本 | 持有任一台 Agent 的通行證 | core/plugins/_agent_console_update.py:57 |
回給 Agent 的內容另做一份精簡版,拿掉 by、agent_base/current、host_version |
低 |
為什麼三條都是「低」:每一條都要先控制一台 Agent,而且影響只到「延誤升級、塞硬碟、洩漏一個帳號名稱」,不能拿到資料、也不能執行程式。
白話說明。 推送採「先推 1 台,成功了才推其餘」,靠批次編號決定誰等誰。Agent 主控台按「請更新這一台」時,程式用這台 Agent 所屬客戶的身分去查「這一版目前排到第幾批」。因為有 RLS(就是「每個客戶只能看自己資料」的資料庫隔離機制),它只看得到自己客戶的列,所以以為自己是第 2 批。可是每分鐘的排程用系統身分推進批次,看到的是全部客戶的列,按批次編號分組。結果甲客戶這台跟乙客戶的「先推的那 1 台」被算成同一批。甲客戶這台只要回報失敗,排程就把乙客戶第 3 批全部暫停;如果它一直不回報,乙客戶第 3 批就永遠等下去。
在哪裡。
agent_rollout_service.py:483(_rebatch 算 base)agent_rollout_service.py:448-466(_advance_group 依 batch 分組)core/plugins/_agent_console_update.py:90(tenant_context(agent.tenant_id))怎麼修。 二選一:
_rebatch 的 base 改在系統身分下計算_advance_group 的分組條件加上 tenant_id,或每次推送一個獨立 id首腦核對。 我開檔追過:request_update 確實在 tenant_context 內呼叫 push/reapply,tick → _advance 是系統身分。成立。
白話說明。 Agent 每次升級完會上傳一份壓縮的紀錄檔(每份最多 10 MB)。工單編號是 Agent 自己帶的,主系統只檢查格式,不檢查「這張工單是不是真的派給過這台」。被控制的 Agent 每次換一個新編號,主系統就多存一份檔案、多一筆紀錄,而且紀錄表禁止刪除。
在哪裡。
agent_update_event_handlers.py:235-247(_save_log:同一張工單只擋重傳,不擋新編號)api/remote_agent/routes/agent_update_log_route.py:26這支檔案不在本棒範圍內,因為範圍內的上傳端點會走到它才列入。
怎麼修。
_save_log 先確認該 job_id 已經有這台 Agent 的升級事件列,沒有就回 404,不要新建首腦核對。 我開檔看過 _save_log:self._row(...) 遇到沒看過的編號會直接建新列。成立。
白話說明。 Agent 主控台按「立即檢查」時,如果 5 分鐘內有人查過,就直接回上次的結果。這份結果是主系統更新中心用的,只拿掉了簽章原文,下列欄位都還在:
by:上次按檢查的管理員帳號host_version:主系統版本agent_base:所有客戶裡最舊的 Agent 版本在更新中心,要有更新中心的讀取權限才看得到這份資料;在 Agent 這條路上,任何一台 Agent 都看得到。
在哪裡。
core/plugins/_agent_console_update.py:57app/product_update/service/agent_release_service.py:484-494(public_check 只拿掉 signed_manifest)怎麼修。 在 check_for_agent 回一份給 Agent 專用的欄位:status、latest、compatible、notes、at。
首腦核對。 我開過 public_check,確認只拿掉 signed_manifest。成立。
agent_rollout_service.py:_push):不成立。get_remote_agent_by_uid 在登入者身分(受 RLS)下查,別的客戶的 uid 查不到,直接列入 skipped。推送端點 ProductUpdateAgentPushRoute 有掛 @jwt_required() 加 @require_capability(CAP_UPDATE)(api/product_update/routes/product_update_route.py:258-259),也就是有檢查這個人有沒有更新權限。另外有發現:雖然推不到別人的 Agent,但批次編號會互相干擾,見第 1 條。_agent_console_update.py):不成立。哪一台只認通行證(current_agent().agent_uid),不收 body 帶的編號。版本由主系統手上的包決定,Agent 不能指定。is_lagging 只推比目標舊的台,不會降版。同版重套要主系統開關打開,而且更新站上沒有更新的版。_agent_update.py):不成立。心跳交給 Agent 的只有 target_version、bundle_sha256、size,沒有下載網址。下載端點有掛 agent_pass_required,路由檔註明撤銷、停用、待核准一律擋(api/remote_agent/routes/agent_update_bundle_route.py:3,17)。_tenant_of 也會排除 revoked。之後的 _dispatchable_bundle 要求這台要有「已下發」的推送列,下發前只放 active 的台(_take_woken、_push 都檢查 status == active)。agent_pass_required:成立。agent_update_request_route.py:25,35,45 三支、agent_update_log_route.py:17 一支,全都有掛。_agent_update_log_ownership.py):成立,會強制下載。檔名由系統產生,固定是 agent-update-<編號>.tar.gz;.tar.gz 不在可直接開啟的副檔名清單(pdf/html/svg/圖片)內。jedi-file-upload 的 decide_preview 對清單外的副檔名一律設成附件下載加 octet-stream,所以瀏覽器不會直接開啟。讀取要有 system.product-update.read 權限,寫入與刪除一律拒絕。agent_rollout_views.py):部分成立。error 欄位確實是 Agent 自報的原文,但存進去之前已經在 agent_update_event_handlers.py:116 用 _text(..., _ERROR_MAX=500) 截到 500 字。這份資料用 JSON 回給前端,顯示時是否會被當成網頁內容執行,要看前端怎麼渲染,本棒沒有掃前端。全部結論都是讀程式碼得出的,沒有執行程式,也沒有實際攻擊驗證。
| 項目 | 值 |
|---|---|
| 工具 | Claude Code claude-security plugin 0.12.0,effort low,著重攻擊面 |
| run ID | wf_2d63cdaa-128 |
| 掃描的版本 | eee99445e,working tree 乾淨 |
| 範圍 | 7 檔/1181 行 |
| 研究員 | 派 2 位、回 2 位 |
| 候選發現 | 3 條 |
| 投票 | 三個檢查員對 3 條各投一票,9 票全數投出,沒有漏投 |
stamp verification.status |
verified(reason_kind 為空) |
| 耗時 | 約 45 分鐘 |
| 原始產物 | docs/features/security-scan-consolidated/runs/be/CLAUDE-SECURITY-20261108-180500/ |