你大概跑過 vLLM。但 vLLM 旁邊那層KV cache 加速,你未必查過它開的 port。
2026 年 10 月 7 日,JFrog Security Research 公布 LMCache 的 multiprocess 模式有一個未認證的遠端程式碼執行:CVE-2026-105192,CVSS 3.1 打 9.8。機制不複雜——它的 ZeroMQ 傳輸 port(預設 5555)沒有任何認證,而 device IPC descriptor 是用 pickle.loads 解開的。一條精心構造的消息,就能以 LMCache 進程的權限跑任意指令;官方 Docker 映像裡那個進程是 root。(source: JFrog JFSA-2026-001694382, LMCache Team)
影響範圍是 0.3.9 到 0.5.5,加上 0.5.6 的 release candidate 和修補前的 dev 分支。修補(把 pickle 換成 versioned MessagePack descriptor,PR #5644)在 10 月 12 日合併、0.5.6 已上線。(source: PyPI lmcache, GitHub GHSA-w5f2-9q9c-ff4h)
⚠️ 先講結論:如果你只是單機、用 LMCache 的 in-process 模式,預設不會開這個 port,基本不受影響。危險的是多機 / 分散式的 MP 模式——那需要你主動把 transport 綁到可路由的位址讓 peer 連進來,而那正是 9.8 這個分數的來源。
白話導讀
想像你有一間資料中心,裡面的機器要分享一份「計算快取」(KV cache,讓長 prompt 不用每次重算注意力)來互相提速。為了讓這些機器快速交換快取塊,它們開了一條內部專線,而且因為「這些都是自己人」,專線沒裝門禁。
pickle 就是那扇門的問題。pickle 是 Python 的「把一個物件凍結成一串 bytes、再還原成物件」的格式。它很方便,但它會執行資料:還原一個 pickle 的時候,它可以呼叫任意函式。所以一條「快取塊」如果裡面塞了一個會執行 os.system("...") 的物件,對方一收到、一解開,你的機器就跑了那段指令。
💡 重點:LMCache 的 transport 預設是綁 localhost(單機安全),但官方给的 K8s 範例把 transport 綁了所有介面,而且官方映像跑 root。你如果是照著官方範例部署的,要假設自己是被暴露的那一群。(source: LMCache Team)
漏洞到底怎麼走
LMCache 的 multiprocess(MP,也叫 distributed)模式會開一個 ZeroMQ ROUTER socket(預設 port 5555),讓 worker 進程註冊、分享 KV cache 塊。這個 socket 的「預期客戶」是同一叢集裡的兄弟 LMCache 進程,但沒有 CURVE、沒有 ZAP、沒有密碼、沒有 message 認證——什麼都沒有。(source: GitHub GHSA)
socket 上的訊息是 msgpack。其中 extension code 1 註冊給 DeviceIPCWrapper,會一路送進 lmcache/v1/platform/base/ipc_wrapper.py 的 DeviceIPCWrapper.Deserialize,而那裡呼叫的是 pickle.loads。
最糟的時機:這個 hook 是在 REGISTER_KV_CACHE 的參數還在建解碼、handler 還沒開始跑的時候就被觸發的(ext_hook in lmcache/v1/multiprocess/custom_types.py)。換句話說——你的指令在「訊息被正式處理」之前就已經跑完了。就算 handler 因為收到的東西型不對而報錯,那也發生在指令執行之後。(source: GitHub GHSA)
JFrog 的 PoC 簡化成三步:
- 啟動 MP 模式,transport 綁可路由位址:
python -m lmcache.v1.multiprocess.http_server \ --http-host 127.0.0.1 --http-port 8080 \ --host 0.0.0.0 --port 5555 ... - 構造一個
REGISTER_KV_CACHE訊息,cache slot 塞進一個 pickle:class EvilPayload: def __reduce__(self): return (os.system, ("id > /tmp/zmq_pwn 2>&1",)) evil = pickle.dumps(EvilPayload()) # 塞進 msgpack ext code 1 - 從另一個 ZMQ DEALER
send_multipart送過去。完事。官方映像跑 root,所以cat /tmp/zmq_pwn回的是uid=0(root)。(source: GitHub GHSA)
注意 PoC 的 __reduce__ 直接回 (os.system, (cmd,))——pickle 還原時等於幫你呼叫 os.system(cmd)。這不是「解析錯誤」,是格式本身的設計:pickle 天生能帶 code。
誰真的被暴露
| 部署方式 | 是否暴露 |
|---|---|
| 單機 in-process(IP)模式 | 否——port 不開啟 |
| MP 模式、transport 綁 localhost(預設) | 否——只有同一台 host 連得到 |
MP 模式、--host 0.0.0.0 / 可路由位址 |
是 |
| 多機 / 分散式(peer 要互連) | 是 |
| 照官方舊 K8s 範例部署(綁了所有介面) | 是(PR #5684 已改回 localhost) |
那 9.8 的分數是「可路由配置」的情形。預設單機、留預設 bind 的部署,其他機器連不到,理論上不在暴露集裡——但這不該是你能指望的事。(source: LMCache Team)
修:0.5.6 做了什麼
- 換掉 pickle:descriptor 改成 versioned、typed 的 MessagePack,先驗證再使用;device wrapper 的型別從一個固定的本機 allowlist 選,類名不再從網路輸入來,而且沒有 pickle fallback。ZeroMQ 和 gRPC 兩條路徑都覆蓋,畸形 descriptor 會被拒。(source: GitHub GHSA, LMCache Team)
- 官方映像改跑非 root(PR #5685)
- K8s 範例改回 localhost(PR #5684)
⚠️ 兩個還沒修的尾巴(0.5.6 只修了 device IPC 這條路):
- RPC transport 的認證——LMCache server 跟 MP coordinator、frontend 之間的 RPC 目前也不需要認證,計畫 0.5.7 才加。(source: LMCache Team)
- engine-driven 路徑的 pickle——CPU-data / retrieve-response 處理裡還用到 pickle,但那条路預設關閉、也從網路打不到,計畫 0.5.8 前移除。
wire format 變了:0.5.6 的 descriptor 格式跟舊版不相容,worker 和 server 要一起升級,不能一新一舊混著跑。(source: GitHub GHSA)
怎麼查你有沒有中招
# 1. 你的 LMCache 版本
pip show lmcache | grep -E '^(Name|Version)'
# 0.3.9 <= v < 0.5.6 且在跑 MP 模式 → 在影響範圍
# 2. transport 有沒有綁可路由位址(關鍵)
sudo ss -ltnp | grep ':5555'
# 0.0.0.0:5555 或 10.x / 192.168.x:5555 = 暴露
# 127.0.0.1:5555 = 只有本機連得到
# 3. 啟動參數裡有沒有 --host 0.0.0.0
ps -eo args | grep -o 'lmcache[^\ ]*multiprocess[^\ ]* --host [^ ]*'
# 4. K8s 的話,看你照的範例 bind 了什麼
grep -rn '0.0.0.0\|host:' <你的 lmcache deployment yaml>
判斷是否被入侵(LMCache 團隊的原話要照做):如果 transport port 曾經被不受信網路到達過,先當 host 已經被入侵處理。因為指令是在訊息被處理之前跑的,所以「log 裡看到一個被拒 / 畸形的請求」不代表攻擊失敗。(source: LMCache Team)
- 查 transport port 有無來自奇怪來源的連線
- 查 LMCache 進程有沒有長出不該有的子進程、shell、外連
- 把受影响容器能碰到的 credential / secret 全部 rotate,再用乾淨映像重部署
If You're Stuck
- 我不確定自己有沒有開 MP 模式 → 單機用
lmcache当 vLLM 的 in-process 加速、沒有獨立起multiprocess.http_server的,就是 IP 模式,port 不開啟、不受影響。你如果手動python -m lmcache.v1.multiprocess.http_server過,就是 MP 模式 ss -ltnp看不到 :5555 但不安 → 確認你的 LMCache 是不是真的在跑 MP;in-process 模式不會開這個 port,看不到是正常的- 多機部署想先降風險再升級 → 把 transport 收回 localhost(或只信任的叢集內網)、用 network policy 擋 5555、把進程降成 non-root(0.5.6 官方映像已內建)
- 升級 0.5.6 後 worker 連不上 server → wire format 變了,所有 worker 跟 server 要同時升,混版本不支援
- 我在 0.5.6 之後但還想確認 pickle 沒殘留 → 0.5.6 修的是 device IPC 這條路;engine-driven 那條(預設關、網路打不到)要到 0.5.8 才清,短期不用管,但要知道它還在
- 我想確認 0.5.6 真的上線了 →
pip install lmcache==0.5.6或直接看 PyPI;NVD 紀錄也指向 v0.5.6 release
跟之前幾篇的關係
這個 blog 的 security 系列已經出現過很多次同一個根因:
| 日期 | 主題 | 共同點 |
|---|---|---|
| 11/1 | LMDeploy SSRF (CVE-2026-33626) | 推論伺服器無認證 → 從裡面打出去 |
| 11/3 | Langflow 33 CVE | Agent builder 預設 auto-login → 不用登入就 RCE |
| 11/4 | Node-RED (CVE-2026-58115) | IoT 工具預設無認證 → 一筆請求拿 shell |
| 11/6 | LMCache (CVE-2026-105192) | 推論加速層的內部 transport 無認證 → 一條訊息 pickle RCE |
這次多一個層次:它不是「對外的那個 API」,是機器之間那條內部專線。你通常不會把內部專線當攻擊面,但「預設自己人」+「pickle 這種會執行資料的格式」= 攻擊者只要進到你的 trusted network,就有一条路直達 root。(source: JFrog)
🔒 通用教訓:任何「機器之間快速交換資料」的內部 port(ZMQ、gRPC、Redis、etcd、consul)都該當對外的門來保護——加認證、收 bind、放 firewall。內部不代表安全,只代表「攻擊者要先花一步進來」。
Further Reading
- Inference server SSRF: 12 小時被利用 — 推論伺服器的「無認證代理」模式,同一類「預設不對外要憑證」
- Langflow 33 CVE 同批曝光 — Agent builder 的 auto-login 預設
- Node-RED CVSS 10 漏洞 — 一筆 HTTP 請求拿 shell 的另一個版本
- 一天兩個 kernel 致命漏洞 — 推論 host 底層 OS 的攻擊面
Sources
- LMCache Team: An Update on CVE-2026-105192(2026-10-13,官方:漏洞、修補、暴露面、受入侵檢查、後續工作)
- JFrog Security Research: JFSA-2026-001694382 — LMCache Unauthenticated RCE via Pickle Deserialization(CVSS 9.8、PoC、影響版本)
- GitHub Security Advisory GHSA-w5f2-9q9c-ff4h(affected
>=0.3.9 <0.5.6、fixed0.5.6、完整 PoC、workaround、CVSS vector) - NVD: CVE-2026-105192(2026-10-07,fixed version 指向 v0.5.6 release)
- PyPI: lmcache(0.5.6 已上線)
- Fix PRs(pickle → MessagePack)、#5684(K8s 綁 localhost)、#5685(非 root 映像)
