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
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:
1 parent
d43e15e8db
commit
7ec427c743
3 files changed
+60
-9
No files matched your search
@@ -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"
|
||||
Reference in new issue
Block a user