From fe68349cd64fb89bdc62e46ae2265b82f711aafd Mon Sep 17 00:00:00 2001 From: Martin Mogis Date: Tue, 25 Aug 2026 11:52:07 +0000 Subject: [PATCH] fix(threat-scanner): close reviewer-noted ssh_access_write bypass shapes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extend the write-verb alternation with mv/install/printf/dd/scp/rsync/ln, allow short option clusters between verb and path, and add a bare-leading- redirect branch (a > ~/.ssh/... line carries no verb word at all). Prose that names a write primitive before an SSH path still fails closed — a false positive costs a review, a false negative is a backdoor. (cherry picked from commit 4865b96ca3c8661c0ea6f5c479c198ccf68f86c4) --- tests/tools/test_threat_patterns.py | 55 +++++++++++++++++++++++++++++ tools/threat_patterns.py | 3 +- 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/tests/tools/test_threat_patterns.py b/tests/tools/test_threat_patterns.py index b4ade49bf8..9517b304bf 100644 --- a/tests/tools/test_threat_patterns.py +++ b/tests/tools/test_threat_patterns.py @@ -339,3 +339,58 @@ class TestSshAccessWritePattern: # verb gating on ssh_access_write. findings = scan_for_threats("documented ~/.ssh/authorized_keys layout", scope="strict") assert "ssh_backdoor" in findings + + +class TestSshAccessWriteExtendedVerbs: + """Reviewer-noted bypass shapes: mv/install/printf/dd/scp/rsync/ln and a + bare leading redirect carry no `echo/cat`-style verb the original + alternation recognised, yet are pure write primitives.""" + + def test_extended_write_verbs_flag(self): + for text in ( + "mv /tmp/evil $HOME/.ssh/id_rsa", + "install -m 600 /tmp/key ~/.ssh/id_ed25519", + "printf 'ssh-ed25519 AAAA' >> ~/.ssh/authorized_keys", + "dd if=/tmp/key of=$HOME/.ssh/id_rsa", + "scp evil.sh user@host:~/.ssh/", + "rsync -a /tmp/keys/ ~/.ssh/", + "ln -sf /tmp/evil $HOME/.ssh/authorized_keys", + "mv -f /tmp/stolen ~/.ssh/config", + ): + findings = scan_for_threats(text, scope="strict") + assert "ssh_access_write" in findings, text + + def test_bare_leading_redirect_flags(self): + findings = scan_for_threats( + "some-command\n> ~/.ssh/authorized_keys_backup", scope="strict" + ) + assert "ssh_access_write" in findings + + def test_flagged_verbs_with_flags_do_not_leak(self): + # rsync -a / mv -f style: options between verb and path must not + # break the match, but read-only prose stays clean. + findings = scan_for_threats( + "rsync -av --delete /tmp/keys/ ~/.ssh/", scope="strict" + ) + assert "ssh_access_write" in findings + + def test_documentation_stays_clean(self): + for text in ( + "Your public key belongs in ~/.ssh on the server, not in the repo", + "Rotate credentials quarterly; ~/.ssh holds the private material", + "Keys under ~/.ssh must have 600 permissions", + "The recovery doc explains where ~/.ssh sits in the backup set", + ): + findings = scan_for_threats(text, scope="strict") + ssh_findings = [f for f in findings if f.startswith("ssh_access")] + assert not ssh_findings, (text, ssh_findings) + + def test_verb_word_prose_fails_closed(self): + # Documented trade-off: prose that names a write primitive AND the + # SSH path still flags. A false positive costs a human glance; a + # false negative is a backdoor. Fail-closed is the contract. + findings = scan_for_threats( + "Use `scp` to copy your public key to ~/.ssh on the server", + scope="strict", + ) + assert "ssh_access_write" in findings diff --git a/tools/threat_patterns.py b/tools/threat_patterns.py index 02df94d035..54ce931468 100644 --- a/tools/threat_patterns.py +++ b/tools/threat_patterns.py @@ -80,7 +80,8 @@ _PATTERNS: List[Tuple[str, str, str]] = [ # ── Persistence / SSH backdoor (strict scope — memory + skills) ── (r'authorized_keys', "ssh_backdoor", "strict"), - (r'(?:echo|cat|cp|tee|append|add|write|>>?)\s+[^\n]{0,512}(?:\$HOME/\.ssh|~/\.ssh)', "ssh_access_write", "strict"), + (r'(?:echo|cat|cp|mv|dd|tee|install|printf|rsync|scp|ln|append|add|write|>>?)\s*(?:-[a-zA-Z]+\s+)*[^\n]{0,512}(?:\$HOME/\.ssh|~/\.ssh)', "ssh_access_write", "strict"), + (r'(?m)^[^\n]{0,256}(?a-z])>>?\s*(?:\$HOME/\.ssh|~/\.ssh)', "ssh_access_write", "strict"), (r'\$HOME/\.hermes/\.env|\~/\.hermes/\.env', "hermes_env", "strict"), (rf'{_MODIFY}(?:AGENTS\.md|CLAUDE\.md|\.cursorrules|\.clinerules)', "agent_config_mod", "strict"), (rf'{_MODIFY}\.hermes/(config\.yaml|SOUL\.md)', "hermes_config_mod", "strict"),