diff --git a/core/harness/command_guard.py b/core/harness/command_guard.py index 60dd5422..d5111b74 100644 --- a/core/harness/command_guard.py +++ b/core/harness/command_guard.py @@ -155,7 +155,7 @@ def screen_command(command: str) -> str | None: continue if not tokens: continue - reason = _classify(tokens[0].lower(), tokens[1:]) + reason = _classify(_program(tokens[0]), tokens[1:]) if reason is not None: return reason diff --git a/tests/test_command_guard.py b/tests/test_command_guard.py index 363f3c5c..3a381db8 100644 --- a/tests/test_command_guard.py +++ b/tests/test_command_guard.py @@ -83,3 +83,50 @@ def test_unparseable_command_defers_to_sandbox(): # Unbalanced quotes can't be tokenised; the screen must not raise and must # pass the command through (None) so the sandbox stays the boundary. assert screen_command('echo "unterminated') is None + + +@pytest.mark.parametrize( + "command", + [ + "/bin/rm -rf /", + "/usr/bin/rm -rf /tmp/x", + "/bin/rm -r -f /tmp/x", # split flags behind a path + "./rm --recursive --force /tmp/x", + "/usr/bin/sudo reboot", + "/bin/dd if=/dev/zero of=/dev/sda", + "/sbin/mkfs.ext4 /dev/sda1", + "/bin/chmod 777 /etc", + "/usr/bin/chmod a+rwx /etc", + "C:/Windows/System32/shutdown.exe /s", # Windows extension stripping + "/usr/bin/rm.exe -rf C:/temp", + "echo hi && /bin/rm -rf /", # path-qualified stage in a sequence + "ls | /usr/bin/rm -rf /", # path-qualified stage in a pipeline + ], +) +def test_blocks_path_qualified_destructive_commands(command): + """An absolute or relative path to the program must not defeat the screen. + + ``screen_egress`` / ``screen_install`` resolve the program through + ``_program()``, which drops the directory and any Windows extension before + matching. ``screen_command`` compared ``tokens[0].lower()`` directly, so the + very same command was classified when typed bare and passed when typed with + its path — ``/bin/rm -rf /`` slipped past the check that ``rm -rf /`` + trips. + """ + + assert screen_command(command) is not None + + +@pytest.mark.parametrize( + "command", + [ + "/bin/ls -la", # path-qualified but benign + "/usr/bin/rm notes.txt", # plain remove stays allowed + "/usr/local/bin/python train.py", + "/bin/echo done && /bin/ls -la", # benign path-qualified sequence + ], +) +def test_allows_path_qualified_benign_commands(command): + """Normalising the program must not turn every absolute path into a hit.""" + + assert screen_command(command) is None