File size: 3,383 Bytes
d8c40b3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
# PoC β€” NeMo `setup_model()` target-allowlist bypass

`malicious_target_bypass.nemo` is an uncompressed tar archive (`tarfile.open(mode="w:")`, the
same mode NeMo itself uses to write `.nemo` files) containing:

- `model_config.yaml` β€” a minimal config whose top-level `target` field is
  `subprocess.Popen`, a class completely unrelated to any NeMo model and **not** present in
  `nemo.core.classes.common.ALLOWED_TARGET_PREFIXES`.
- `model_weights.ckpt` β€” a placeholder (not a real PyTorch checkpoint); it is never reached in
  this specific demonstration because `subprocess.Popen` has no `.restore_from()`.

## How this is meant to be used

```
python examples/asr/transcribe_speech.py model_path=malicious_target_bypass.nemo audio_dir=<any>
```

Expected effect, per the vulnerability described in the accompanying `SUBMISSION.md`:

1. `setup_model()` (`nemo/collections/asr/parts/utils/transcribe_utils.py:315-344`) calls
   `ASRModel.restore_from(restore_path=model_path, return_config=True)`, which extracts and
   parses `model_config.yaml` **without** ever calling `_is_target_allowed()` /
   `safe_instantiate()`.
2. `classpath = model_cfg.target` β†’ `"subprocess.Popen"`.
3. `model_utils.import_class_by_path("subprocess.Popen")` succeeds β€” this is the raw,
   unprotected function; it imports `subprocess` and resolves `Popen` with no allowlist check
   at all.
4. `subprocess.Popen.restore_from(restore_path=..., map_location=...)` is then attempted and
   raises `AttributeError` (`Popen` has no `restore_from`), so the process crashes at this
   point.

The crash on step 4 is the **intentionally non-destructive, minimum-guaranteed** demonstration:
it proves the import of an arbitrary, attacker-chosen, out-of-allowlist class already happened
(step 3) before anything fails. Swapping `subprocess.Popen` for a module with dangerous
import-time side effects, or for a compatible internal NeMo model class, changes the outcome
without changing the underlying bug.

## Caveat β€” not run end-to-end against a live NeMo installation

The exact bypass mechanism (`import_class_by_path()` accepting `"subprocess.Popen"` while
`_is_target_allowed()` rejects it) **was** empirically verified by importing and calling the
real, unmodified functions from this commit's `nemo/utils/model_utils.py` and
`nemo/core/classes/common.py` directly (see `attachments/poc_allowlist_bypass.py` +
`attachments/run_output.txt` in the main submission folder).

What was **not** independently re-verified is running `transcribe_speech.py` end-to-end against
this exact `.nemo` file inside a full NeMo installation (setting up the complete pinned
dependency stack β€” `lightning`, `nv_one_logger*`, `megatron-core`, etc. β€” proved impractical in
the available verification environment due to unrelated third-party version incompatibilities
in the `nv_one_logger` / `lightning` integration layer, not to anything about this bug). The
static trace of `setup_model()` β†’ `restore_from(..., return_config=True)` β†’
`load_config_and_state_dict()` (`nemo/core/connectors/save_restore_connector.py:98-202`) was
read in full and is quoted in the submission; only the live end-to-end run was not reproduced.

If it does not reproduce cleanly against a specific NeMo install, the standalone function-level
PoC remains valid, verified, independent evidence of the root-cause bug regardless.