fix: keep the sudo password out of files written as root (0.2.1)
CI / test (3.10) (push) Successful in 27s
CI / test (3.11) (push) Successful in 27s
CI / test (3.12) (push) Successful in 25s
CI / test (3.10) (pull_request) Successful in 29s
CI / test (3.11) (pull_request) Successful in 24s
CI / test (3.12) (pull_request) Successful in 25s

run_command prepended the sudo password to the caller's stdin. A sudo
that does not prompt left it for the command, and fix_snmp's
'cat > snmpd.conf' wrote it as the configuration's first line before
making the file world-readable.

run_command now refuses stdin to a privileged command while a sudo
password is set. fix_snmp stages the configuration in a mktemp file
without privilege, and root installs it next to the target and renames
it over, so nothing root runs follows a symlink.

Closes #8
This commit is contained in:
Christian Manivong committed 2026-10-16 11:48:25 +02:00
1 parent 992bc6d5e9
commit 707e048792
4 files changed
+122 -20

No files matched your search

+56 -13
View File
@@ -44,6 +44,25 @@ _SNMPD_CONF = (
"sysLocation Managed by netOrk\n"
"sysContact netork@localhost\n"
)
#: Why ``run_command`` refuses stdin to a privileged command while a sudo
#: password is set (#8).
_PRIVILEGED_STDIN = (
"run_command(privileged=True) takes no stdin while a sudo password is set: "
"sudo -S reads the password from stdin, and a sudo that does not prompt "
"(NOPASSWD, a cached timestamp) leaves it for the command, which would read it "
"as the first line of the data. Stage the data without privilege and have root "
"install that file."
)
#: Stages content without privilege: a fresh ``mktemp`` file only the login user
#: may read, never a fixed name another user could create first. Prints its path.
_STAGE_TEMPLATE = "/tmp/napalm-bsd-stage.XXXXXX"
_STAGE_PREFIX = _STAGE_TEMPLATE[: -len("XXXXXX")]
_STAGE = (
f"umask 077 && t=$(mktemp {_STAGE_TEMPLATE}) && "
'{ cat > "$t" && printf %s "$t" || { s=$?; rm -f -- "$t"; exit $s; }; }'
)
#: What a working agent answers sysDescr.0 with.
_SNMP_TYPES = ("STRING:", "INTEGER:", "OID:", "Timeticks:", "Hex-STRING:", "IpAddress:")
@@ -155,10 +174,38 @@ class BsdDriver(HostStatusMixin, OSDriver):
timeout: float = 60,
stdin: Optional[bytes] = None,
) -> CommandResult:
"""Run *command* on an exec channel: no PTY, stderr apart, a real exit code."""
"""Run *command* on an exec channel: no PTY, stderr apart, a real exit code.
:raises ValueError: for *stdin* to a privileged command while a sudo
password is set (see ``_PRIVILEGED_STDIN``).
"""
line, prefix = self._privileged(command, privileged)
data = (prefix or b"") + (stdin or b"") if (prefix or stdin) else None
return run_on_transport(self._transport(), line, stdin=data, timeout=timeout)
if prefix and stdin is not None:
raise ValueError(_PRIVILEGED_STDIN)
return run_on_transport(self._transport(), line, stdin=prefix or stdin, timeout=timeout)
def _install_file_as_root(self, path: str, content: str) -> CommandResult:
"""Replace *path* with *content*, as a new file owned by root, mode 644.
The content is staged without privilege (``_STAGE``), so it never follows
a sudo password on stdin. Root then installs the staged file next to *path*
and renames it over *path*; nothing root runs follows a symlink at *path*.
Nothing runs as root when staging failed.
"""
staged = self.run_command(_STAGE, timeout=30, stdin=content.encode())
lines = staged.stdout.strip().splitlines()
tmp = lines[-1] if lines else ""
if staged.exit_code != 0 or not tmp.startswith(_STAGE_PREFIX) or "/" in tmp[len(_STAGE_PREFIX):]:
reason = (staged.stderr or staged.stdout).strip() or f"could not stage {path}"
return CommandResult(reason, "", staged.exit_code or 1)
tmp_q, part_q, path_q = (quote(p) for p in (tmp, f"{path}.napalm-tmp", path))
directory = quote(path.rsplit("/", 1)[0] or "/")
return self.run_command(
f"mkdir -p {directory} && install -m 644 -o root -g wheel {tmp_q} {part_q}"
f" && mv -f {part_q} {path_q}; s=$?; rm -f {tmp_q} {part_q}; exit $s",
privileged=True,
timeout=30,
)
def open_stream(self, command: str, *, privileged: bool = False) -> ByteStream:
line, prefix = self._privileged(command, privileged)
@@ -420,18 +467,14 @@ class BsdDriver(HostStatusMixin, OSDriver):
(NetOrk/netork#800). Stops at the first step that fails.
"""
lines: list[str] = []
conf_dir = self.SNMPD_CONF.rsplit("/", 1)[0]
steps = (
("install", self.INSTALL_COMMAND.format(name="net-snmp"), None),
(
"config",
f"mkdir -p {conf_dir} && cat > {self.SNMPD_CONF} && chmod 644 {self.SNMPD_CONF}",
_SNMPD_CONF.encode(),
),
("service", self.SNMPD_START, None),
("install", lambda: self.run_command(
self.INSTALL_COMMAND.format(name="net-snmp"), privileged=True, timeout=600)),
("config", lambda: self._install_file_as_root(self.SNMPD_CONF, _SNMPD_CONF)),
("service", lambda: self.run_command(self.SNMPD_START, privileged=True, timeout=600)),
)
for label, command, stdin in steps:
result = self.run_command(command, privileged=True, timeout=600, stdin=stdin)
for label, step in steps:
result = step()
output = "\n".join(filter(None, (result.stdout.strip(), result.stderr.strip())))
lines.append(f"[{label}] {output[-300:]}".rstrip())
if result.exit_code != 0: