Skip to main content

shell_sanitize_rules/
shell_meta.rs

1use shell_sanitize::{Rule, RuleResult, RuleViolation};
2
3use crate::charset::CharSet;
4
5/// Rejects input containing shell metacharacters.
6///
7/// Default denied characters: `;`, `|`, `&`, `$`, `` ` ``, `(`, `)`, `{`, `}`,
8/// `<`, `>`, `!`, `#`, `\`, `'`, `"`, `~`, `\n`, `\r`.
9///
10/// # Rationale
11///
12/// These characters enable:
13/// - Command chaining (`;`, `&&`, `||`)
14/// - Piping (`|`)
15/// - Subshell / command substitution (`$()`, `` ` ``)
16/// - Redirection (`<`, `>`)
17/// - Backgrounding (`&`)
18/// - Comment injection (`#`)
19/// - Home directory expansion (`~`)
20pub struct ShellMetaRule {
21    denied: CharSet,
22}
23
24const DEFAULT_DENIED: &[char] = &[
25    ';', '|', '&', '$', '`', '(', ')', '{', '}', '<', '>', '!', '#', '\\', '\'', '"', '~', '\n',
26    '\r',
27];
28
29impl Default for ShellMetaRule {
30    fn default() -> Self {
31        Self {
32            denied: CharSet::from_chars(DEFAULT_DENIED),
33        }
34    }
35}
36
37impl ShellMetaRule {
38    /// Create a rule with a custom set of denied characters.
39    pub fn with_denied(denied: Vec<char>) -> Self {
40        Self {
41            denied: CharSet::from_chars(&denied),
42        }
43    }
44
45    /// Create a rule with additional denied characters on top of defaults.
46    pub fn with_extra_denied(extra: &[char]) -> Self {
47        let mut chars: Vec<char> = DEFAULT_DENIED.to_vec();
48        chars.extend(extra);
49        Self {
50            denied: CharSet::from_chars(&chars),
51        }
52    }
53}
54
55impl Rule for ShellMetaRule {
56    fn name(&self) -> &'static str {
57        "shell_meta"
58    }
59
60    fn check(&self, input: &str) -> RuleResult {
61        let violations: Vec<_> = input
62            .char_indices()
63            .filter(|(_, c)| self.denied.contains(*c))
64            .map(|(i, c)| {
65                RuleViolation::new(self.name(), format!("shell metacharacter {:?} found", c))
66                    .at(i)
67                    .with_fragment(c.to_string())
68            })
69            .collect();
70
71        if violations.is_empty() {
72            Ok(())
73        } else {
74            Err(violations)
75        }
76    }
77}