diff --git a/codex-rs/shell-command/src/bash.rs b/codex-rs/shell-command/src/bash.rs index 007fbf956c..ba5a54eecd 100644 --- a/codex-rs/shell-command/src/bash.rs +++ b/codex-rs/shell-command/src/bash.rs @@ -156,10 +156,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(parse_unquoted_word(child, src)?); } "string" => { let parsed = parse_double_quoted_string(child, src)?; @@ -176,8 +176,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(&parse_unquoted_word(part, src)?); } "string" => { let parsed = parse_double_quoted_string(part, src)?; @@ -201,6 +200,19 @@ fn parse_plain_command_from_node(cmd: tree_sitter::Node, src: &str) -> Option, src: &str) -> Option { + if !matches!(node.kind(), "word" | "number") { + return None; + } + + let word = node.utf8_text(src.as_bytes()).ok()?; + if word.contains(['*', '?', '[']) { + return None; + } + + Some(word.to_owned()) +} + fn parse_heredoc_command_words(cmd: Node<'_>, src: &str) -> Option> { if cmd.kind() != "command" { return None; @@ -489,6 +501,34 @@ mod tests { ); } + #[test] + fn rejects_unquoted_glob_expansions() { + for script in [ + "echo *", + "echo file?.txt", + "echo [ab].txt", + r#"echo prefix*"suffix""#, + ] { + assert!( + parse_seq(script).is_none(), + "expected unquoted glob expansion to be rejected: {script}" + ); + } + } + + #[test] + fn accepts_quoted_glob_characters() { + assert_eq!( + parse_seq(r#"echo "*" '?' '[ab]'"#).unwrap(), + vec![vec![ + "echo".to_string(), + "*".to_string(), + "?".to_string(), + "[ab]".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 bca44139cd..82548a2f56 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 @@ -435,6 +435,16 @@ mod tests { "--output=/tmp/git-show-out-test", "HEAD", ]))); + assert!(!is_known_safe_command(&vec_str(&[ + "bash", + "-lc", + "git show --no-patch --output?poison HEAD", + ]))); + assert!(is_known_safe_command(&vec_str(&[ + "bash", + "-lc", + "git show -- '*.rs'", + ]))); } #[test]