All vulnerabilities
GHSA-8f32-8649-rv87

monai: Arbitrary code execution via unguarded pickle.load of attacker-influenceable postprocessing_file in nnUNetV2Runner

monai
CVSS
7.3High
ExploitationNo known exploitationNot in CISA KEV, which does not prove it is unexploitedCISA KEV
Exploit odds (EPSS)No scoreNo FIRST EPSS score in this snapshot yet
Fixed in1.6.1Fixed versionGitHub
Published27 Sep 2026Updated 27 Sep 2026GitHub

What fixes it

A fixed version on one release line is not the upgrade target for every line. Check your exact version and any vendor backports.

Description

Summary

monai.apps.nnunet.nnunetv2_runner.nnUNetV2Runner unpickles an attacker-influenceable file path with no safe-loader, allowlist, or environment gate. At nnunetv2_runner.py:29 MONAI imports load_pickle as an alias of batchgenerators.utilities.file_and_folder_operations.load_pickle — a thin pickle.load(open(path,'rb')) wrapper. In predict_ensemble_postprocessing(run_postprocessing=True) the runner unpickles the file named by self.best_configuration["best_model_or_ensemble"]["postprocessing_file"], a string read verbatim from inference_information.json. A malicious .pkl whose __reduce__ runs a payload yields arbitrary code execution as the inference user. This is a novel variant of MONAI's known pickle-deserialization class that the maintainers' grep-based hardening missed because the sink is an aliased import (invisible to a pickle.load/torch.load grep).

Root Cause

  • nnunetv2_runner.py:29: load_pickle, _ = optional_import("batchgenerators.utilities.file_and_folder_operations", name="load_pickle") — batchgenerators' load_pickle is an unguarded pickle.load.
  • best_configuration is loaded (~line 958) from os.path.join(self.nnunet_results, self.dataset_name, "inference_information.json") via ConfigParser.load_config_file, which validates JSON syntax / duplicate keys only; string values are trusted and unrestricted.
  • The sink (line 994 on the 1.6.0 release tag) is load_pickle(self.best_configuration["best_model_or_ensemble"]["postprocessing_file"]). There is NO MONAI_ALLOW_PICKLE gate anywhere under monai/apps/nnunet/ (grep is empty), although MONAI uses exactly this env-gated pickle guard elsewhere (monai/utils/misc.py, monai/auto3dseg/utils.py) — proving the sink was simply missed.

Impact

Arbitrary code execution as the training/inference user when the runner is executed against attacker-influenced nnU-Net results (shared/multi-tenant results directory, or a second-party nnU-Net artifact). Controlling a results/model artifact is NOT root-equivalent, so a genuine trust boundary is crossed (same class as the accepted GHSA-p8cm-mm2v-gwjm / GHSA-89gg-p5r5-q6r4 pickle advisories, in a new unhardened file).

Proof of Concept

  1. Plant inference_information.json in nnunet_results/<dataset>/ with best_model_or_ensemble.postprocessing_file = "/path/evil.pkl", plus a malicious evil.pkl whose __reduce__ executes a command.
  2. Victim runs nnUNetV2Runner(...).predict_ensemble_postprocessing(run_postprocessing=True) (also reachable via run(run_predict...=True)).
  3. load_pickle → pickle.load → __reduce__ executes.

Attack Chain

  1. Entry: attacker plants inference_information.json (postprocessing_file → evil.pkl) + evil.pkl in the nnUNet results dir. Guard: ConfigParser.load_config_file (config_parser.py). Bypass proof: validates JSON syntax + duplicate keys only; string values are trusted/unrestricted.
  2. Processing: victim runs predict_ensemble_postprocessing(run_postprocessing=True). Guard: none. Bypass proof: control flows straight to the sink; grep of monai/apps/nnunet/ for MONAI_ALLOW_PICKLE is empty — no allowlist/weights_only/env gate.
  3. Sink: load_pickle(postprocessing_file) → pickle.load(f) → __reduce__ executes (nnunetv2_runner.py:994 on tag 1.6.0).
  4. Impact: arbitrary code execution as the inference user.

Bypass Evidence

  • Alias at nnunetv2_runner.py:29 confirmed to resolve to batchgenerators load_pickle (== pickle.load).
  • No MONAI_ALLOW_PICKLE/safe-loader/allowlist present under monai/apps/nnunet/.
  • ConfigParser.load_config_file does not constrain string values.

Affected Versions

<= 1.6.0 (sink confirmed present at the 1.6.0 release tag via git show 1.6.0:monai/apps/nnunet/nnunetv2_runner.py; no post-1.6.0 fix — later commits touching the file are CLI-flag/type-checker changes only). Not covered by GHSA-p8cm-mm2v-gwjm (data/utils.py pickle_operations) or GHSA-89gg-p5r5-q6r4/GHSA-qxq5-qhx6-94qw (auto3dseg algo_from_pickle).

Suggested Fix

Gate the unpickle behind an explicit opt-in (reuse the existing MONAI_ALLOW_PICKLE pattern), and/or validate/allowlist postprocessing_file against a trusted directory before deserialization.

--- Reported by zx (Jace) — GitHub: @manus-use

Text from GitHub Advisory Database

Affected versions 1

  • monai, pip<= 1.6.0GitHub