From 33283bfeb671155610ca63fbb88bc54fff04982c Mon Sep 17 00:00:00 2001 From: marshmallow Date: Thu, 30 Apr 2026 10:45:20 +1000 Subject: [PATCH 1/3] encode commands in base64 to reduce quote bugs --- CHANGELOG.md | 3 +++ crates/core/src/commands/noninteractive.rs | 7 +++++-- crates/core/src/commands/pty/mod.rs | 18 ++++++++++++------ 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4de16e8..93372e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/crates/core/src/commands/noninteractive.rs b/crates/core/src/commands/noninteractive.rs index 8ba07bb..4a5e743 100644 --- a/crates/core/src/commands/noninteractive.rs +++ b/crates/core/src/commands/noninteractive.rs @@ -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}, @@ -55,10 +56,12 @@ pub(crate) async fn non_interactive_command_with_env>( } ); + let encoded = STANDARD.encode(&command_string); + let command_string = if let Some(escalation_command) = &arguments.privilege_escalation_command { - format!("{escalation_command} sh -c '{command_string}'") + format!("{escalation_command} sh -c 'echo {encoded} | base64 -d | bash'") } else { - command_string + format!("echo {encoded} | base64 -d | bash") }; debug!("{command_string}"); diff --git a/crates/core/src/commands/pty/mod.rs b/crates/core/src/commands/pty/mod.rs index a2ae67c..546fc7c 100644 --- a/crates/core/src/commands/pty/mod.rs +++ b/crates/core/src/commands/pty/mod.rs @@ -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; @@ -244,9 +245,9 @@ pub(crate) async fn interactive_command_with_env>( async fn print_authenticate_warning>( arguments: &CommandArguments, ) -> 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; @@ -264,7 +265,8 @@ async fn print_authenticate_warning>( 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(), @@ -330,10 +332,14 @@ async fn build_command>( 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 { + command.arg(format!( + "{escalation_command} sh -c 'echo {encoded} | base64 -d | bash'" + )); } else { - command.arg(format!("bash -c '{command_string}'")); + command.arg(format!("echo {encoded} | base64 -d | bash")); } Ok(command) From a1753468896c55660783f3e564f1392ee26779e1 Mon Sep 17 00:00:00 2001 From: marshmallow Date: Thu, 30 Apr 2026 14:37:34 +1000 Subject: [PATCH 2/3] try feeding <&0 into bash --- crates/core/src/commands/noninteractive.rs | 14 ++++++++++++-- crates/core/src/commands/pty/mod.rs | 16 ++++++++++++---- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/crates/core/src/commands/noninteractive.rs b/crates/core/src/commands/noninteractive.rs index 4a5e743..585f581 100644 --- a/crates/core/src/commands/noninteractive.rs +++ b/crates/core/src/commands/noninteractive.rs @@ -58,10 +58,20 @@ pub(crate) async fn non_interactive_command_with_env>( 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 'echo {encoded} | base64 -d | bash'") + if arguments.keep_stdin_open { + format!("{escalation_command} sh -c '{command_string}'") + } else { + format!("{escalation_command} sh -c 'echo {encoded} | base64 -d | bash'") + } } else { - format!("echo {encoded} | base64 -d | bash") + if arguments.keep_stdin_open { + command_string + } else { + format!("echo {encoded} | base64 -d | bash") + } }; debug!("{command_string}"); diff --git a/crates/core/src/commands/pty/mod.rs b/crates/core/src/commands/pty/mod.rs index 546fc7c..0bdfd8c 100644 --- a/crates/core/src/commands/pty/mod.rs +++ b/crates/core/src/commands/pty/mod.rs @@ -335,11 +335,19 @@ async fn build_command>( let encoded = STANDARD.encode(command_string); if let Some(escalation_command) = &arguments.privilege_escalation_command { - command.arg(format!( - "{escalation_command} sh -c 'echo {encoded} | base64 -d | bash'" - )); + 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'" + )); + } } else { - command.arg(format!("echo {encoded} | base64 -d | bash")); + if arguments.keep_stdin_open { + command.arg(command_string); + } else { + command.arg(format!("echo {encoded} | base64 -d | bash")); + } } Ok(command) From 1d8fb2948cc465466e29a26ae2bc6cfa76200757 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 30 Apr 2026 05:30:27 +0000 Subject: [PATCH 3/3] [autofix.ci] apply automated fixes --- crates/core/src/commands/noninteractive.rs | 8 +++----- crates/core/src/commands/pty/mod.rs | 8 +++----- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/crates/core/src/commands/noninteractive.rs b/crates/core/src/commands/noninteractive.rs index 585f581..01898e1 100644 --- a/crates/core/src/commands/noninteractive.rs +++ b/crates/core/src/commands/noninteractive.rs @@ -66,12 +66,10 @@ pub(crate) async fn non_interactive_command_with_env>( } else { format!("{escalation_command} sh -c 'echo {encoded} | base64 -d | bash'") } + } else if arguments.keep_stdin_open { + command_string } else { - if arguments.keep_stdin_open { - command_string - } else { - format!("echo {encoded} | base64 -d | bash") - } + format!("echo {encoded} | base64 -d | bash") }; debug!("{command_string}"); diff --git a/crates/core/src/commands/pty/mod.rs b/crates/core/src/commands/pty/mod.rs index 0bdfd8c..eec5718 100644 --- a/crates/core/src/commands/pty/mod.rs +++ b/crates/core/src/commands/pty/mod.rs @@ -342,12 +342,10 @@ async fn build_command>( "{escalation_command} sh -c 'echo {encoded} | base64 -d | bash'" )); } + } else if arguments.keep_stdin_open { + command.arg(command_string); } else { - if arguments.keep_stdin_open { - command.arg(command_string); - } else { - command.arg(format!("echo {encoded} | base64 -d | bash")); - } + command.arg(format!("echo {encoded} | base64 -d | bash")); } Ok(command)