mirror of
https://github.com/NCBM/plyngent.git
synced 2026-11-08 16:37:55 +08:00
core/tools: announce a remote shell instead of judging it
`ssh prod` runs no command this scan can resolve, and what is typed or piped into the shell it opens never reaches any gate — `write_pty` and `run_argv(stdin=…)` are both unchecked input. The tool description said nothing about it, so the call looked like any other. The hop is now reported even when no command follows (`RemoteTarget.shell`), and the review announces it: `remote shell on 'prod' (ssh with no command; its input is not checked)`. It is a soft confirm like the other reviews (YOLO may skip it, an escalation behind a hop still may not) — the client itself stays outside the denylist, since there is no program to name.
This commit is contained in:
@@ -275,6 +275,8 @@ class RemoteTarget:
|
||||
host: str
|
||||
#: ``-o BatchMode=yes``: the client never prompts, so the command runs.
|
||||
batch: bool = False
|
||||
#: No command followed: the client opens a shell whose input is not scanned.
|
||||
shell: bool = False
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -346,7 +348,8 @@ def unwrap_command(argv: Sequence[str]) -> UnwrappedCommand:
|
||||
``ssh`` / ``mosh`` are unwrapped the same way — the program under review is
|
||||
the one after the destination (``ssh prod rm -rf /`` runs ``rm``), and the
|
||||
hop is reported in :attr:`UnwrappedCommand.remote`. A client with no command
|
||||
opens a remote shell, which this scan does not look into.
|
||||
opens a remote shell (``remote.shell``); nothing behind it can be resolved
|
||||
here.
|
||||
"""
|
||||
parts = list(argv)
|
||||
wrappers: list[str] = []
|
||||
@@ -359,6 +362,8 @@ def unwrap_command(argv: Sequence[str]) -> UnwrappedCommand:
|
||||
start = _skip_remote(name, parts, index + 1)
|
||||
if start is not None:
|
||||
remote = start.target
|
||||
if start.index is None:
|
||||
break # a remote shell: the client is the whole command
|
||||
wrappers.append(name)
|
||||
index = start.index
|
||||
continue
|
||||
@@ -384,12 +389,13 @@ def unwrap_command(argv: Sequence[str]) -> UnwrappedCommand:
|
||||
class _RemoteStart:
|
||||
"""Where a remote client's own command starts, and the hop it reaches."""
|
||||
|
||||
index: int
|
||||
#: Index of the remote command, or ``None`` when only a shell opens.
|
||||
index: int | None
|
||||
target: RemoteTarget
|
||||
|
||||
|
||||
def _skip_remote(name: str, parts: list[str], start: int) -> _RemoteStart | None:
|
||||
"""Where the command *name* runs elsewhere starts; ``None`` for a bare shell.
|
||||
"""Where the command *name* runs elsewhere starts; ``None`` without a hop.
|
||||
|
||||
The destination is the first token that is not one of the client's own
|
||||
options (``ssh -o BatchMode=yes -p 2222 [user@]host …``); the command follows
|
||||
@@ -415,7 +421,7 @@ def _skip_remote(name: str, parts: list[str], start: int) -> _RemoteStart | None
|
||||
if name == "mosh" and index < len(parts) and parts[index] == "--":
|
||||
index += 1
|
||||
if index >= len(parts):
|
||||
return None
|
||||
return _RemoteStart(index=None, target=RemoteTarget(wrapper=name, host=host, batch=batch, shell=True))
|
||||
return _RemoteStart(index=index, target=RemoteTarget(wrapper=name, host=host, batch=batch))
|
||||
|
||||
|
||||
|
||||
@@ -63,15 +63,20 @@ def _command_reason(argv: Sequence[str], *, via: str) -> DangerReason | None:
|
||||
A command on another machine (``ssh``/``mosh``) is reviewed by the program it
|
||||
runs there — the client's destination and options are skipped. With
|
||||
``-o BatchMode=yes`` the client cannot prompt at either end, so the command
|
||||
is always confirmed, whatever program it is.
|
||||
is always confirmed, whatever program it is. A client with no command opens a
|
||||
remote shell, which is announced: nothing typed or piped into it can be
|
||||
checked, so the human decides before it starts.
|
||||
"""
|
||||
scan = unwrap_command(argv)
|
||||
interpreter = is_interpreter(scan.base)
|
||||
findings = [f"{name} (detached run)" for name in scan.self_review]
|
||||
if interpreter:
|
||||
findings.append(f"interpreter {scan.base!r}")
|
||||
if scan.remote is not None and scan.remote.batch:
|
||||
findings.append(f"remote command on {scan.remote.host!r} (BatchMode=yes, no prompt on either end)")
|
||||
remote = scan.remote
|
||||
if remote is not None and remote.shell:
|
||||
findings.append(f"remote shell on {remote.host!r} ({remote.wrapper} with no command; its input is not checked)")
|
||||
elif remote is not None and remote.batch:
|
||||
findings.append(f"remote command on {remote.host!r} (BatchMode=yes, no prompt on either end)")
|
||||
findings.extend(f"{span.label} runs a command line" for span in scan.command_strings)
|
||||
findings = list(dict.fromkeys(findings))
|
||||
if not findings:
|
||||
|
||||
Reference in New Issue
Block a user