From 9cad948bde10bd7197f22d72171cc21cdd4405e1 Mon Sep 17 00:00:00 2001 From: interplains Date: Tue, 20 Oct 2026 15:42:33 +0300 Subject: [PATCH] Refuse network paths for --iocs and --json in the single-file download The downloadable single file has its own entry point, generated by build/bundle.py, separate from the source CLI. The local-disk check for --iocs and --json was added only to the source CLI, so the download still accepted UNC and mapped-drive paths. Add the same check to the bundled entry point, before anything is opened, and test it against the built bundle: network paths are refused, and the documented relative "--json report.json" still works. --- build/bundle.py | 17 ++++++++++++ scan/tests/test_bundle_release.py | 43 +++++++++++++++++++++++++++++-- 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/build/bundle.py b/build/bundle.py index efead9f..8c27ce6 100644 --- a/build/bundle.py +++ b/build/bundle.py @@ -282,6 +282,23 @@ def main(argv=None): _hold(args) return 1 + # Refuse a user-typed --iocs/--json path that is not on a local disk. + # Opening or creating a UNC path or mapped network drive makes Windows open + # an SMB session to that host - sending this account's network credentials + # and, for --json, the whole report. Checked before anything is opened and + # before the scan rather than after it. This entry point is separate from + # the source CLI, so the guard has to live here too. abspath first, because + # path_problem refuses relative paths and "--json report.json" is the + # documented usage. + for label, value in (("--iocs", args.iocs), ("--json", args.json)): + if value: + problem = path_problem(os.path.abspath(value)) + if problem: + print("error: {} {}: {} - use a path on a local disk".format( + label, value, problem), file=sys.stderr) + _hold(args) + return 1 + try: ioc_set = _load_embedded_iocs(args.iocs) except IocError as exc: diff --git a/scan/tests/test_bundle_release.py b/scan/tests/test_bundle_release.py index d6249ef..be52174 100644 --- a/scan/tests/test_bundle_release.py +++ b/scan/tests/test_bundle_release.py @@ -5,6 +5,7 @@ import importlib.util import io import json +import os from pathlib import Path import subprocess import sys @@ -54,7 +55,7 @@ def setUp(self): "review.invalid\tcc\tprimary\ttest\n" "later.invalid\tcc\tprimary\ttest\n", encoding="utf-8") - def run_bundle(self, browser_run=None): + def run_bundle(self, browser_run=None, extra=()): b = self.bundle documents = [] with contextlib.ExitStack() as stack: @@ -73,9 +74,47 @@ def run_bundle(self, browser_run=None): stack.enter_context(patch.object(b, "run_browser", browser_run)) stack.enter_context(contextlib.redirect_stdout(io.StringIO())) stack.enter_context(contextlib.redirect_stderr(io.StringIO())) - code = b.main(["--no-pause", "--iocs", str(self.ioc_path)]) + code = b.main(["--no-pause", "--iocs", str(self.ioc_path)] + list(extra)) return code, documents[0] + # The download has its own entry point, separate from the source CLI. The + # network-path guard was once added only to cli.py and was silently absent + # from the file people actually download, so it is tested here directly. + @unittest.skipUnless(sys.platform == "win32", + "UNC and mapped-drive paths are a Windows concept") + def test_download_refuses_network_paths_before_opening_anything(self): + b = self.bundle + bs = chr(92) + unc = bs + bs + "server.invalid" + bs + "share" + bs + "x.txt" + opened = [] + + def must_not_load(*args, **kwargs): + opened.append(args) + raise AssertionError("indicator file opened before the refusal") + + for flag in ("--iocs", "--json"): + err = io.StringIO() + with contextlib.ExitStack() as stack: + stack.enter_context(patch.object(b, "_load_embedded_iocs", + must_not_load)) + stack.enter_context(patch.object(b, "_force_utf8_io", + lambda: None)) + stack.enter_context(contextlib.redirect_stderr(err)) + stack.enter_context(contextlib.redirect_stdout(io.StringIO())) + code = b.main(["--no-pause", flag, unc]) + self.assertEqual(code, 1, flag) + self.assertIn("local disk", err.getvalue(), flag) + self.assertEqual(opened, []) + + def test_download_accepts_a_relative_local_json_path(self): + # "--json report.json" is the documented usage and must keep working. + old = os.getcwd() + os.chdir(self.root) + self.addCleanup(os.chdir, old) + code, _document = self.run_bundle(extra=("--json", "report.json")) + self.assertNotEqual(code, 1) + self.assertTrue((self.root / "report.json").exists()) + def test_module_failure_preserves_already_observed_hit(self): b = self.bundle