你把模型權重下載到自己的 GPU 主機,再用 vLLM 或 SGLang 開成 API,資料確實不必先送往雲端;但本機 LLM 安全不會因此自動成立。當模型輸出經由有漏洞的 parser/dispatcher 形成執行路徑,而 runtime 又位於高權限容器時,原本只是文字的 token 才可能進一步影響服務或主機。
這篇專為第一次維運推論服務的讀者寫:不公開攻擊 payload,也不假裝重現主機接管;我們會把已證實的漏洞、仍屬假說的情境分開,再用 vLLM/SGLang 的現行官方控制、Docker 最小權限與無破壞測例,做出一套能重跑的硬化流程。
先記住唯一的核心式:本機 LLM 安全=把模型輸出當不可信輸入 × 把推論 runtime 當低權限服務。前半段避免 token 被誤當指令;後半段在 parser 出錯時,把 process 可取得的檔案、網路與主機權限壓到最低。容器仍共用 host kernel/GPU driver,不能保證阻止 escape。
先說結論:你真正要守的是「解讀文字的程式」
- 模型權重不必藏惡意程式,風險仍可能存在:外部 messages/tools 先經 chat template 形成模型輸入;生成 token 後,reasoning/tool-call parser 與應用程式 dispatcher 才處理輸出。任何一層把不可信字串交給危險函式,都可能成為執行路徑。
- 已有具體漏洞,但證據邊界很重要:官方 advisory 證明的是特定舊版與設定下的執行路徑,並未證明模型曾自主規劃與利用該漏洞;它也不等於所有模型、所有 parser 或 SGLang 都會中招。
- 容器不是安全貼紙:非 root、唯讀 root filesystem、能力移除、私網與資源上限要一起用;GPU container 仍共用 host kernel 與驅動。
- API key 不是整台伺服器的防火牆:vLLM 官方明說內建 key 並不保護所有路徑;兩套引擎都應放在只開放必要 endpoint 的反向代理後面。
- 測試不需要攻擊主機:先用截斷 JSON、未知工具與錯誤型別驗證 gateway 會拒絕,再在隔離環境對真正的引擎 parser 做有上限的穩定性、health 與告警測試。
本機 LLM 安全為什麼會從 token 一路碰到主機?
把 LLM 想成一位被綁在椅子上、只能說話的天才。外部 messages、角色與 tools 先由 chat template 整理成模型輸入;模型生成 token 後,推論引擎再把它還原成文字,reasoning/tool-call parser 抽出結構、工具名稱與參數,最後應用程式才決定是否真的呼叫檔案、網路或 Shell。token 是它說出的音節,本身沒有 Unix 權限;問題出在旁邊的翻譯員與執行員。

