找到 3 個問題,都是低風險,而且都要先控制一台已註冊的 Agent 才打得到。最值得修的是第 3 條:被控制的 Agent 可以冒用主系統任何一個真實帳號的名字寫稽核紀錄。
Agent 裝在客戶機器上,會透過 POST /agents/events 回報自己的狀態、錯誤日誌、升級進度與結果,以及主控台上有人做了什麼(寫進主系統稽核)。這一棒假設 Agent 已經被人控制,檢查主系統收下這些回報時信了多少:能不能改到不該改的欄位、冒用別人名義、謊報升級成功、或把主系統灌爆。
範圍 10 個檔、約 870 行(清單見卡片 CM-3715)。
| # | 這是什麼問題 | 出事會怎樣 | 要先有什麼才打得到 | 在哪裡 | 怎麼修 | 嚴重度 |
|---|---|---|---|---|---|---|
| 1 | Agent 送的錯誤日誌,「訊息」如果不是文字(例如一個陣列),就不會被截到 8KB 上限,整包原樣存進資料庫 | 資料庫被塞大、日誌查詢頁與診斷包匯出讀到很大的列時吃記憶體變慢 | 控制一台已註冊的 Agent(拿得到它的通行證與簽章金鑰) | domain/support/service/agent_log_domain_service.py:59-67 |
先轉成文字再截長度 | 低 |
| 2 | 事件去重用的「發生時間」與「工單編號」是 Agent 自己填的,長度沒限制,整串存進 Redis(主系統的快取伺服器)一天 | Redis 記憶體被灌滿;Redis 也存著所有 Agent 的「防重放」紀錄,滿了會影響其他 Agent 連線 | 控制一台已註冊的 Agent;Redis 沒設記憶體上限 | app/remote_agent/service/agent_event_dispatcher.py:92、:119 |
組去重鍵前限制長度與格式,或改成雜湊值 | 低 |
| 3 | 主控台稽核紀錄的「操作者」是 Agent 自己報的名字,主系統沒核對這個人真的有登入過那台 Agent | 稽核報表可以出現「某管理員在這台 Agent 下載了檔案」但其實不是他;還能偽造本機管理員登入觸發告警信 | 控制一台已註冊、屬於該客戶的 Agent | core/plugins/_agent_console_audit.py:97-105 |
Agent 每筆附主控台登入票的編號,主系統核對是不是發給這台、這個人的;核不到就標「Agent 自報」不換使用者身分 | 低 |
為什麼都是「低」不是「中」:三條的共同前提都是「Agent 已經被控制」。被控的 Agent 本來就能在它自己那台機器上做很多事;這三條的後果是讓主系統變慢或稽核紀錄失真,不會讓攻擊者讀到別的客戶資料,也拿不到主系統帳號權限。
:60);如果送一個陣列或物件,就跳過截斷,在 :67 被轉成文字整包寫進 public.agent_logs.message。domain/support/service/agent_log_domain_service.py:59-67(to_rows)。to_rows 裡把 message 先轉成 str(或非文字直接丟掉),再套 message_max 截斷。:60 的 isinstance(message, str) 判斷與 :67 的 str(message) 確實如描述。:92),處理成功後存進 Redis 24 小時(:119)。工單編號與發生時間都來自 Agent 送的內容,協定只檢查「是字串」,沒限長度。agent_status、agent_logs、console_events 三種事件的處理都沒檢查這兩欄(升級事件有用 clean_job_id 檢查工單編號、event_time 解析時間,但要等處理成功才寫鍵,格式錯會先 400,所以升級事件不受影響)。agent_status、附超長的發生時間,每次換值就多一個大鍵,一天內不消失,Redis 記憶體可被填滿;Redis 同時存著所有 Agent 的防重放紀錄(DPoP,就是防止別人把攔到的請求重送一次的機制)。app/remote_agent/service/agent_event_dispatcher.py:92(組鍵)、:119(寫入)。parse 或 dispatch 組鍵前,要求發生時間是 64 字以內且 fromisoformat 解得開的時間、工單編號符合 _JOB_ID_RE;或把這兩段做雜湊後再組鍵。console_events 給主系統寫稽核。每筆的 actor(操作者帳號)是 Agent 自己填的,主系統在 :102 直接把它當成這筆稽核的使用者,還去查真實使用者的暱稱(:148)。中間沒有核對「這個帳號真的拿過主系統發給這台 Agent 的主控台登入票」。:152)。core/plugins/_agent_console_audit.py:97-105(_write)、:148(查暱稱)。actor,才換成那個使用者寫稽核。核不到時不換使用者身分,改在稽核列標「Agent 自報::98-102 確實直接取 event.get("actor") 設進 user context,沒有任何票據核對。| # | 重點 | 人工查證結果 |
|---|---|---|
| 1 | 去重鍵用 Agent 自報的 at、job_id,換值就能無限重送;有沒有頻率與大小上限;handler 冪等夠不夠 |
部分成立。身分與租戶只取通行證(agent_event_dispatcher.py:57-68),沒問題。換 at 確實能繞過去重重送,但各 handler 有自己的冪等:日誌以 (agent_uid, agent_log_id) 唯一、主控台稽核以已寫入的 at 去重、升級事件以 at 新舊判斷、狀態只寫有變動的值,所以重送不會造成重複資料。本棒範圍內沒看到每台 Agent 的頻率上限(可能在通行證守門或前門,不在 scope)。長度沒上限的部分就是第 2 條。 |
| 2 | _agent_status.py 白名單鍵寫回 remote_agents,有沒有能改狀態、租戶、憑證、enabled 的鍵;browser_port 能不能把任意網址塞進 CSP |
不成立。:46-49 先濾成 AGENT_STATUS_KEYS 白名單內的鍵,再交給套件各自的值域檢查;browser_port 只收 1~65535 的整數(:37-41)。接縫 core/plugins/_agent_origins.py:28-35 用的主機名取自 base_url、埠是整數,組好後再用 _SAFE_ORIGIN 正規式驗過才寫進 CSP,塞不進任意網址。白名單內各鍵的實際值域在 jedi_remote_agent 套件,不在 scope,沒逐鍵深追。 |
| 3 | _agent_logs.py+agent_log_domain_service.py:訊息長度、筆數、等級白名單;文字之後顯示在日誌查詢頁 |
部分成立。筆數上限 500(:52)、等級只收白名單(:56)、其他欄位有截長度(:65)。訊息長度有一個洞,就是第 1 條。顯示到日誌查詢頁時是否跳脫屬前端,不在 scope。 |
| 4 | _finish 用 Agent 自報版本與成敗改更新中心結果:偽造「升級成功」會不會讓更新中心停止重試 |
成立但屬設計前提。agent_update_event_handlers.py:188-200 確實直接用 Agent 自報的版本與成敗呼叫 record_result;被控的 Agent 謊報成功,更新中心會把它自己那一台記成升完。但它只能謊報自己,不能替別台 Agent 報;是否會讓整批「已全部升完」取決於 record_result 與 tick(在 jedi_remote_agent/更新中心,不在 scope)。主系統沒有別的管道得知 Agent 真實版本以外的心跳版號,這是信任模型的限制,不算本棒新增的洞,工具也沒報。建議首腦評估是否要用下一次心跳回報的版本二次確認。 |
| 5 | save_log 附件大小上限;檔名由系統產;clean_job_id 白名單 |
不成立。:224 先擋 10MB 上限、:226 檢查 gzip 檔頭、:223 用 ^[A-Za-z0-9._:-]{1,64}$ 白名單檢查工單編號,檔名由系統組(:243),同一工單重傳回第一次那份(:240)。注意:大小檢查發生在整包已讀進記憶體之後,讀取上限在 route(不在 scope)。 |
| 6 | _agent_console_audit.py 把 Agent 自報的 actor 寫進稽核:能不能以真實帳號名義寫稽核 |
成立,就是第 3 條。 |
| 7 | _agent_error_tracking.py 註冊回應附帶錯誤追蹤設定:送給 Agent 的是不是只有它該拿的那一份 |
不成立。:16-26 只回 enabled、environment 與 Agent 專用的 SENTRY_AGENT_DSN,不帶後端自己的 SENTRY_DSN;:46 只在註冊端點且 200 時才補。所有 Agent 拿到同一份 Agent 專用連線設定(不分租戶),這是錯誤追蹤的連線位址,本身不是可以讀資料的密碼。 |
jedi_remote_agent、jedi_agent_protocol)與前門、通行證守門只追到接縫,沒深掃;掃描是讀程式碼推論,沒有實際執行任何攻擊。| 項目 | 值 |
|---|---|
| run ID | wf_80d1e3fa-d85 |
| 掃描版本 | 31345fbe0c9c(工作目錄乾淨) |
| 設定 | scoped,10 檔,effort low,focus attack-surface |
| verification.status | verified |
| reason_kind | 無(null) |
| candidates | 3(去重後 3) |
| panel_votes | 9(三個檢查員對 3 條各投一票,9 票全數投出,沒有漏投) |
| researchers_dispatched/returned | 2/2 |
| 耗時 | 約 33 分鐘 |
| 原始報告歸檔 | docs/features/security-scan-consolidated/runs/be/CLAUDE-SECURITY-20261108-171721/ |