diff --git a/tests/tools/test_approval.py b/tests/tools/test_approval.py index 49d13b0cd60..683e621b044 100644 --- a/tests/tools/test_approval.py +++ b/tests/tools/test_approval.py @@ -369,6 +369,12 @@ class TestTeePattern: assert dangerous is True assert key is not None + def test_tee_absolute_home_bashrc(self): + bashrc = Path.home() / ".bashrc" + dangerous, key, desc = detect_dangerous_command(f"echo x | tee {bashrc}") + assert dangerous is True + assert key is not None + def test_tee_custom_hermes_home_env(self): dangerous, key, desc = detect_dangerous_command("echo x | tee $HERMES_HOME/.env") assert dangerous is True @@ -560,11 +566,37 @@ class TestSensitiveRedirectPattern: assert dangerous is True assert key is not None + def test_append_to_absolute_home_ssh_authorized_keys(self): + authorized_keys = Path.home() / ".ssh" / "authorized_keys" + dangerous, key, desc = detect_dangerous_command(f"cat key >> {authorized_keys}") + assert dangerous is True + assert key is not None + def test_append_to_tilde_ssh_authorized_keys(self): dangerous, key, desc = detect_dangerous_command("cat key >> ~/.ssh/authorized_keys") assert dangerous is True assert key is not None + def test_redirect_to_absolute_home_bashrc(self): + bashrc = Path.home() / ".bashrc" + dangerous, key, desc = detect_dangerous_command(f"echo 'alias ll=\"ls -la\"' > {bashrc}") + assert dangerous is True + assert key is not None + + def test_redirect_to_home_set_after_import(self, monkeypatch, tmp_path): + late_home = tmp_path / "late-home" + late_home.mkdir() + monkeypatch.setenv("HOME", str(late_home)) + + dangerous, key, desc = detect_dangerous_command(f"echo x > {late_home}/.bashrc") + assert dangerous is True + assert key is not None + + def test_redirect_to_other_absolute_home_bashrc_is_not_current_user_sensitive(self): + dangerous, key, desc = detect_dangerous_command("echo x > /tmp/not-current-home/.bashrc") + assert dangerous is False + assert key is None + def test_redirect_to_safe_tmp_file(self): dangerous, key, desc = detect_dangerous_command("echo hello > /tmp/output.txt") assert dangerous is False diff --git a/tools/approval.py b/tools/approval.py index 9cc7adcc109..ce6be6e8f98 100644 --- a/tools/approval.py +++ b/tools/approval.py @@ -561,6 +561,11 @@ def _normalize_command_for_detection(command: str) -> str: command = re.sub(r'\\([^\n])', r'\1', command) # Strip empty-string literals that split tokens: r''m → rm, r"\"m → rm. command = re.sub(r"''|\"\"", '', command) + # Fold the current user's resolved absolute home path into ~/ at detection + # time so static user-sensitive patterns catch /home/alice/.bashrc the same + # way they catch ~/.bashrc. Do not snapshot this at import time: tests and + # profile/session launchers can set HOME after this module is imported. + command = _rewrite_resolved_user_home(command) # Fold the resolved absolute active-profile home path into the canonical # ~/.hermes/ form so the Hermes config/env patterns catch it. In Docker and # gateway deployments the agent often references the resolved absolute path @@ -572,6 +577,36 @@ def _normalize_command_for_detection(command: str) -> str: return command +def _rewrite_resolved_user_home(command: str) -> str: + """Rewrite the current user's absolute home prefix to ``~/``. + + Resolves HOME at detection time, including its symlink-resolved form, so + terminal commands targeting absolute home paths are checked by the same + static patterns as tilde and $HOME forms. No-op when HOME is unset or + degenerate. + """ + try: + home = os.path.expanduser("~") + candidates = [ + home.rstrip("/"), + os.path.realpath(home).rstrip("/"), + ] + except Exception: + return command + seen: set[str] = set() + for path in candidates: + if not path or path in seen: + continue + seen.add(path) + # Require an absolute path below root so a bad HOME cannot rewrite the + # whole filesystem namespace. + normalized = path.rstrip("/") + if not normalized.startswith("/") or normalized.count("/") < 2: + continue + command = command.replace(normalized + "/", "~/") + return command + + def _rewrite_resolved_hermes_home(command: str) -> str: """Rewrite the resolved absolute Hermes home prefix to ``~/.hermes/``.