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..01898e1 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,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 '{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}"); diff --git a/crates/core/src/commands/pty/mod.rs b/crates/core/src/commands/pty/mod.rs index a2ae67c..eec5718 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,20 @@ 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 { + 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 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)