Files
buzz/desktop/src-tauri
npub17jjz49l9jjmhhk7cac63j8yt9z555n9cw8vk7v5jz4vzw4ppld5qgj57ccandtlongwell-block c3c705583b feat(desktop): fence the terminal child's environment, PATH, and shell
A PTY child inherits its parent's environment by default, and Buzz's own
process holds the user's nsec. `CommandBuilder::new()` pre-seeds its env map
from `std::env::vars_os()` (`cmdbuilder.rs:218` -> `:74`), so the naive spawn
hands a live shell 70 variables including `BUZZ_PRIVATE_KEY`: press the toggle,
type `env`, read the signing key off the screen. Verified against a real PTY,
not inferred.

Three fences, each with the failure it exists to prevent:

* Environment: `env_clear()` first, then rebuild from an allowlist. Order is
  load-bearing and the reverse fails silently -- clearing after populating
  yields an empty environment and no error -- so the gate asserts both
  directions. Allowlist rather than denylist because a denylist is only as
  current as the last person who remembered to extend it.

* PATH: constructed, never inherited, never filtered. Buzz runs under Hermit
  activation, so an inherited PATH makes the user's `cargo` our pinned build
  toolchain -- on Linux, where no `path_helper` reorders it, at the front. The
  test seeds a uniquely-named executable into the parent PATH and proves the
  child cannot resolve it, rather than comparing PATH strings.

* Shell: `$SHELL` -> passwd -> `/bin/sh`, each candidate validated as an
  executable regular file. `access(X_OK)` alone accepts a directory, and a
  `$SHELL` of `/tmp` produces a child that aborts with a Rust runtime panic
  while `get_shell()`, `spawn_command()`, and every layer above report success.
  Raw mode bits alone accept a file the caller cannot execute. The conjunction
  is the check. The resolved shell is injected as `SHELL`, not inherited: those
  differ in exactly the cases the fallback chain exists for.

GUI context crosses a trust boundary. A channel name is attacker-controlled and
lands in a variable shells interpolate into prompts, so `BUZZ_CHANNEL` is
validated against a conservative character class and, on rejection, replaced by
the channel UUID rather than stripped -- a stripped `$(evil)` becomes `evil`,
which looks like a real channel.

Nineteen fixtures, each shown failing under the mutation it exists to catch:
deleting `env_clear`, reordering it to the end, inheriting PATH, denylisting
instead of allowlisting, dropping either half of the executability predicate,
inheriting `SHELL`, sniffing the shell's name for login flags, removing the
passwd candidate, weakening the shared validator, skipping channel-name
validation, stripping instead of substituting, dropping the length cap,
letting the display name reach `BUZZ_CHANNEL_ID`, emitting an empty
`BUZZ_THREAD_ID`, accepting `=` in a key, and adding a credential to the
desktop crate's reserved list without covering it here.

Co-authored-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>
Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>
2026-08-01 20:43:26 -04:00
..
…