Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Cargo dependency updates.
- Switched (back) to the https://snix.dev/ `nix_compat` crate for internal nix
json log parsing.
- Executed commands are encoded in base64 to harden the strings and help them
survive cross-shell parsing errors.

### Fixed

- Status bar is cleaned every time after execution is completed.
- `deployment.privilegeEscalationCommand` not being consistently applied.
- Fixed garnix docs links in documentation.
- Forces `bash` instead of remote user's potentially unsupported shell. This bug
was causing strange and hard to diagnose issues.
Expand Down
15 changes: 13 additions & 2 deletions crates/core/src/commands/noninteractive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use crate::{
errors::{CommandError, HiveLibError},
hive::node::SharedTarget,
};
use base64::{Engine, engine::general_purpose::STANDARD};
use itertools::Itertools;
use tokio::{
io::{AsyncWriteExt, BufReader},
Expand Down Expand Up @@ -55,10 +56,20 @@ pub(crate) async fn non_interactive_command_with_env<S: AsRef<str>>(
}
);

let encoded = STANDARD.encode(&command_string);

// keep_stdin_open requires that bash not have its stdin messed up by
// base64, so a special case is created specifically for that.
let command_string = if let Some(escalation_command) = &arguments.privilege_escalation_command {
format!("{escalation_command} sh -c '{command_string}'")
} else {
if arguments.keep_stdin_open {
format!("{escalation_command} sh -c '{command_string}'")
} else {
format!("{escalation_command} sh -c 'echo {encoded} | base64 -d | bash'")
}
} else if arguments.keep_stdin_open {
command_string
} else {
format!("echo {encoded} | base64 -d | bash")
};

debug!("{command_string}");
Expand Down
24 changes: 18 additions & 6 deletions crates/core/src/commands/pty/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use crate::commands::pty::output::{WatchStdoutArguments, handle_pty_stdout};
use crate::hive::node::SharedTarget;
use crate::status::{UI_SENDER, UiMessage};
use aho_corasick::PatternID;
use base64::{Engine, engine::general_purpose::STANDARD};
use itertools::Itertools;
use nix::sys::termios::{LocalFlags, SetArg, Termios, tcgetattr, tcsetattr};
use nix::unistd::pipe;
Expand Down Expand Up @@ -244,9 +245,9 @@ pub(crate) async fn interactive_command_with_env<S: AsRef<str>>(
async fn print_authenticate_warning<S: AsRef<str>>(
arguments: &CommandArguments<S>,
) -> Result<(), HiveLibError> {
if !arguments.is_elevated() {
let Some(ref privilege_escalation_command) = arguments.privilege_escalation_command else {
return Ok(());
}
};

let target_display = if let Some(ref target) = arguments.target {
let target = target.0.read().await;
Expand All @@ -264,7 +265,8 @@ async fn print_authenticate_warning<S: AsRef<str>>(
if let Some(tx) = UI_SENDER.get() {
let _ = tx.send(UiMessage::LogLine(
format!(
"{target_display} | Authenticate for \"sudo {}\":\n",
"{target_display} | Authenticate {} \"{}\":\n",
privilege_escalation_command,
arguments.command_string.as_ref()
)
.into_bytes(),
Expand Down Expand Up @@ -330,10 +332,20 @@ async fn build_command<S: AsRef<str>>(
command
};

if arguments.is_elevated() {
command.arg(format!("sudo -u root -- bash -c '{command_string}'"));
let encoded = STANDARD.encode(command_string);

if let Some(escalation_command) = &arguments.privilege_escalation_command {
if arguments.keep_stdin_open {
command.arg(format!("{escalation_command} sh -c '{command_string}'"));
} else {
command.arg(format!(
"{escalation_command} sh -c 'echo {encoded} | base64 -d | bash'"
));
Comment on lines +337 to +343
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the file structure and the specific lines mentioned
cd crates/core/src/commands/pty && wc -l mod.rs

Repository: forallsys/wire

Length of output: 69


🏁 Script executed:

# Read the context around lines 337-343 to understand the full scope
head -n 360 crates/core/src/commands/pty/mod.rs | tail -n 50

Repository: forallsys/wire

Length of output: 1466


🏁 Script executed:

# Search for IO_SUBS definition and usage
rg "IO_SUBS" crates/core/src/commands/pty/

Repository: forallsys/wire

Length of output: 272


🏁 Script executed:

# Search for command_string definition to understand what it contains
rg -B5 -A5 "let command_string" crates/core/src/commands/pty/mod.rs

Repository: forallsys/wire

Length of output: 605


🏁 Script executed:

# Search for encoded definition to understand the Base64 payload
rg -B5 -A5 "let encoded" crates/core/src/commands/pty/mod.rs

Repository: forallsys/wire

Length of output: 381


Use bash with Base64 encoding in the elevated keep_stdin_open path.

Lines 338–339 pass command_string through sh -c, but command_string always includes IO_SUBS, which contains bash-only process substitution (>(...)). On systems where /bin/sh is not bash, this will fail. Additionally, the approach is vulnerable to quote loss if command_string contains embedded single quotes.

The non-elevated keep_stdin_open path works because it runs the command directly without invoking a shell. The elevated non-keep_stdin_open path works because it uses the Base64 payload and explicitly pipes to bash. Apply the same Base64 + bash pattern to the elevated keep_stdin_open path:

Suggested fix
     if let Some(escalation_command) = &arguments.privilege_escalation_command {
         if arguments.keep_stdin_open {
-            command.arg(format!("{escalation_command} sh -c '{command_string}'"));
+            command.arg(format!(
+                r#"{escalation_command} env WIRE_PTY_COMMAND_B64={encoded} bash -c 'eval "$(printf %s "$WIRE_PTY_COMMAND_B64" | base64 -d)"'"#
+            ));
         } else {
             command.arg(format!(
                 "{escalation_command} sh -c 'echo {encoded} | base64 -d | bash'"
             ));
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/core/src/commands/pty/mod.rs` around lines 337 - 343, The elevated
keep_stdin_open branch currently passes command_string through "sh -c" which
breaks because command_string contains bash-only IO_SUBS and may lose quotes;
change that branch (the block that checks arguments.privilege_escalation_command
&& arguments.keep_stdin_open) to use the same Base64+bash pattern as the other
elevated branch: base64-encode command_string (the existing encoded variable)
and pass an argument like "{escalation_command} sh -c 'echo {encoded} | base64
-d | bash'" so the decoded payload runs under bash and avoids shell
quoting/compatibility issues.

}
} else if arguments.keep_stdin_open {
command.arg(command_string);
} else {
command.arg(format!("bash -c '{command_string}'"));
command.arg(format!("echo {encoded} | base64 -d | bash"));
}

Ok(command)
Expand Down
Loading