fix: never let root read the SSH configuration from stdin
CI / test (3.10) (push) Successful in 23s
CI / test (3.11) (push) Successful in 22s
CI / test (3.12) (push) Successful in 24s
CI / test (3.10) (pull_request) Successful in 23s
CI / test (3.11) (pull_request) Successful in 25s
CI / test (3.12) (pull_request) Successful in 24s

A driver gains root with sudo -S and puts the password in front of stdin.
A sudo that does not prompt -- NOPASSWD, a cached timestamp -- leaves it
there, and the cat under sudo wrote it into the drop-in as its first line.
Seen on the first real run (NetOrk/netork#1027): sshd -t rejected the file
and the rollback removed it, but the password was in a 0644 file for a
moment and in the error the caller showed.

The content now goes without privilege to a mktemp file only the login user
can read; root installs that file (mode 644, root-owned) and removes it.
The fake host in the tests prepends the password to every privileged stdin,
as a non-prompting sudo does.

Bumps to 4.5.1. NetOrk/netork#1027, NetOrk/netork#1029
This commit is contained in:
christianmanivong committed 2026-10-11 23:48:51 +02:00
1 parent d43e15e8db
commit 7ec427c743
3 files changed
+60 -9

No files matched your search

+38 -4
View File
@@ -188,10 +188,29 @@ class FakeHost(OpenSshServerMixin):
eff[kind] = parts[1].split(",")
return eff
#: What a real ``sudo -S`` leaves on stdin when it does not prompt (NOPASSWD,
#: or a cached timestamp): the password the driver put in front of the data.
SUDO_PASSWORD = "hunter2"
def run_command(self, command: str, *, privileged: bool = False, timeout: float = 60,
stdin: Optional[bytes] = None) -> CommandResult:
self.ran.append(command)
if privileged and stdin is not None:
stdin = f"{self.SUDO_PASSWORD}\n".encode() + stdin
m = re.search(r"mktemp", command)
if m:
assert not privileged, "the content must reach a file without sudo in the way"
path = f"/tmp/netork-ssh.{len(self.files)}"
self.files[path] = (stdin or b"").decode()
return CommandResult(path, "", 0)
assert privileged, command
m = re.search(r"install -m 644 -- '?([^' ]+)'? '?([^' ]+)'?", command)
if m:
src, tmp = m.group(1), m.group(2)
target = re.search(r"mv -f -- '?[^' ]+'? '?([^' ;]+)'?", command).group(1)
self.files[target] = self.files.pop(src)
assert tmp.endswith(".netork-tmp")
return CommandResult("", "", 0)
if command.startswith("sshd -T") or "sshd -T" in command:
eff = self._effective()
out = "".join(f"{d} {','.join(eff[k])}\n" for d, k in _DIRECTIVE.items())
@@ -204,10 +223,6 @@ class FakeHost(OpenSshServerMixin):
if path in self.files:
return CommandResult(self.files[path], "", 0)
return CommandResult("", "No such file", 1)
m = re.search(r"mv -f -- \"\$tmp\" '?([^' ]+)'?", command)
if m:
self.files[m.group(1)] = (stdin or b"").decode()
return CommandResult("", "", 0)
m = re.match(r"rm -f -- '?([^' ]+)'?$", command)
if m:
self.files.pop(m.group(1), None)
@@ -301,3 +316,22 @@ def test_main_config_second_fix_keeps_what_the_first_set():
eff = host._effective()
assert "ecdsa-sha2-nistp256" not in eff["hostkey"]
assert "hmac-sha1" not in eff["mac"]
def test_the_sudo_password_never_reaches_a_file():
"""napalm-linux puts the sudo password in front of stdin for ``sudo -S``. A
sudo that does not prompt leaves it there, and a ``cat`` under sudo wrote it
into the drop-in (NetOrk/netork#1027, first run on a real host). The content
now reaches a file of the login user first; root only installs that file."""
host = FakeHost()
host.harden_ssh_server({"mac": ["hmac-sha1"]})
assert host.SUDO_PASSWORD not in host.files[DROPIN_PATH]
assert host.files[DROPIN_PATH].startswith("# Written by netOrk")
assert not [p for p in host.files if p.startswith("/tmp/")]
def test_restoring_an_earlier_dropin_leaks_nothing_either():
host = FakeHost()
host.files[DROPIN_PATH] = "# netOrk\nHostKeyAlgorithms ssh-ed25519\n"
host.harden_ssh_server({"mac": ["hmac-sha1"]}, verify=lambda: False)
assert host.files[DROPIN_PATH] == "# netOrk\nHostKeyAlgorithms ssh-ed25519\n"