From aaf88f1ba32f2b13177e0049b14dedca6e092863 Mon Sep 17 00:00:00 2001 From: Ollie Matthews Date: Wed, 29 Apr 2026 15:01:54 -0700 Subject: [PATCH] [codex] decode escaped bash words in safety parser --- codex-rs/shell-command/src/bash.rs | 44 +++++++++++++++++-- .../src/command_safety/is_safe_command.rs | 9 ++++ 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/codex-rs/shell-command/src/bash.rs b/codex-rs/shell-command/src/bash.rs index 2fe299108c..dc8e3d6f31 100644 --- a/codex-rs/shell-command/src/bash.rs +++ b/codex-rs/shell-command/src/bash.rs @@ -149,10 +149,10 @@ fn parse_plain_command_from_node(cmd: tree_sitter::Node, src: &str) -> Option { - words.push(child.utf8_text(src.as_bytes()).ok()?.to_owned()); + words.push(decode_unquoted_shell_word(child, src)?); } "string" => { let parsed = parse_double_quoted_string(child, src)?; @@ -169,8 +169,7 @@ fn parse_plain_command_from_node(cmd: tree_sitter::Node, src: &str) -> Option { - concatenated - .push_str(part.utf8_text(src.as_bytes()).ok()?.to_owned().as_str()); + concatenated.push_str(&decode_unquoted_shell_word(part, src)?); } "string" => { let parsed = parse_double_quoted_string(part, src)?; @@ -194,6 +193,23 @@ fn parse_plain_command_from_node(cmd: tree_sitter::Node, src: &str) -> Option Option { + let raw = node.utf8_text(src.as_bytes()).ok()?; + let mut decoded = String::with_capacity(raw.len()); + let mut chars = raw.chars(); + while let Some(ch) = chars.next() { + if ch == '\\' { + let next = chars.next()?; + if next != '\n' { + decoded.push(next); + } + } else { + decoded.push(ch); + } + } + Some(decoded) +} + fn parse_heredoc_command_words(cmd: Node<'_>, src: &str) -> Option> { if cmd.kind() != "command" { return None; @@ -482,6 +498,26 @@ mod tests { ); } + #[test] + fn decodes_backslash_escapes_in_unquoted_words() { + let cmds = parse_seq(r#"find /app -maxdepth 0 -e\xec sh -c 'echo ok' sh \;"#).unwrap(); + assert_eq!( + cmds, + vec![vec![ + "find".to_string(), + "/app".to_string(), + "-maxdepth".to_string(), + "0".to_string(), + "-exec".to_string(), + "sh".to_string(), + "-c".to_string(), + "echo ok".to_string(), + "sh".to_string(), + ";".to_string(), + ]] + ); + } + #[test] fn rejects_concatenation_with_variable_substitution() { // Environment variables in concatenated strings should be rejected diff --git a/codex-rs/shell-command/src/command_safety/is_safe_command.rs b/codex-rs/shell-command/src/command_safety/is_safe_command.rs index 26fe235fbe..c867b3b7be 100644 --- a/codex-rs/shell-command/src/command_safety/is_safe_command.rs +++ b/codex-rs/shell-command/src/command_safety/is_safe_command.rs @@ -469,6 +469,15 @@ mod tests { } } + #[test] + fn shell_escaped_find_exec_is_not_safe() { + assert!(!is_known_safe_command(&vec_str(&[ + "bash", + "-lc", + r#"find /app -maxdepth 0 -e\xec sh -c '/usr/bin/su\do -n id' sh \;"#, + ]))); + } + #[test] fn base64_output_options_are_unsafe() { for args in [