Severity Daily

IT and AI security incidents, checked against the primary source

vLLM ran attacker model code with trust_remote_code set to False, and the CVE arrived 17 days after the fix

PATCH SOON — vLLM ran attacker model code with trust_remote_code set to False, and the CVE arrived 17 days after the fix

Written by

in

One model loader handed the safety flag to a function that never accepted it, the argument vanished into **kwargs, and untrusted model code ran anyway.

What happened

NVD published CVE-2026-90553 at 1:16 p.m. UTC on Saturday, September 12, 2026. The record is still marked Received, which means NVD has not completed its own analysis; every score on it belongs to VulnCheck, the CNA that assigned the identifier.

The description, verbatim: “vLLM before 0.28.0 contains a remote code execution vulnerability in the LlavaOnevision2 processor loader that ignores the trust_remote_code parameter when loading remote processor classes. Attackers can craft a malicious model with arbitrary code in processing_llava_onevision2.py that executes with vLLM process authority even when trust_remote_code is set to False.”

vLLM is an open-source inference server for large language models, widely deployed for self-hosted serving. The project does not publish deployment counts, and no figure in circulation is verifiable from a primary source, so this story gives none.

The mechanism is set out in the project’s own advisory, GHSA-3c86-2m5g-59q7, published August 28, 2026. The _load_ov2_processor function loads remote processor classes through transformers.dynamic_module_utils.get_class_from_dynamic_module(). It passes a trust_remote_code argument. The transformers function does not take that argument, so it lands in **kwargs and is discarded. GitHub’s advisory says it plainly: the parameter “is silently ignored via **kwargs.” vLLM ships a wrapper, try_get_class_from_dynamic_module, that resolves the trust setting before calling through. The LlavaOnevision2 path called the raw transformers function instead.

GitHub rates the flaw High at 7.8, with the vector CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H. VulnCheck carries that same v3.1 vector and score on the NVD record, and adds a CVSS v4.0 score of 8.5 from the vector CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:P/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N. The weakness is CWE-94. GitHub states the impact as “Arbitrary Python execution with vLLM process authority when loading untrusted community models, undermining the security intent of the trust_remote_code=False safety setting.” The reporter is credited as NLx64, with jperezdealgaba as coordinator.

The fix shipped first. PyPI’s release metadata puts vllm 0.28.0 on the index at 10:07 a.m. UTC on August 26, 2026 — two days before the advisory, and seventeen days before the CVE record. Version 0.29.0 followed on September 9. Until yesterday afternoon, GitHub’s advisory carried the line “No known CVE.”

Two companion records landed in the same batch, at the same minute. CVE-2026-90554 covers versions 0.10.2 through 0.28.0 and reads: “vLLM versions >=0.10.2 and <0.28.0 do not apply any audio decode-size or duration limit when extracting audio from video input for NanoNemotronVL models.” It is CWE-400, scored 6.2 under v3.1 and 6.9 under v4.0, and it applies only where use_audio_in_video=True. CVE-2026-90555 reads: “vLLM versions before 0.28.0 fail to validate audio sample rate headers in the transcription endpoint, allowing authenticated clients to bypass duration checks. Attackers can submit forged FLAC headers with inflated sample rates to trigger excessive memory allocation and crash the API server process affecting all tenants.” It is CWE-409, scored 6.5 under v3.1 and 7.1 under v4.0.

No source checked reports exploitation. Neither GitHub nor VulnCheck claims any, and none of the three CVEs appears in the Known Exploited Vulnerabilities catalog.

Why it matters

The control that failed is the one the ecosystem tells operators to rely on. Pulling a model from a public hub means downloading weights and, for some architectures, Python. trust_remote_code is the boundary between those two things. It is the flag that appears in every hardening guide for self-hosted inference, the one an auditor asks about, the one a platform team sets to False in a base image and considers the question closed. For one model family, an operator who did exactly that got arbitrary code execution anyway.