2025 年公開的 vLLM CVE-2025-9141 官方公告提供一個清楚案例:受影響範圍是 >=0.10.0,<0.10.1.1,要同時啟用自動工具選擇、指定 qwen3_coder parser,並遇到未被辨認的參數型別,才會走到 parser 內不安全的 Python eval()。修補版是 0.10.1.1。
這個漏洞證明「模型產生的工具參數 → parser → 程式碼執行」在特定舊版與設定下是真實路徑;但公告描述的攻擊者是能誘導特製工具參數的已認證使用者,並沒有證明當代模型曾自主找到漏洞、規劃 payload 再接管主機。提出這個威脅模型的 Boyd Kane 分析也明確保留不確定性。正確態度不是恐慌,而是把「可能的路徑」變成可驗收的邊界。
先分清楚 4 種入口,別把所有風險都叫「惡意 token」
- 外部請求:使用者提供 prompt、圖片、音訊、影片或 URL;歷史上多媒體 decoder 與 URL fetcher 都出現過 RCE、SSRF 或資源耗盡問題。
- 模型供應鏈:模型 repository 可能帶自訂 Python、template、tokenizer 或舊格式權重。即使
trust_remote_code關閉,也不能把未知模型與可寫 cache 當成可信程式庫。 - 模型輸出:token 經 reasoning/tool parser 轉成結構化資料;CVE-2025-9141 就位在這一段。
- 控制面與工具:分散式通訊、LoRA/權重更新、profiler、gRPC、插件與真正的工具 executor,往往比聊天 endpoint 更有權力,也更不該公開。
換句話說,「只跑純文字模型」可以縮小多媒體入口,卻沒有消除模型供應鏈、parser、管理 endpoint 與容器權限。若你還在選引擎,可先看 LLM 推論引擎完整比較;硬化前也要依 本機 LLM 顯存指南估算資源,避免把記憶體不足誤判成攻擊或 parser crash。
本機 LLM 安全硬化 7 層:從版本到復原
1. 版本層:鎖 release、image digest 與模型 commit
截至 2026 年 8 月 28 日,兩個專案最新穩定 release 分別是 vLLM v0.28.0(8 月 26 日)與 SGLang v0.5.18(8 月 22 日)。這只是查核快照,不是「最新版已無漏洞」的保證;上線前仍要重看 release、專案 advisory、CVE 與實際 image digest。
docker pull vllm/vllm-openai:v0.28.0
docker image inspect vllm/vllm-openai:v0.28.0 \
--format '{{index .RepoDigests 0}}' > vllm-image-digest.txt
# 產出必須是 image@sha256:…;Production 改用這個不可變引用
grep -Eq '^(docker\.io/)?vllm/vllm-openai@sha256:[0-9a-f]{64}$' \
vllm-image-digest.txt
SGLang 官方 image 依硬體有不同 tag;先從該版安裝文件選對 runtime,再用同一個 RepoDigests 方法保存收據。模型也要固定 repository revision/commit,先在下載區完成掃描,再以唯讀 mount 提供給推論服務。模型升級與退役的回歸方法,可接著用 Model Retirement Eval管理。
2. 容器層:非 root、唯讀、零額外 capability
vLLM 官方 CUDA image 預設仍以 root 執行,但提供 UID 2000 供 --user 2000:0 使用。下面是純文字模型、host loopback Lab 的起始範本,不是每台機器都能直接照搬;先把 GPU-UUID-HERE、模型路徑與剛才保存的 digest 換掉,再在拋棄式主機做相容性測試。
docker run --detach --name vllm-contained --init \
--gpus 'device=GPU-UUID-HERE' \
--env NVIDIA_DRIVER_CAPABILITIES=compute,utility \
--env VLLM_MAX_N_SEQUENCES=64 \
--env VLLM_MEDIA_URL_ALLOW_REDIRECTS=0 \
--env VLLM_PLUGINS= \
--user 2000:0 \
--read-only \
--mount type=bind,src=/srv/models/verified-model,dst=/models/model,readonly \
--tmpfs /home/vllm/.cache:rw,nosuid,nodev,size=16g,uid=2000,gid=0,mode=0770 \
--tmpfs /tmp:rw,nosuid,nodev,size=1g,mode=1777 \
--security-opt no-new-privileges=true \
--security-opt seccomp=builtin \
--cap-drop ALL \
--pids-limit 512 --memory 64g --memory-swap 64g --cpus 16 \
--shm-size 16g \
--publish 127.0.0.1:8000:8000 \
vllm/vllm-openai@sha256:REPLACE_WITH_VERIFIED_DIGEST \
--model /models/model --host 0.0.0.0 --port 8000 \
--no-trust-remote-code --disable-fastapi-docs
64g、16g 與 CPU 數只是示例,要按模型與批次壓測調整;唯讀、ephemeral cache、seccomp 與 cap-drop 的完整組合也必須對該 digest 做啟動回歸,確認所有必要寫入路徑都可用。用指定 shared-memory 容量,是為了避免直接共用 host IPC;不要因啟動失敗就加入 --privileged、host PID/network/IPC,或掛載 /var/run/docker.sock。
--user 2000:0 只代表 container 內非 root,不等於 Docker daemon 已用 rootless mode。CPU、host RAM 與 PID 上限也不會限制 GPU VRAM/算力;指定 GPU UUID 只縮小裝置可見範圍。SGLang image 的 Unix UID 與可寫路徑要逐版回讀,不能照抄 vLLM 的 2000:0。
這個 Lab 的 proxy 要拒絕所有遠端媒體欄位;若業務真的需要多模態 URL,才在 vLLM 命令加入精確的 --allowed-media-domains media.example.com。不要把「旗標未設定」解讀成遠端媒體已被拒絕。
3. 網路層:入口只到 proxy,出口預設拒絕
上面的 127.0.0.1:8000:8000 只把 host-published port 綁在 loopback;同一 Docker network 的 container 仍可能直連服務,default bridge 出口也未受限。Production 應把 proxy 與引擎放在專用 backend network,由 nginx、Envoy 或 Gateway 明確 allowlist 需要的聊天/completion 路徑,再做身份驗證、request size、rate limit 與日誌。vLLM 的 官方安全指南明說 --api-key 不保護整個 server,分散式與 KV 通訊也應只存在隔離網路。
SGLang v0.5.18 的 HTTP 預設 host 是 127.0.0.1;若在 container 內為了 port forwarding 改成 0.0.0.0,host 端仍要只綁 loopback 或私網。內建 --api-key 與 --admin-api-key 只能當內層控制;由於該版 /server_info 原始碼仍回傳完整啟動設定,代理層應拒絕這個路徑,以及權重更新、LoRA-from-tensors、profiler、dumper 等控制面,只放行實際使用的 inference API。
完全離線的 parser 單元測試可直接用 --network none,Docker 此時只建立 loopback。需要提供 API 時,則用 default-deny firewall/NetworkPolicy,逐條開放 proxy ingress;Kubernetes 的 NetworkPolicy 只有在 CNI 實際支援並執行時才有效。模型先在另一個受控流程下載,推論 container 不帶 Hugging Face token,也不需要任意網路出口。SGLang v0.5.18 的 SGLANG_USE_PICKLE_IPC 仍預設為 true;CERT/CC 建議在相容時設為 false,但分散式功能要先在固定版本做回歸。
4. Parser 層:不用的 tool 與自訂程式一律不開
如果服務只做文字 completion,就不要加入 vLLM 的 --enable-auto-tool-choice、--tool-call-parser 或 --tool-server;SGLang 維持 --tool-call-parser、--tool-server 未設定,並確認 container 沒有意外繼承 EXA_API_KEY。這不是因為功能一定危險,而是每多一個 parser、插件或執行器,就多一段要更新與測試的可信程式碼。
真的要用工具時,把推論引擎與 executor 拆成兩個服務:引擎只能回傳結構化資料;gateway 只接受固定工具名稱與 JSON schema;executor 使用參數陣列,不把字串拼進 Shell,並在另一個低權限 sandbox 執行。這就是 AI Agent Harness 的 guardrail 概念;實作 dispatcher 前也可先看 最小 Agent Harness 偽代碼。
5. 資料層:模型、cache、媒體與 Secret 分開
- 模型:固定來源與 commit,預先下載,唯讀掛載;沒有明確需求就不開 remote code。
- cache:視為可信程式資產,只讓服務帳號寫;不要與陌生使用者或多租戶共用。發生可疑 process RCE 後要銷毀 cache,不能把舊 volume 再接回乾淨容器。
- 遠端媒體:vLLM 使用精確的
--allowed-media-domains,並關閉 redirect;SGLang v0.5.18 也提供 hostname allowlist 與檔案大小上限,但未設定時仍允許任意 domain 的 HTTP(S) 媒體。gateway 還要拒絕非預期 local path/data:URL;egress policy 只負責限制外連。 - Secret:只交給真正需要的 proxy/executor,不放進模型 prompt、掛載的 home、crash dump 或高詳細度 request log。完整方法可參考 AI Agent 密鑰安全指南。
6. 資源與觀測層:限制 request,也限制失敗方式
資源耗盡不必拿到 Shell,也能讓服務失效。vLLM 官方文件目前列出 VLLM_MAX_N_SEQUENCES 預設 16,384,並以 64 或 128 作為公開服務的調整示例;它只限制 completions/chat completions 的 n,不取代 body size、prompt token、concurrency 與 rate limit。不要盲抄數字,要依正常批次、延遲與 GPU 記憶體壓測。SGLang 可用 --max-running-requests、--max-queued-requests 與 --max-total-tokens 管理排程/token memory pool,再以 SGLANG_MAX_NEW_TOKENS_LIMIT 限制單次輸出;disaggregation mode 需另查哪些 queue 參數會被忽略。
至少監看 parser 拒絕率、未知 tool name、4xx/5xx、process restart、GPU OOM、延遲與 queue depth。SGLang 的 request logging 預設關閉;若為除錯開啟,要同時設定 --log-requests-level 0 才是 metadata-only,不要沿用會記部分 input/output 的預設 level 2。watchdog 能讓卡住的 process 結束,外部 supervisor 才負責退避重啟;兩者不能混為一談。
7. 復原層:準備「重建」,不要在可疑容器裡修補
保存 image digest、模型 commit、啟動參數、proxy route 與測試收據。若 parser 異常、來源不明的 tool call 或控制面存取觸發事件,先隔離節點、保全必要日誌、輪替可能接觸到的 key、銷毀可疑 cache,再從已驗證 digest 與空白 cache 重建。獨立 GPU node 可以縮小資料與憑證的爆炸半徑,但 container、MIG 或 GPU time-slicing 都不能替 host kernel/driver 提供完整隔離。

