use crate::linter::{Diagnostic, Fix, LintResult, Severity, Span};
fn check_find_exec_injection(line: &str, line_num: usize) -> Option<Diagnostic> {
if !line.contains("find ") || !line.contains("-exec") {
return None;
}
if !(line.contains("sh -c") || line.contains("bash -c")) || !line.contains("{}") {
return None;
}
if !is_braces_in_shell_string(line) {
return None;
}
let col = find_braces_in_quotes(line)?;
let span = Span::new(line_num + 1, col + 1, line_num + 1, col + 3);
Some(
Diagnostic::new(
"SEC003",
Severity::Error,
"Command injection: {} embedded in shell string. Use positional params: sh -c 'cmd \"$1\"' _ {}",
span,
)
.with_fix(Fix::new("\"$1\"")),
)
}
pub fn check(source: &str) -> LintResult {
if source.is_empty() { return LintResult::new(); }
contract_pre_classify_injection!(source);
let mut result = LintResult::new();
for (line_num, line) in source.lines().enumerate() {
if let Some(diag) = check_find_exec_injection(line, line_num) {
result.add(diag);
}
}
result
}
fn is_braces_in_shell_string(line: &str) -> bool {
let single_quote_pattern = |s: &str| {
let mut in_single = false;
let mut found_braces_in_quote = false;
let chars: Vec<char> = s.chars().collect();
for i in 0..chars.len() {
if chars[i] == '\'' {
in_single = !in_single;
}
if in_single && i + 1 < chars.len() && chars[i] == '{' && chars[i + 1] == '}' {
found_braces_in_quote = true;
}
}
found_braces_in_quote
};
let double_quote_pattern = |s: &str| {
let mut in_double = false;
let mut found_braces_in_quote = false;
let chars: Vec<char> = s.chars().collect();
for i in 0..chars.len() {
if chars[i] == '"' && (i == 0 || chars[i - 1] != '\\') {
in_double = !in_double;
}
if in_double && i + 1 < chars.len() && chars[i] == '{' && chars[i + 1] == '}' {
found_braces_in_quote = true;
}
}
found_braces_in_quote
};
single_quote_pattern(line) || double_quote_pattern(line)
}
fn find_braces_in_quotes(line: &str) -> Option<usize> {
let mut in_single = false;
let mut in_double = false;
let chars: Vec<char> = line.chars().collect();
for i in 0..chars.len() {
if chars[i] == '\'' && !in_double {
in_single = !in_single;
}
if chars[i] == '"' && !in_single && (i == 0 || chars[i - 1] != '\\') {
in_double = !in_double;
}
if (in_single || in_double) && i + 1 < chars.len() && chars[i] == '{' && chars[i + 1] == '}'
{
return Some(i);
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_SEC003_detects_sh_c_with_embedded_braces_single_quote() {
let script = "find . -exec sh -c 'echo {}' \\;";
let result = check(script);
assert_eq!(result.diagnostics.len(), 1);
let diag = &result.diagnostics[0];
assert_eq!(diag.code, "SEC003");
assert_eq!(diag.severity, Severity::Error);
assert!(diag.message.contains("Command injection"));
}
#[test]
fn test_SEC003_detects_bash_c_with_embedded_braces_double_quote() {
let script = r#"find . -exec bash -c "rm {}" \;"#;
let result = check(script);
assert_eq!(result.diagnostics.len(), 1);
}
#[test]
fn test_SEC003_provides_fix_for_injection() {
let script = "find . -exec sh -c 'cat {}' \\;";
let result = check(script);
assert_eq!(result.diagnostics.len(), 1);
assert!(result.diagnostics[0].fix.is_some());
let fix = result.diagnostics[0].fix.as_ref().unwrap();
assert_eq!(fix.replacement, "\"$1\"");
}
#[test]
fn test_SEC003_no_warning_for_standard_find_exec() {
let script = r#"find . -name "*.sh" -exec chmod +x {} \;"#;
let result = check(script);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_SEC003_no_warning_for_rm_with_braces() {
let script = "find /tmp -type f -exec rm {} \\;";
let result = check(script);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_SEC003_no_warning_for_batch_mode() {
let script = "find . -type f -exec chmod 644 {} +";
let result = check(script);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_SEC003_no_false_positive_no_find() {
let script = "echo {} something";
let result = check(script);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_SEC003_no_false_positive_find_exec_standard() {
let script = r#"find . -name "*.txt" -exec rm {} \;
find . -type f -exec chmod 644 {} +"#;
let result = check(script);
assert_eq!(
result.diagnostics.len(),
0,
"SEC003 must NOT flag {{}} in find -exec - it's handled by find, not the shell"
);
}
#[test]
fn test_SEC003_no_false_positive_find_execdir() {
let script = "find . -execdir mv {} {}.bak \\;";
let result = check(script);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_SEC003_safe_positional_params() {
let script = r#"find . -exec sh -c 'echo "$1"' _ {} \;"#;
let result = check(script);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_mutation_sec003_line_num_calculation() {
let bash_code = "# comment\nfind . -exec sh -c 'echo {}' \\;";
let result = check(bash_code);
assert_eq!(result.diagnostics.len(), 1);
assert_eq!(
result.diagnostics[0].span.start_line, 2,
"Line number must use +1, not *1"
);
}
#[test]
fn test_mutation_sec003_column_calculation() {
let bash_code = "find . -exec sh -c 'echo {}' \\;";
let result = check(bash_code);
assert_eq!(result.diagnostics.len(), 1);
let span = result.diagnostics[0].span;
assert!(
span.start_col > 20,
"Start column should be inside the quoted string"
);
assert_eq!(
span.end_col - span.start_col,
2,
"Span should cover {{}} (2 chars)"
);
}
#[test]
fn test_mutation_sec003_double_quotes_detection() {
let bash_code = r#"find . -exec bash -c "test {}" \;"#;
let result = check(bash_code);
assert_eq!(result.diagnostics.len(), 1);
}
#[test]
fn test_mutation_sec003_requires_find_keyword() {
let bash_code = "sh -c 'echo {}' \\;";
let result = check(bash_code);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_mutation_sec003_requires_exec_flag() {
let bash_code = "find . -name '*.sh' | sh -c 'echo {}'";
let result = check(bash_code);
assert_eq!(result.diagnostics.len(), 0);
}
#[test]
fn test_SEC003_issue_87_no_false_positive_dirname() {
let script = r#"DIRS=$(find "$CORPUS" -name "Cargo.toml" -exec dirname {} \; 2>/dev/null | sort -u)"#;
let result = check(script);
assert_eq!(
result.diagnostics.len(),
0,
"SEC003 must NOT flag {{}} in 'find -exec dirname {{}} \\;' - it's a separate argument, not embedded in shell string"
);
}
#[test]
fn test_SEC003_issue_87_no_false_positive_command_substitution() {
let script = "FILES=$(find /path -type f -exec basename {} \\;)";
let result = check(script);
assert_eq!(result.diagnostics.len(), 0);
}
}