CVE-2026-22778 is a CVSS 9.8 unauthenticated RCE reachable by posting a crafted video URL to any vLLM server running a video-capable multimodal model, and it was live in released builds for 302 days. Patching to 0.14.1 is not sufficient: a June 2026 advisory found the same error-message leak still present in five other response paths, which puts the real floor at 0.24.0 with 0.26.0 current. The --api-key flag only guards routes under the /v1 prefix, and CVE-2026-48746 (CVSS 9.1) made even that bypassable for 853 days through a Host header, so authentication belongs at a reverse proxy rather than at vLLM. vLLM binds every interface when --host is unset, and CVE-2026-27893 shows an explicit --trust-remote-code=False being silently overridden by two model files. Fix network placement first, then the flags.
CVE-2026-22778 carries a CVSS of 9.8 and needs no credentials at all: one HTTP POST containing a crafted video_url to a vLLM server running any video-capable multimodal model. The chain leaks a heap address out of an error message to defeat ASLR, then abuses a JPEG2000 channel-definition box in the bundled video decoder to remap the luma channel into the chroma buffer. That writes 9,600 bytes into a 2,400-byte allocation, which is 7,200 bytes past the end and directly on top of adjacent function pointers. Those pointers were redirected to libc system().
vLLM security hardening is now a Day-2 requirement rather than a nice-to-have, because the arithmetic on exposure is unpleasant. The vulnerable code shipped in v0.8.3 on 2025-04-06. The advisory landed on 2026-02-02. That is 302 days during which every released build carried a pre-auth remote code execution path, reachable by anyone who could open a TCP connection to the API port. And the API port, by default, listens on every interface.
This is not an advisory summary. It covers the mechanism behind the headline 2026 CVEs, three separately-verified reasons the standard mitigations fail on a stock deployment, and a hardening checklist a platform team can execute: network placement, where authentication belongs, multimodal input handling, resource limits, the exact flags to change, and what to watch afterward. We ran the same audit on the orchestration layer in our LangChain and LangGraph security CVE audit; this one targets the serving layer, the component most on-prem teams treat as infrastructure rather than as an internet-facing application.
Start here: which vLLM version are you running
Before anything else, get the number.
python -c "import vllm; print(vllm.__version__)" # or, from outside the container: curl -s http://<host>:8000/version
Then map it against the advisories that set a floor. These are the 2026 vLLM CVEs that change your minimum safe version, not the full list (there were 32 advisories between 2026-01-08 and 2026-07-27).
The bottom line from that table: 0.24.0 is the minimum defensible floor, and 0.26.0 (2026-07-27) is the current target. Every version below 0.24.0 is missing at least one fix for a vulnerability that is still exploitable in its patched-looking form. If your change board wants a single number to approve, that is the number.
| CVE | CVSS | Affected | Patched in | What it gives an attacker |
|---|---|---|---|---|
| CVE-2026-22778 | 9.8 | >= 0.8.3, < 0.14.1 | 0.14.1 | Unauthenticated RCE via crafted video URL |
| CVE-2026-48746 | 9.1 | >= 0.3.0 | 0.22.0 | Full API auth bypass via Host: header |
| CVE-2026-27893 | 8.8 | >= 0.10.1 through 0.17.x | 0.18.0 | Silent override of --trust-remote-code=False |
| CVE-2026-22807 | 8.8 | >= 0.10.1, < 0.14.0 | 0.14.0 | RCE via auto_map dynamic module loading |
| CVE-2026-54232 | 8.8 | < 0.22.1 | 0.22.1 | Dependency confusion in the vLLM Dockerfile |
| CVE-2026-41523 | 7.5 | < 0.22.0 | 0.22.0 | Code execution when running under python -O |
| CVE-2026-47155 | 6.5 | <= 0.22.0 | 0.22.0 | Pinned deployments load unpinned code and weights |
| CVE-2026-34753 | 5.4 | >= 0.16.0 | 0.19.0 | SSRF in media URL fetching |
| CVE-2026-54236 | 5.3 | <= 0.23.0 | 0.24.0 | Incomplete CVE-2026-22778 fix, leak persists |
CVE-2026-22778: how an error message becomes code execution
The interesting part of this CVE is that neither half of the chain is exploitable alone, and the first half looks like a logging bug.
When vLLM received malformed image bytes, the imaging library raised UnidentifiedImageError, and the exception message embedded the Python object repr: something in the shape of cannot identify image file <_io.BytesIO object at 0x7a95e299e750>. vLLM returned that string to the client verbatim. That hex value is a live heap address, which turns address space layout randomization from a probabilistic defense into a known quantity. Secondary write-ups put a number on how far it collapses the search space; the advisory does not, so treat the size of the reduction as unquantified and the effect as certain.
The second half is a heap overflow in the JPEG2000 decoder bundled with the video processing path. A JPEG2000 file carries a channel-definition box describing how coded components map to display channels, and the decoder trusted that box without bounds-checking the mapping. A crafted box points the luma channel at the chroma buffer, and the luma plane is four times the size, so 9,600 bytes of decoded data land in a 2,400-byte allocation. The 7,200-byte overflow reaches adjacent function pointers, and with the heap address already leaked, those pointers can be aimed at system().
Two entry points reach it: an unauthenticated POST of a video_url to /v1/chat/completions, or the same payload to /v1/invocations. Remember the second one.
Why 0.14.1 was never the fix line
The fix for CVE-2026-22778 added a message-sanitizing helper and applied it at four exception-handling sites in the OpenAI-compatible router, plus a decoder version bump. That is a correct fix for four call sites out of nine. A follow-up advisory published 2026-06-11 (CVE-2026-54236) documents the five that were missed. Two live in the Anthropic-compatible router (POST /v1/messages and POST /v1/messages/count_tokens), one in that router's server-sent-events converter, and two in the speech-to-text realtime WebSocket connection handler. All five build a response containing str(exc) directly. The reason a global exception handler does not save them is worth internalizing, because it generalizes well past vLLM. The server registers app.exception_handler(Exception) with a handler that does sanitize. But framework exception handlers only fire on exceptions that propagate out of the route function. These routes catch Exception inside the coroutine and build the JSON response themselves, so the registered handler never runs. The WebSocket paths never traverse the HTTP handler chain at all. That is the same shape of failure as CVE-2026-27893 below and as the python -O finding: a control exists, is visible in code review, and does not execute on the path that matters. If you are auditing an inference stack, grepping for the presence of a control is not evidence the control runs.
Your `--api-key` was decorative
The standard mitigation advice for an exposed inference server is "set --api-key." On vLLM, that advice produced a compliance artifact rather than a control, for two independent reasons.
Reason one: the flag does not cover every route. vLLM's security documentation is explicit that --api-key and VLLM_API_KEY apply only to OpenAI-compatible endpoints under the /v1 path prefix, and it names /invocations, /pause, /resume, and /update_weights as routes that remain unauthenticated. CVE-2026-22778 lists /invocations as an entry point. Prove it on your own box in ten seconds:
# with --api-key set, this should 401
curl -s -o /dev/null -w '%{http_code}\n' -X POST \
http://127.0.0.1:8000/v1/chat/completions -d '{}'
# not covered by --api-key on any version
curl -s -o /dev/null -w '%{http_code}\n' -X POST \
http://127.0.0.1:8000/invocations -d '{}'Reason two: the key itself was bypassable for 853 days. CVE-2026-48746 (CVSS 9.1, published 2026-06-02) affects everything from v0.3.0, which shipped 2024-01-31, until the fix in 0.22.0. That is 853 days, roughly two years and four months, during which --api-key was skippable. The middleware looked like this:
url_path = URL(scope=scope).path.removeprefix(root_path)
headers = Headers(scope=scope)
if url_path.startswith("/v1") and not self.verify_token(headers):
response = JSONResponse(content={"error": "Unauthorized"}, status_code=401)
return response(scope, receive, send)
return self.app(scope, receive, send)The ASGI toolkit reconstructs the URL as f"{scheme}://{host_header}{path}", and neither it nor the common ASGI servers filter / or ? out of the Host: header. Inject those characters into Host: and the reconstructed .path no longer starts with /v1, so the middleware waves the request through. The router, which dispatches on the real HTTP path, still routes it to the real endpoint. Authentication skipped, endpoint reached.
The advisory contains the line that should reshape your architecture: instances behind an RFC-conforming web server were not affected. For 853 days the control that actually worked was a reverse proxy that nobody had counted as a security measure, and the control that appeared in the compliance evidence was bypassable.
The opinionated conclusion: authenticate at the proxy, not at vLLM. Terminate TLS and identity at an ingress you already operate, enforce policy on the real HTTP path, and pass only the routes you intend to expose. Keep --api-key set as a second layer, but never count it as the boundary. This is the same argument we make for tool servers in the MCP server hardening checklist: the process that runs untrusted input is not the right place to put the authorization decision.
Network placement: what is actually listening
vLLM binds every interface unless told otherwise. In the 0.26.0 source, the --host argument defaults to None, and the server constructs its listener as:
sock_addr = (args.host or "", args.port)
Python binds "" to INADDR_ANY, so the default is every interface on port 8000. The documentation does not state a default for --host, so treat this as source-verified behavior and confirm it on the version you run.
The better fix is not --host 127.0.0.1. It is --uds /run/vllm/api.sock, a first-class option that removes the TCP listener entirely (host and port arguments are ignored when it is set). A Unix domain socket with a proxy in front answers the bind problem and the CVE-2026-48746 proxy requirement in one move, and it makes "is this port reachable from the pod network" a question with no answer instead of a question with a bad one.
The API port is not the only listener. Audit all of them:
ss -ltnp | grep -E 'python|vllm' # expect 127.0.0.1 or a UDS only; 0.0.0.0:8000 is the finding
Three more surfaces deserve individual attention:
--kv-port defaults to 14579 and --kv-ip to 127.0.0.1. vLLM's documentation states plainly that PyTorch Distributed, KV-cache transfer, and tensor/pipeline/data-parallel communication are insecure by default, and recommends placing the nodes on an isolated network. For multi-node serving, that isolated network is not optional.--grpc-port exposes an interface that the documentation says implements no authentication, authorization, or encryption, and should be treated as a private interface between co-located services only.pickle deserialization over ZeroMQ sockets bound to all interfaces, copied between four separate projects: vLLM (CVE-2025-30165), Meta's Llama Stack (CVE-2024-50050), and NVIDIA TensorRT-LLM (CVE-2025-23254, CVSS 9.3) among them. That study reported thousands of ZeroMQ TCP sockets exposed to the public internet across the AI inference ecosystem, without attributing a count to any single project. vLLM's remediation was replacing the vulnerable V0 engine with a safe V1 default, recommended floor v0.8.0.The general rule for a regulated deployment: the inference server sits on a segment where the only inbound path is your proxy and the only outbound paths are explicitly allowed. Our comparison of cloud and on-premise AI security and cost covers why that segmentation is easier to prove on-prem than in a shared VPC.
Controls that lie: the silent-override audit
Three 2026 advisories share a theme that should worry anyone whose compliance evidence is a screenshot of a config file.
CVE-2026-27893 (CVSS 8.8): two model implementation files passed a hardcoded trust_remote_code=True into model loading and image-processor loading, overriding an operator's explicit --trust-remote-code=False. The override produced no warning, no error, and no audit entry. On versions 0.10.1 through 0.17.x, an operator who set the flag correctly and documented it was still executing remote model code on those two architectures. Fixed in 0.18.0, which shipped 2026-03-20, less than a week before the advisory was published.
CVE-2026-41523 (CVSS 7.5): the pooler activation loader restricted loadable functions to torch.nn.modules using a Python assert. Python removes assert statements under -O or PYTHONOPTIMIZE=1. A performance flag deletes the security check, and an attacker-controlled function name from a model's config.json reaches an unrestricted import-and-getattr resolver that the advisory notes is called from roughly 20 sites with no validation of its own.
CVE-2026-47155 (CVSS 6.5): pinned deployments could load unpinned code, weights, and processors. If your reproducibility argument to an auditor is "we pin the image and the model revision," this advisory says the pin decayed at runtime on versions up to 0.22.0.
Two checks belong in your deployment pipeline, not in a quarterly review:
# 1. Is the silent trust_remote_code override present in the INSTALLED package? site=$(python -c "import vllm,os;print(os.path.dirname(vllm.__file__))") grep -rn "trust_remote_code=True" "$site/model_executor/models/" || echo "clean" # 2. Is a throughput flag deleting a security control? python -c "import sys; sys.exit(0 if __debug__ else 1)" \ || echo "FAIL: running under -O / PYTHONOPTIMIZE, asserts stripped"
The second one is four lines and catches a CVSS 7.5 condition that no dependency scanner will flag, because the vulnerable thing is your environment, not your version.
The multimodal ingress surface keeps re-breaking
The media-fetch path in vLLM has been patched for server-side request forgery four separate times in twelve months: a 2025 advisory fixed in 0.11.0, then CVE-2026-24779 fixed in 0.14.1, then CVE-2026-25960 fixed in 0.17.0, then CVE-2026-34753 fixed in 0.19.0. Same component, four patch releases, four bypasses of the previous fix.
That pattern is the argument against version-chasing as a strategy. A component with four bypasses in a year will produce a fifth. Assume the next one exists and put a control outside the process:
--allowed-media-domains, and set VLLM_MEDIA_URL_ALLOW_REDIRECTS=0 so a redirect cannot walk out of the allowlist. Confirm the flag exists on your version with vllm serve --help before you bake it into a chart: it is documented on the security page but was absent from the CLI reference page when we checked.VLLM_MAX_IMAGE_PIXELS defaults to roughly 179 million pixels, VLLM_MAX_AUDIO_CLIP_FILESIZE_MB to 25, and VLLM_MAX_AUDIO_DECODE_DURATION_S to 600. Setting any of them to 0 disables the limit, which the documentation explicitly does not recommend for deployments exposed to untrusted users.Here is a hardened invocation with every flag tied to what it defends:
vllm serve <model> \ --uds /run/vllm/api.sock \ --api-key "$VLLM_API_KEY" \ --allowed-media-domains cdn.internal.example \ --allowed-local-media-path "" \ --prefix-caching-hash-algo sha256 # --uds: no TCP listener at all; the proxy is the only ingress (CVE-2026-48746) # --api-key: defense in depth only; covers the /v1 prefix and nothing else # --allowed-media-domains: SSRF containment (CVE-2026-24779 / 25960 / 34753) # --prefix-caching-hash-algo sha256: FIPS-acceptable, replaces xxhash # --trust-remote-code omitted: default is already False, and CVE-2026-27893 # proves that setting it explicitly bought nothing on 0.10.1 to 0.17.x
With the environment pinned alongside it:
VLLM_MEDIA_URL_ALLOW_REDIRECTS=0 VLLM_MAX_IMAGE_PIXELS=16000000 VLLM_MAX_AUDIO_CLIP_FILESIZE_MB=10 VLLM_MAX_AUDIO_DECODE_DURATION_S=120 VLLM_MAX_N_SEQUENCES=64 VLLM_ALLOW_RUNTIME_LORA_UPDATING=0 VLLM_MM_HASHER_ALGORITHM=sha256 # FIPS environments; blake3 is the default # VLLM_SERVER_DEV_MODE must be unset or 0: never enable it in production # PYTHONOPTIMIZE must be unset (CVE-2026-41523)
Multi-tenant blast radius
Everything above is a single-tenant problem. On a shared engine, two categories change severity.
Cross-tenant data exposure. CVE-2026-53923 describes an integer truncation in GGUF dequantization that exposes uninitialized GPU memory. On a dedicated box that is a curiosity. On an engine serving several business units, uninitialized GPU memory is whatever the previous request left there. Patched in 0.24.0, which is another reason that floor is the right one.
Denial of service becomes a shared-fate event. The 2026 advisory set includes an unbounded n parameter (CVE-2026-34756), prompt lists that fan out into unbounded engine requests, regular-expression denial of service through structured_outputs.regex in two backends, and an audio decompression bomb (CVE-2026-54233). Each is a nuisance when one team owns the GPU and an incident when four teams share it.
Put request bounds in two places. At the gateway: body size, per-tenant request rate, and a hard reject on parameter values outside your product's range. In vLLM: VLLM_MAX_N_SEQUENCES (default 16384, not a limit in any practical sense) tightened to what your traffic needs. The reasoning matches what we work through for retrieval in multi-tenant RAG isolation patterns: a shared structure with a tenant discriminator means the discriminator is the entire security property.
Day-2: patch cadence and what to monitor
Track the project's advisory feed, not a CVE feed. Of the 32 vLLM advisories published between 2026-01-08 and 2026-07-27, 5 have no CVE ID at all, including three published on 2026-07-27 that affect versions up to 0.26.0. A scanner keyed on CVE identifiers reports those images as clean.
gh api repos/vllm-project/vllm/security-advisories --paginate \
--jq '.[] | select(.published_at > "2026-01-01") |
"\(.published_at[0:10]) \(.cve_id // "NO-CVE") \(.severity) \(.summary)"'Run that on a schedule and diff it. One cron entry, and it is the only mechanism that catches the no-CVE advisories.
Cadence. vLLM cut 15 releases between 2026-01-20 and 2026-07-27, roughly one every two weeks. Tracking head is not viable under a change-approval process, and lagging two quarters stacks multiple criticals. A monthly patch window with a same-week exception path for critical severity is the trade most regulated platform teams can sustain. Canary one replica, run your own eval suite against it (an upgrade shifts memory behavior as well as security posture, which we cover in debugging vLLM KV cache OOM), then roll.
What to monitor, in rough order of signal value:
Log requests, but audit what you are logging. Two of the CWEs on CVE-2026-22778 are error-message information disclosure and sensitive information in logs. --disable-log-requests defaults to False, which means prompts are going to your log aggregator by default. In a regulated environment that is often a bigger finding than the CVE itself.
| Signal | Why it matters | Alert on |
|---|---|---|
Requests reaching /invocations, /pause, /resume, /update_weights | Never gated by --api-key; legitimate clients rarely touch them | Any request at all from outside the proxy |
| 401 rate at the proxy versus request rate at vLLM | A gap means traffic is bypassing the proxy | Non-zero divergence |
Host: headers containing / or ? | The CVE-2026-48746 exploit signature | Any occurrence |
| Egress denials from the inference namespace | SSRF attempts, or a media allowlist that is too tight | Sustained non-zero |
| Listener inventory drift | A --host or UDS regression in a chart update | Any new TCP listener |
| 5xx responses carrying long exception strings | Information disclosure regressions | Rate change after upgrade |
The checklist
Work top to bottom. Items 1 through 4 are the ones that would have blunted every CVE in this post.
--uds /run/vllm/api.sock with a reverse proxy in front. If you must use TCP, --host 127.0.0.1 plus a firewall that permits only the proxy./invocations, /pause, /resume, /update_weights at the edge unless you deliberately use them.PYTHONOPTIMIZE, no VLLM_SERVER_DEV_MODE, no VLLM_ALLOW_RUNTIME_LORA_UPDATING, no shared cache directories (cache contents are loaded without integrity verification).VLLM_MAX_N_SEQUENCES from its default of 16384.trust_remote_code=True and fail the build on a hit.The uncomfortable summary is that most self-hosted inference runs with a threat model borrowed from a research notebook: trusted network, trusted callers, trusted model files. Three of the CVEs above only reach severity because that assumption is still baked into the defaults. Fix the placement first and the version becomes a maintenance task rather than an incident. More on securing model infrastructure sits in our AI security pillar.
FAQ
Quick answers to the questions this post tends to raise.




