monai: Arbitrary code execution via unguarded pickle.load of attacker-influenceable postprocessing_file in nnUNetV2Runner
What fixes it
- 1.6.1monai, pipGitHub
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_pickleis an unguardedpickle.load.best_configurationis loaded (~line 958) fromos.path.join(self.nnunet_results, self.dataset_name, "inference_information.json")viaConfigParser.load_config_file, which validates JSON syntax / duplicate keys only; string values are trusted and unrestricted.- The sink (line 994 on the
1.6.0release tag) isload_pickle(self.best_configuration["best_model_or_ensemble"]["postprocessing_file"]). There is NOMONAI_ALLOW_PICKLEgate anywhere undermonai/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
- Plant
inference_information.jsoninnnunet_results/<dataset>/withbest_model_or_ensemble.postprocessing_file = "/path/evil.pkl", plus a maliciousevil.pklwhose__reduce__executes a command. - Victim runs
nnUNetV2Runner(...).predict_ensemble_postprocessing(run_postprocessing=True)(also reachable viarun(run_predict...=True)). load_pickle→pickle.load→__reduce__executes.
Attack Chain
- Entry: attacker plants
inference_information.json(postprocessing_file → evil.pkl) +evil.pklin 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. - Processing: victim runs
predict_ensemble_postprocessing(run_postprocessing=True). Guard: none. Bypass proof: control flows straight to the sink; grep ofmonai/apps/nnunet/forMONAI_ALLOW_PICKLEis empty — no allowlist/weights_only/env gate. - Sink:
load_pickle(postprocessing_file)→pickle.load(f)→__reduce__executes (nnunetv2_runner.py:994 on tag 1.6.0). - Impact: arbitrary code execution as the inference user.
Bypass Evidence
- Alias at
nnunetv2_runner.py:29confirmed to resolve to batchgeneratorsload_pickle(==pickle.load). - No
MONAI_ALLOW_PICKLE/safe-loader/allowlist present undermonai/apps/nnunet/. ConfigParser.load_config_filedoes 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