diff --git a/tests/tools/test_approval.py b/tests/tools/test_approval.py index b7598380708..49d13b0cd60 100644 --- a/tests/tools/test_approval.py +++ b/tests/tools/test_approval.py @@ -633,6 +633,43 @@ class TestProjectSensitiveCopyPattern: assert desc is None +class TestSensitiveCopyMovePattern: + """cp/mv/install OVERWRITING ~/.ssh/*, credential files (~/.netrc etc.), + shell rc files, or ~/.hermes/config.yaml/.env must require approval — the + tee/redirection forms were already gated (#14639 family / commit 4e9d886d), + but cp/mv/install on these targets was an unpaired half-door (key implant / + shell-rc command injection slipped through auto-approve).""" + + def test_cp_to_ssh_authorized_keys(self): + dangerous, key, desc = detect_dangerous_command("cp /tmp/evil ~/.ssh/authorized_keys") + assert dangerous is True + assert key is not None + + def test_mv_to_ssh_private_key(self): + dangerous, key, desc = detect_dangerous_command("mv /tmp/k ~/.ssh/id_rsa") + assert dangerous is True + + def test_install_to_netrc(self): + dangerous, key, desc = detect_dangerous_command("install -m600 /tmp/c ~/.netrc") + assert dangerous is True + + def test_cp_to_bashrc(self): + dangerous, key, desc = detect_dangerous_command("cp /tmp/e ~/.bashrc") + assert dangerous is True + + def test_cp_to_hermes_config(self): + dangerous, key, desc = detect_dangerous_command("cp /tmp/evil.yaml ~/.hermes/config.yaml") + assert dangerous is True + + def test_cp_from_ssh_is_safe(self): + dangerous, key, desc = detect_dangerous_command("cp ~/.ssh/config /tmp/x") + assert dangerous is False + + def test_cp_unrelated_files_safe(self): + dangerous, key, desc = detect_dangerous_command("cp a.txt b.txt") + assert dangerous is False + + class TestProjectSensitiveTeePattern: def test_tee_to_local_dotenv_requires_approval(self): dangerous, key, desc = detect_dangerous_command("printenv | tee .env.local") diff --git a/tools/approval.py b/tools/approval.py index 3baf1fd10d5..9cc7adcc109 100644 --- a/tools/approval.py +++ b/tools/approval.py @@ -441,6 +441,20 @@ DANGEROUS_PATTERNS = [ # /private/etc/ mirror). (rf'\b(cp|mv|install)\b.*\s{_SYSTEM_CONFIG_PATH}', "copy/move file into system config path"), (rf'\b(cp|mv|install)\b.*\s["\']?{_PROJECT_SENSITIVE_WRITE_TARGET}["\']?{_COMMAND_TAIL}', "overwrite project env/config file"), + # cp/mv/install OVERWRITING a sensitive credential/SSH/shell-rc/Hermes file. + # The tee/redirection patterns above already gate _SENSITIVE_WRITE_TARGET + # (~/.ssh/*, ~/.netrc/.pgpass/.npmrc/.pypirc, shell rc files, + # ~/.hermes/config.yaml/.env), but cp/mv/install was only paired for /etc and + # project-relative env/config — so `cp evil ~/.ssh/authorized_keys` (key + # implant), `cp creds ~/.netrc`, and `cp evil ~/.bashrc` (login-time command + # injection) slipped through with auto-approve. Same unpaired-door rationale + # as #14639 / the sed-tee-redirect pairing on these targets. + # Anchor the sensitive target to the command tail so this fires on the + # DESTINATION (last arg) only — `cp evil ~/.ssh/authorized_keys` is gated, + # but reading OUT of a sensitive path (`cp ~/.ssh/config /tmp/x`) stays safe. + # The trailing `[^\s"\']*` consumes the rest of the destination filename + # (e.g. `authorized_keys` after the `~/.ssh/` fragment). + (rf'\b(cp|mv|install)\b.*\s["\']?{_SENSITIVE_WRITE_TARGET}[^\s"\']*["\']?{_COMMAND_TAIL}', "copy/move file into sensitive credential/SSH/shell-rc path"), (rf'\bsed\s+-[^\s]*i.*\s{_SYSTEM_CONFIG_PATH}', "in-place edit of system config"), (rf'\bsed\s+--in-place\b.*\s{_SYSTEM_CONFIG_PATH}', "in-place edit of system config (long flag)"), # In-place edit of a Hermes-managed security file (~/.hermes/config.yaml or