diff --git a/monai/auto3dseg/utils.py b/monai/auto3dseg/utils.py index f349561bdc..518c91919d 100644 --- a/monai/auto3dseg/utils.py +++ b/monai/auto3dseg/utils.py @@ -493,6 +493,14 @@ def algo_from_json(filename: str, template_path: PathLike | None = None, **kwarg if state_template_path: algo_config["template_path"] = state_template_path + warnings.warn( + f"Loading {filename}: the file's `_target_` value is resolved to an imported callable and " + "invoked, and template directories from the file may be added to `sys.path`; only load " + "algo_object.json files from a source you trust " + "(see https://github.com/Project-MONAI/MONAI/security/advisories/GHSA-2wx3-8x3w-r8qv).", + stacklevel=2, + ) + parser = ConfigParser(algo_config) algo = parser.get_parsed_content() used_template_path = path diff --git a/tests/apps/test_auto3dseg.py b/tests/apps/test_auto3dseg.py index 57e05d1ee6..c310afc76a 100644 --- a/tests/apps/test_auto3dseg.py +++ b/tests/apps/test_auto3dseg.py @@ -11,9 +11,11 @@ from __future__ import annotations +import json import os import tempfile import unittest +import warnings from copy import deepcopy from numbers import Number @@ -36,6 +38,7 @@ SampleOperations, SegSummarizer, SummaryOperations, + algo_from_json, datafold_read, verify_report_format, ) @@ -177,6 +180,20 @@ def __call__(self, data): return d +class _DummyAlgo: + """Minimal stand-in for an Auto3DSeg Algo object used in warning tests.""" + + def __init__(self) -> None: + self.template_path: str | None = None + self.output_path = os.getcwd() + + def load_state_dict(self, state: dict) -> None: + pass + + def get_output_path(self) -> str: + return self.output_path + + class TestDataAnalyzer(unittest.TestCase): def setUp(self): self.test_dir = tempfile.TemporaryDirectory() @@ -619,5 +636,23 @@ def tearDown(self) -> None: self.test_dir.cleanup() +class TestAlgoFromJsonSecurityWarning(unittest.TestCase): + def test_warns_about_untrusted_target(self) -> None: + with tempfile.TemporaryDirectory() as tmpdir: + algo_file = os.path.join(tmpdir, "algo_object.json") + with open(algo_file, "w", encoding="utf-8") as f: + json.dump({"_target_": f"{__name__}._DummyAlgo"}, f) + + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + algo_from_json(algo_file) + + messages = [str(w.message) for w in caught] + self.assertTrue( + any("algo_object.json" in msg and "trust" in msg for msg in messages), + f"Keywords 'algo_object.json' and 'trust' not found in warning messages: {messages}", + ) + + if __name__ == "__main__": unittest.main()