Compare commits

...

1 Commits

Author SHA1 Message Date
Eva Wong
f8cf9a7cb7 Reject shell globs in safe command parsing 2026-05-11 11:40:58 -07:00
2 changed files with 47 additions and 4 deletions

View File

@@ -152,10 +152,10 @@ fn parse_plain_command_from_node(cmd: tree_sitter::Node, src: &str) -> Option<Ve
if word_node.kind() != "word" {
return None;
}
words.push(word_node.utf8_text(src.as_bytes()).ok()?.to_owned());
words.push(parse_unquoted_literal(word_node, src)?);
}
"word" | "number" => {
words.push(child.utf8_text(src.as_bytes()).ok()?.to_owned());
words.push(parse_unquoted_literal(child, src)?);
}
"string" => {
let parsed = parse_double_quoted_string(child, src)?;
@@ -172,8 +172,7 @@ fn parse_plain_command_from_node(cmd: tree_sitter::Node, src: &str) -> Option<Ve
for part in child.named_children(&mut concat_cursor) {
match part.kind() {
"word" | "number" => {
concatenated
.push_str(part.utf8_text(src.as_bytes()).ok()?.to_owned().as_str());
concatenated.push_str(&parse_unquoted_literal(part, src)?);
}
"string" => {
let parsed = parse_double_quoted_string(part, src)?;
@@ -197,6 +196,18 @@ fn parse_plain_command_from_node(cmd: tree_sitter::Node, src: &str) -> Option<Ve
Some(words)
}
fn parse_unquoted_literal(node: Node, src: &str) -> Option<String> {
let text = node.utf8_text(src.as_bytes()).ok()?;
if text.starts_with('~') || text.chars().any(is_unquoted_shell_expansion_char) {
return None;
}
Some(text.to_string())
}
fn is_unquoted_shell_expansion_char(c: char) -> bool {
matches!(c, '*' | '?' | '[' | ']' | '{' | '}')
}
fn parse_heredoc_command_words(cmd: Node<'_>, src: &str) -> Option<Vec<String>> {
if cmd.kind() != "command" {
return None;
@@ -500,6 +511,26 @@ mod tests {
assert!(parse_seq("rg -g\"$(echo '*.py')\" pattern").is_none());
}
#[test]
fn rejects_unquoted_shell_expansion_words() {
assert!(parse_seq("base64 -i input.txt -* cc06_out").is_none());
assert!(parse_seq("ls *.rs").is_none());
assert!(parse_seq("echo -{o,}").is_none());
}
#[test]
fn accepts_quoted_shell_expansion_literals() {
assert_eq!(
parse_seq(r#"echo "-*" '*.rs' "-{o,}""#).unwrap(),
vec![vec![
"echo".to_string(),
"-*".to_string(),
"*.rs".to_string(),
"-{o,}".to_string(),
]]
);
}
#[test]
fn parse_shell_lc_single_command_prefix_supports_heredoc() {
let command = vec![

View File

@@ -738,6 +738,18 @@ mod tests {
);
}
#[test]
fn bash_lc_glob_expansion_option_injection_is_not_safe() {
assert!(
!is_known_safe_command(&vec_str(&[
"bash",
"-lc",
"base64 -i input.txt -* cc06_out",
])),
"Unquoted shell expansion can rewrite argv into base64 -o at runtime"
);
}
#[test]
fn direct_powershell_words_use_windows_safelist() {
let command = vec_str(&["Get-Content", "Cargo.toml"]);