The bug class deserves attention beyond this one project. A keyword argument was passed to a function that does not accept it and was absorbed by a catch-all. Python raises nothing. Linters raise nothing when the callee’s signature ends in **kwargs. Code review sees a call site that reads correctly, because it does read correctly — the argument is right there in the source. A test asserting “the model loads” passes. The test that would have caught this asserts that a hostile model fails to load, and that is the test almost nobody writes. This publication keeps running into the same shape: a guard that exists, reads as intended, and does nothing at runtime. It is a failure mode that survives every review that looks at the code rather than at the behavior.

Then there is the fifteen-day gap between the advisory and the identifier. This is not a complaint about volunteer labor; it is a description of what two different scanners told two different organizations running the same software. A team using GitHub-native tooling was told on August 28. A team whose software composition analysis keys on CVE IDs sourced from NVD — which is most vulnerability-management and most compliance reporting — was told nothing until yesterday. Both scans were working correctly. Both answers came from a maintained database. One of them was wrong for a little over two weeks, and the organization holding it had no way to know that from inside its own tooling. Any vLLM scan performed between August 28 and September 12 that returned clean should be treated as unrun.

The scoring vector is worth a second look, not because it is wrong but because of the picture it paints. AV:L with UI:R describes a person sitting at a machine who chooses to load a particular model. A good many vLLM deployments are not shaped like that. The model is a string in a configuration file. A container pulls it on startup. An autoscaler pulls it again on every new node. In that shape the required user interaction happened once, months ago, when somebody edited a YAML file, and every subsequent load is automatic. The score is defensible on its own terms. The mental image it produces — a developer at a laptop, one deliberate action, local access — is not the one that applies to a production inference fleet that resolves model names against a public hub on every scale-out event.

Finally, the shape of the batch. All three records are in multimodal handling: audio extraction from video, sample-rate headers on a transcription endpoint, and a vision-language processor loader. Inference servers have been growing codecs, container parsers, and dynamic class loading at speed, and in doing so they have inherited the bug classes that media handling has carried for thirty years. Two of these three are memory-exhaustion crashes reachable through a decoder. That is not an AI problem. That is a parser problem that has arrived inside AI infrastructure, and it will keep arriving.

What to do

Upgrade to vllm 0.28.0 or later; 0.29.0 has been on PyPI since September 9. That single release closes all three records.

If you cannot upgrade immediately, note that the code execution path requires loading a LlavaOnevision2 model. Audit which model families your serving configuration permits, and pin models to a vetted internal mirror rather than resolving names against a public hub at runtime. CVE-2026-90554 applies only to NanoNemotronVL models with use_audio_in_video=True, so check that configuration key directly. CVE-2026-90555 requires an authenticated client, which makes it a tenant-against-tenant problem on shared endpoints; cap request body size at the proxy and rate-limit the transcription route.

Stop treating trust_remote_code=False as a sufficient control on its own. Run the server as an unprivileged user, in a container that has no cloud credentials mounted and no token that can write to your model registry, so that a loader bug costs you a process rather than a supply chain.

Re-run your dependency scan against vLLM. If your tooling reads NVD, it acquired these three identifiers yesterday afternoon and not before.

Sourcing note

Checked: the NVD records for CVE-2026-90553, CVE-2026-90554, and CVE-2026-90555, all published at 1:16 p.m. UTC on September 12, 2026, and all carrying vulnStatus of Received; the vLLM project’s GitHub Security Advisories GHSA-3c86-2m5g-59q7, GHSA-936p-m5pv-vvjf, and GHSA-99f2-hwrc-gvq8; VulnCheck’s advisory page for the LlavaOnevision2 flaw; and PyPI’s release metadata for the vllm package, used as the primary source for release upload timestamps. CISA’s website returns 403 to automated fetching, so the Known Exploited Vulnerabilities catalog was read from the cisagov/kev-data GitHub mirror, catalog version 2026.09.11, released at 7:32 p.m. UTC on September 11 with 1,709 entries; none of these three CVEs is in it. Could not reach: GitHub’s REST API returned 403, so release dates come from PyPI and from the advisory pages themselves rather than from release objects. Unresolved: neither the project nor VulnCheck explains why the identifiers were assigned fifteen days after the advisory, and no source checked reports exploitation of any of the three.