"""Security tests: test_cmd extra_args injection into pytest subprocess. muse/core/test_runner.py runs pytest as an isolated subprocess with shell=False (safe) but passes user-supplied extra_args directly to cmd. An attacker can inject pytest flags that alter test discovery and import semantics: --rootdir=/ → changes pytest's root, may discover and run tests outside the repo tree --import-mode=importlib → alters Python import resolution --override-ini=key=value → override pyproject.toml settings --confcutdir=/ → expands conftest search beyond the repo The fix: filter extra_args through _SAFE_PYTEST_FLAG_PREFIXES allowlist before building the subprocess command. """ from __future__ import annotations from muse.core.test_runner import ( RunConfig, _SAFE_PYTEST_FLAG_PREFIXES, _filter_extra_args, ) # --------------------------------------------------------------------------- # § 1 — _filter_extra_args strips dangerous pytest flags # --------------------------------------------------------------------------- class TestFilterExtraArgs: """_filter_extra_args must strip dangerous pytest flags.""" def test_function_is_importable(self) -> None: assert callable(_filter_extra_args) def test_rootdir_stripped(self) -> None: result = _filter_extra_args(["--rootdir=/", "-v"]) assert "--rootdir=/" not in result assert "-v" in result def test_rootdir_with_value_stripped(self) -> None: result = _filter_extra_args(["--rootdir", "/etc", "-x"]) flat = " ".join(result) assert "--rootdir" not in flat assert "/etc" not in flat assert "-x" in result def test_import_mode_stripped(self) -> None: result = _filter_extra_args(["--import-mode=importlib", "-v"]) flat = " ".join(result) assert "--import-mode" not in flat assert "-v" in result def test_override_ini_stripped(self) -> None: result = _filter_extra_args(["--override-ini=addopts=--forked"]) assert not any("override-ini" in a for a in result) def test_confcutdir_stripped(self) -> None: result = _filter_extra_args(["--confcutdir=/"]) assert not any("confcutdir" in a for a in result) def test_safe_flags_preserved(self) -> None: safe = ["-v", "-x", "-s", "-k", "test_foo", "--tb=short", "-m", "fast"] result = _filter_extra_args(safe) for flag in safe: assert flag in result, f"safe flag {flag!r} was stripped" def test_empty_list_preserved(self) -> None: assert _filter_extra_args([]) == [] def test_plugin_flag_stripped(self) -> None: """-p (plugin flag) should be blocked; its value must also be dropped.""" result = _filter_extra_args(["-p", "no:cacheprovider", "-v"]) flat = " ".join(result) assert "cacheprovider" not in flat, "value after dropped -p must be dropped" assert "-p" not in result assert "-v" in result def test_dangerous_combination_stripped(self) -> None: """Realistic injection string after muse code test -- --rootdir=/ ...""" injection = [ "--rootdir=/", "--import-mode=importlib", "--override-ini=addopts=--forked", "-v", "-k", "malicious", ] result = _filter_extra_args(injection) flat = " ".join(result) assert "rootdir" not in flat assert "import-mode" not in flat assert "override-ini" not in flat assert "-v" in result assert "-k" in result # --------------------------------------------------------------------------- # § 2 — RunConfig + _run_partition apply _filter_extra_args at subprocess time # --------------------------------------------------------------------------- class TestRunConfigFiltersExtraArgs: """The subprocess cmd list built from RunConfig must not contain banned flags.""" def test_subprocess_cmd_does_not_contain_rootdir(self) -> None: """Even if extra_args has --rootdir=/, _filter_extra_args strips it.""" # _filter_extra_args is the gate — verify it strips before the call dangerous = ["--rootdir=/", "-v"] safe = _filter_extra_args(dangerous) flat = " ".join(safe) assert "--rootdir=/" not in flat, ( "The subprocess command contains --rootdir=/ — filtering is not applied" ) def test_run_config_accepts_extra_args_field(self) -> None: """RunConfig TypedDict must have an extra_args field.""" config = RunConfig( extra_args=["--rootdir=/", "-v"], workers=1, timeout_s=60.0, env_allowlist=[], cwd=None, ) assert "extra_args" in config # --------------------------------------------------------------------------- # § 3 — _SAFE_PYTEST_FLAG_PREFIXES is defined and covers expected flags # --------------------------------------------------------------------------- class TestSafeFlagPrefixes: def test_constant_defined(self) -> None: assert isinstance(_SAFE_PYTEST_FLAG_PREFIXES, (list, tuple, frozenset, set)) prefixes = set(_SAFE_PYTEST_FLAG_PREFIXES) for flag in ("-v", "-x", "-s", "--tb", "-k"): assert any(flag.startswith(p) or p.startswith(flag) for p in prefixes), ( f"Safe flag {flag!r} is not covered by _SAFE_PYTEST_FLAG_PREFIXES" ) def test_rootdir_not_in_safe_prefixes(self) -> None: prefixes = set(_SAFE_PYTEST_FLAG_PREFIXES) assert not any("rootdir" in p for p in prefixes) def test_import_mode_not_in_safe_prefixes(self) -> None: prefixes = set(_SAFE_PYTEST_FLAG_PREFIXES) assert not any("import-mode" in p for p in prefixes)