gabriel / muse public
test_security_test_runner.py python
146 lines 5.7 KB
Raw
sha256:f8e686793bb93114c2923d0d294162d13b4e6f4d57ae0f6cbc1e0d493e80f965 fix: ls-remote signing identity uses resolved remote URL Sonnet 4.6 patch 11 days ago
1 """Security tests: test_cmd extra_args injection into pytest subprocess.
2
3 muse/core/test_runner.py runs pytest as an isolated subprocess with
4 shell=False (safe) but passes user-supplied extra_args directly to cmd.
5 An attacker can inject pytest flags that alter test discovery and import
6 semantics:
7
8 --rootdir=/ → changes pytest's root, may discover and run tests
9 outside the repo tree
10 --import-mode=importlib → alters Python import resolution
11 --override-ini=key=value → override pyproject.toml settings
12 --confcutdir=/ → expands conftest search beyond the repo
13
14 The fix: filter extra_args through _SAFE_PYTEST_FLAG_PREFIXES allowlist
15 before building the subprocess command.
16 """
17
18 from __future__ import annotations
19
20 from muse.core.test_runner import (
21 RunConfig,
22 _SAFE_PYTEST_FLAG_PREFIXES,
23 _filter_extra_args,
24 )
25
26
27 # ---------------------------------------------------------------------------
28 # § 1 — _filter_extra_args strips dangerous pytest flags
29 # ---------------------------------------------------------------------------
30
31 class TestFilterExtraArgs:
32 """_filter_extra_args must strip dangerous pytest flags."""
33
34 def test_function_is_importable(self) -> None:
35 assert callable(_filter_extra_args)
36
37 def test_rootdir_stripped(self) -> None:
38 result = _filter_extra_args(["--rootdir=/", "-v"])
39 assert "--rootdir=/" not in result
40 assert "-v" in result
41
42 def test_rootdir_with_value_stripped(self) -> None:
43 result = _filter_extra_args(["--rootdir", "/etc", "-x"])
44 flat = " ".join(result)
45 assert "--rootdir" not in flat
46 assert "/etc" not in flat
47 assert "-x" in result
48
49 def test_import_mode_stripped(self) -> None:
50 result = _filter_extra_args(["--import-mode=importlib", "-v"])
51 flat = " ".join(result)
52 assert "--import-mode" not in flat
53 assert "-v" in result
54
55 def test_override_ini_stripped(self) -> None:
56 result = _filter_extra_args(["--override-ini=addopts=--forked"])
57 assert not any("override-ini" in a for a in result)
58
59 def test_confcutdir_stripped(self) -> None:
60 result = _filter_extra_args(["--confcutdir=/"])
61 assert not any("confcutdir" in a for a in result)
62
63 def test_safe_flags_preserved(self) -> None:
64 safe = ["-v", "-x", "-s", "-k", "test_foo", "--tb=short", "-m", "fast"]
65 result = _filter_extra_args(safe)
66 for flag in safe:
67 assert flag in result, f"safe flag {flag!r} was stripped"
68
69 def test_empty_list_preserved(self) -> None:
70 assert _filter_extra_args([]) == []
71
72 def test_plugin_flag_stripped(self) -> None:
73 """-p (plugin flag) should be blocked; its value must also be dropped."""
74 result = _filter_extra_args(["-p", "no:cacheprovider", "-v"])
75 flat = " ".join(result)
76 assert "cacheprovider" not in flat, "value after dropped -p must be dropped"
77 assert "-p" not in result
78 assert "-v" in result
79
80 def test_dangerous_combination_stripped(self) -> None:
81 """Realistic injection string after muse code test -- --rootdir=/ ..."""
82 injection = [
83 "--rootdir=/",
84 "--import-mode=importlib",
85 "--override-ini=addopts=--forked",
86 "-v",
87 "-k", "malicious",
88 ]
89 result = _filter_extra_args(injection)
90 flat = " ".join(result)
91 assert "rootdir" not in flat
92 assert "import-mode" not in flat
93 assert "override-ini" not in flat
94 assert "-v" in result
95 assert "-k" in result
96
97
98 # ---------------------------------------------------------------------------
99 # § 2 — RunConfig + _run_partition apply _filter_extra_args at subprocess time
100 # ---------------------------------------------------------------------------
101
102 class TestRunConfigFiltersExtraArgs:
103 """The subprocess cmd list built from RunConfig must not contain banned flags."""
104
105 def test_subprocess_cmd_does_not_contain_rootdir(self) -> None:
106 """Even if extra_args has --rootdir=/, _filter_extra_args strips it."""
107 # _filter_extra_args is the gate — verify it strips before the call
108 dangerous = ["--rootdir=/", "-v"]
109 safe = _filter_extra_args(dangerous)
110 flat = " ".join(safe)
111 assert "--rootdir=/" not in flat, (
112 "The subprocess command contains --rootdir=/ — filtering is not applied"
113 )
114
115 def test_run_config_accepts_extra_args_field(self) -> None:
116 """RunConfig TypedDict must have an extra_args field."""
117 config = RunConfig(
118 extra_args=["--rootdir=/", "-v"],
119 workers=1,
120 timeout_s=60.0,
121 env_allowlist=[],
122 cwd=None,
123 )
124 assert "extra_args" in config
125
126
127 # ---------------------------------------------------------------------------
128 # § 3 — _SAFE_PYTEST_FLAG_PREFIXES is defined and covers expected flags
129 # ---------------------------------------------------------------------------
130
131 class TestSafeFlagPrefixes:
132 def test_constant_defined(self) -> None:
133 assert isinstance(_SAFE_PYTEST_FLAG_PREFIXES, (list, tuple, frozenset, set))
134 prefixes = set(_SAFE_PYTEST_FLAG_PREFIXES)
135 for flag in ("-v", "-x", "-s", "--tb", "-k"):
136 assert any(flag.startswith(p) or p.startswith(flag) for p in prefixes), (
137 f"Safe flag {flag!r} is not covered by _SAFE_PYTEST_FLAG_PREFIXES"
138 )
139
140 def test_rootdir_not_in_safe_prefixes(self) -> None:
141 prefixes = set(_SAFE_PYTEST_FLAG_PREFIXES)
142 assert not any("rootdir" in p for p in prefixes)
143
144 def test_import_mode_not_in_safe_prefixes(self) -> None:
145 prefixes = set(_SAFE_PYTEST_FLAG_PREFIXES)
146 assert not any("import-mode" in p for p in prefixes)
File History 1 commit
sha256:f8e686793bb93114c2923d0d294162d13b4e6f4d57ae0f6cbc1e0d493e80f965 fix: ls-remote signing identity uses resolved remote URL Sonnet 4.6 patch 11 days ago