無破壞測試:先驗證 gateway fail closed,引擎 parser 另做隔離測試
不要拿 Production GPU 主機做漏洞重現。最安全的第一輪,是在無 GPU、無 Secret、--network none、唯讀 source tree 的小 container,直接測你自己的 gateway/dispatcher。以下 Python 解析 raw JSON,以 spy 取代真實工具,驗證合法 call 會被路由,而截斷、未知、錯型別、非 object 與多餘欄位都會被拒絕:
import json
CASES = [
('{"name":"lookup_order","arguments":{"order_id":"A-100"}}', True),
('{"name":"lookup_order","arguments":{"order_id":', False),
('{"name":"unknown_tool","arguments":{}}', False),
('{"name":"lookup_order","arguments":{"order_id":["A-100"]}}', False),
('[]', False),
('{"name":"lookup_order","arguments":{"order_id":"A-100"},"extra":1}', False),
]
def validate_tool_call(call):
if not isinstance(call, dict):
return False
if set(call) != {"name", "arguments"}:
return False
if call["name"] != "lookup_order":
return False
args = call["arguments"]
return (isinstance(args, dict)
and set(args) == {"order_id"}
and isinstance(args["order_id"], str)
and len(args["order_id"]) <= 64)
spy_calls = []
def dispatch(raw):
try:
call = json.loads(raw)
except json.JSONDecodeError:
return False
if not validate_tool_call(call):
return False
spy_calls.append(call) # 只記錄,不執行真實工具
return True
for raw, expected in CASES:
if dispatch(raw) is not expected:
raise SystemExit(f"FAIL: {raw!r}")
if len(spy_calls) != 1:
raise SystemExit(f"FAIL: spy_calls={len(spy_calls)}")
print("PASS: 1 routed to spy, 5 rejected, 0 real tools executed")
這不是 vLLM 或 SGLang 漏洞重現,也沒有測到引擎 crash handling;它只驗收「引擎回傳之後,gateway 不會把奇怪資料送進真實工具」。接著才在隔離 container 對部署版本的 parser 做 upstream unit test 或私有 fuzz:使用空字串、截斷 JSON、重複 delimiter、引號/括號不平衡、Unicode normalization、串流 chunk 邊界、未知欄位與有上限的 nesting;危險 sink 全部換成會立刻失敗並計數的 stub。
你要保存的 Pass/Fail 收據
docker inspect vllm-contained --format \
'user={{.Config.User}} privileged={{.HostConfig.Privileged}} readonly={{.HostConfig.ReadonlyRootfs}} network={{.HostConfig.NetworkMode}} ipc={{.HostConfig.IpcMode}} pid={{.HostConfig.PidMode}} caps={{json .HostConfig.CapDrop}} security={{json .HostConfig.SecurityOpt}} memory={{.HostConfig.Memory}} pids={{.HostConfig.PidsLimit}} ports={{json .HostConfig.PortBindings}} mounts={{json .Mounts}} devices={{json .HostConfig.DeviceRequests}}'
docker exec vllm-contained sh -lc \
'id -u; test ! -e /var/run/docker.sock; test -w /tmp; test -w /home/vllm/.cache'
curl --fail http://127.0.0.1:8000/health
backend health 通過後,還要從 proxy 外側做兩個負向測試:未帶身份呼叫允許的 inference 路徑應得到 401/403;呼叫刻意封鎖的 /version 或管理路徑應得到 proxy 產生的 403/404。這兩項才證明 route allowlist 真的生效,不能由 docker inspect 代替。
- 隔離:UID 不是 0、root filesystem 為唯讀、capabilities 為
ALLdrop、沒有 Docker socket,host 只在 loopback/私網監聽。 - gateway 正確性:合法工具 call 只抵達 spy;截斷、未知、錯型別、非 object 與多餘欄位全部被拒絕。
- 引擎穩定性:只有在同版本、同 parser 的 bounded integration test 跑完後,才能記錄「無 uncaught exception/hang,測後 health 通過」。
- 可觀測性:只有實際接上 staging log/alert 後,才把 request ID、parser/schema 名稱與錯誤類別標為 Pass;不要記完整 prompt、output 或 Secret。
- 復原:在 Lab 刻意停止 process,驗證 supervisor 的退避恢復與 proxy 503;沒有演練就保持未測。
若任何測例真的碰到 eval、pickle、Shell、subprocess、檔案或網路 sink,就停止公開測試、保存最小 reproducer,依專案的私有安全通報管道回報。公開文章只需留下 mutation 類別與修補後 regression hash,不需要散布可工作的 payload。
上線前 12 項本機 LLM 安全檢查表
- 重新確認 vLLM/SGLang,以及 Docker Engine、containerd/runc、Linux kernel、NVIDIA driver/Container Toolkit 的安全更新。
- image 以 digest 固定;模型以 commit/revision 固定,兩者都有來源收據。
- 推論 process 非 root;root filesystem 與模型 mount 唯讀。
no-new-privileges、default seccomp、cap-drop ALL與 CPU/RAM/PID 上限已回讀。- 沒有 privileged、Docker socket、host PID、host network 或 host IPC。
- 外部只到反向代理;proxy 只 allowlist 必要 inference endpoint。
- 分散式、KV、gRPC 與管理 port 不可從不可信網路抵達。
- 不用的 parser、tool server、plugin、remote code 與 dynamic LoRA 不啟用;proxy 另行拒絕 profiler、LoRA、權重更新與其他控制面路徑。
- 模型 container 沒有下載 token、雲端憑證與任意 egress;媒體 domain、來源型別與大小有限制。
- 合法、截斷、未知、錯誤型別與串流邊界測例都留下 Pass/Fail。
- parser error、restart、OOM、queue 與拒絕率有告警,log 不保存敏感正文。
- 有隔離、輪替、銷毀 cache、重建與回滾流程;至少演練一次,不只寫在文件裡。
常見問題 FAQ
1. 只下載 Safetensors,就能排除本機 LLM 風險嗎?
不能。Safetensors 可縮小權重反序列化風險,但 tokenizer、template、自訂模型程式、cache、parser、媒體 decoder、插件與控制面仍是不同入口。
2. 一般 prompt 能直接讓 vLLM/SGLang 跑 Shell 嗎?
在正常資料流中,純文字 prompt 只是輸入資料。要讓主機執行 Shell,仍須存在額外執行路徑,例如 parser/decoder 漏洞、惡意模型資產、危險插件、可抵達的控制面,或應用程式盲目執行 tool call。這是架構條件,不是對所有版本零漏洞的保證。
3. CVE-2025-9141 代表 SGLang 也有同一個問題嗎?
不代表。該公告針對特定舊版 vLLM 的 Qwen3-Coder tool parser。SGLang 有自己的漏洞紀錄與 parser 實作,必須逐項查核,不能跨專案類推。
4. 用 Docker 跑,就算 parser RCE 也碰不到 host 嗎?
不能這樣保證。容器能限制 process 權限與 mount,但仍共用 host kernel、GPU driver 與部分 runtime。特權模式、Docker socket、host namespace 或過寬 mount 會大幅放大後果。
5. 只綁 127.0.0.1 就安全了嗎?
只解決一部分進站問題。同機其他 process 仍可能連線,容器也可能對外發 request;還需要身份、endpoint allowlist、egress、最小權限與更新。
6. vLLM 的 –api-key 可以取代 reverse proxy 嗎?
不可以。vLLM 官方文件明確指出它不涵蓋整個 HTTP server;proxy 還要負責 endpoint allowlist、額外身份驗證、rate limit、request validation 與日誌。
7. 可以在 Production 直接跑畸形輸出 fuzz 嗎?
不應該。先在無憑證、無網路、低資源、可拋棄的 parser test container 做;Production 只跑已知無破壞、已設上限的 regression case。
8. 獨立 GPU 主機是必要條件嗎?
依風險而定,但對不可信多租戶或會執行工具的服務很值得。它能把日常檔案、憑證與其他 workload 移出爆炸半徑;仍要做 container、網路、更新與監控,不能把獨立主機當成單一防線。
給新手的 5 個重點
- token 是資料;parser、dispatcher 與 executor 才把資料變成權限。
- 已知漏洞證明路徑存在,不等於模型已經自主接管主機。
- 版本固定只能回答「你跑哪一版」;最小權限回答「出錯時能做多大」。
- 內建 API key 是一層,不是 proxy、私網與 egress policy 的替代品。
- 先做 4 個無害 case:合法、截斷、未知工具、錯誤型別;能穩定拒絕再談更進階 fuzz。
接著閱讀
左右滑動查看更多推薦
結語:先讓文字永遠只是資料
回到開頭的式子:把模型輸出當不可信輸入 × 把推論 runtime 當低權限服務。第一項用嚴格 schema、工具 allowlist 與無害 parser regression 阻止 token 變成命令;第二項用非 root、唯讀、私網、資源上限與可重建映像,限制任何軟體錯誤的後果。
今天不要先追求「絕對安全」:先在拋棄式主機固定一個 image digest,關掉不用的 tool parser,跑完合法/截斷/未知/錯誤型別四個 case,再保存 docker inspect 與 health 收據。想把這套檢查擴充成可重跑的 AI 工程流程,可繼續瀏覽 AlphaLab AI 專區,或到 AlphaLab 課程建立自己的安全 Harness。






