fix(threat-scanner): close reviewer-noted ssh_access_write bypass shapes
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)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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"),
|
||||
|
||||
Reference in New Issue
Block a user