-
Notifications
You must be signed in to change notification settings - Fork 0
π‘οΈ Sentinel: Fix Windows cmd.exe environment variable injection #782
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -187,6 +187,39 @@ pub fn cmd_escape(s: &str) -> Cow<'_, str> { | |
| Cow::Owned(escaped) | ||
| } | ||
|
|
||
| /// Escape a string for use as a single argument in Windows cmd.exe. | ||
| /// | ||
| /// This function wraps the string in double quotes and escapes any internal double quotes, | ||
| /// similar to `cmd_escape`. Additionally, it escapes `%` characters by replacing them | ||
| /// with `%""` to prevent variable expansion inside the quoted string. | ||
| /// | ||
| /// This is crucial because `cmd.exe` performs variable expansion even inside double quotes. | ||
| /// By inserting an empty string `""` after the `%`, we break the variable name token | ||
| /// without changing the value (since `""` is empty), effectively preventing expansion. | ||
| /// | ||
| /// # Arguments | ||
| /// | ||
| /// * `s` - The argument string to escape | ||
| /// | ||
| /// # Returns | ||
| /// | ||
| /// * The escaped argument string safe for cmd.exe execution as a literal | ||
| pub fn cmd_arg_escape(s: &str) -> Cow<'_, str> { | ||
| let mut escaped = String::with_capacity(s.len() + 16); | ||
| escaped.push('"'); | ||
|
|
||
| for c in s.chars() { | ||
| match c { | ||
| '"' => escaped.push_str("\"\""), | ||
| '%' => escaped.push_str("%\"\""), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The new Useful? React with πΒ / π. |
||
| _ => escaped.push(c), | ||
| } | ||
| } | ||
|
|
||
| escaped.push('"'); | ||
| Cow::Owned(escaped) | ||
| } | ||
|
|
||
| /// Escapes a string for safe use in PowerShell commands. | ||
| /// | ||
| /// This function handles special characters that could cause issues | ||
|
|
@@ -321,6 +354,17 @@ mod tests { | |
| assert_eq!(cmd_escape(""), "\"\""); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_cmd_arg_escape() { | ||
| assert_eq!(cmd_arg_escape("simple"), "\"simple\""); | ||
| assert_eq!(cmd_arg_escape("with space"), "\"with space\""); | ||
| assert_eq!(cmd_arg_escape("with\"quote"), "\"with\"\"quote\""); | ||
| // Verify % is escaped with %"" | ||
| assert_eq!(cmd_arg_escape("%USERNAME%"), "\"%\"\"USERNAME%\"\"\""); | ||
| assert_eq!(cmd_arg_escape("100%"), "\"100%\"\"\""); | ||
| assert_eq!(cmd_arg_escape(""), "\"\""); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_powershell_escape() { | ||
| assert_eq!(powershell_escape("simple"), "'simple'"); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| use rustible::modules::{validate_command_args, command::CommandModule, Module, ModuleParams, ModuleContext}; | ||
| use rustible::utils::cmd_arg_escape; | ||
| use std::collections::HashMap; | ||
|
|
||
| #[test] | ||
| fn test_validate_command_args_blocks_percent() { | ||
| // This should now fail (Err) because we added % to dangerous_patterns | ||
| assert!(validate_command_args("echo %USERNAME%").is_err()); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_cmd_arg_escape_escapes_percent() { | ||
| let input = "%USERNAME%"; | ||
| let escaped = cmd_arg_escape(input); | ||
| // Should now return "%""USERNAME%" inside quotes (outer quotes + inner escaped quotes) | ||
| // cmd_arg_escape wraps in "...", so result is "%""USERNAME%" | ||
| // Wait, escaped string content: "%""USERNAME%" | ||
| // Representation in Rust string literal: "\"%\"\"USERNAME%\"\"\"" | ||
| assert_eq!(escaped, "\"%\"\"USERNAME%\"\"\""); | ||
| } | ||
|
|
||
| #[test] | ||
| fn test_command_module_argv_escapes_percent() { | ||
| let module = CommandModule; | ||
| let mut params: ModuleParams = HashMap::new(); | ||
| params.insert( | ||
| "argv".to_string(), | ||
| serde_json::json!(["echo", "%USERNAME%"]), | ||
| ); | ||
| params.insert("shell_type".to_string(), serde_json::json!("cmd")); | ||
|
|
||
| let context = ModuleContext::default().with_check_mode(true); | ||
|
|
||
| let result = module.execute(¶ms, &context).unwrap(); | ||
| let msg = result.msg; | ||
|
|
||
| // msg format: "Would execute: <cmd>" | ||
| // cmd should be: "echo" "%""USERNAME%" | ||
| // Expected substring in msg: "\"%\"\"USERNAME%\"\"\"" | ||
|
|
||
| println!("Message: {}", msg); | ||
| assert!(msg.contains("\"%\"\"USERNAME%\"\"\"")); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding
%todangerous_patternsmakesvalidate_command_argsreject all raw commands containing%, even on non-Windows shells where%is commonly valid (for exampledate +%Forprintf '%s\n'). BecauseCommandModule::validate_paramsruns this validation before shell-type handling, this introduces a cross-platform behavior regression from a Windows-specific security fix.Useful? React with πΒ / π.