Skip to main content

security_rust/data/
csv_injection.rs

1// Copyright (c) 2026 erik <erik@erik.xyz> — https://erik.xyz
2
3use 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"(?m)^[=+\-@\t\r]").unwrap(),
10        Regex::new(r"(?im)^\s*DDE").unwrap(),
11        Regex::new(r"(?im)^\s*cmd\s*\|").unwrap(),
12        Regex::new(r"(?im)^\s*@SUM\s*\(").unwrap(),
13    ]
14});
15
16pub struct CsvInjectionDetector;
17
18impl Detector for CsvInjectionDetector {
19    fn name(&self) -> &'static str {
20        "csv_injection"
21    }
22
23    fn detect(&self, input: &str) -> Option<DetectionResult> {
24        regex_detect(&PATTERNS, self.name(), AttackCategory::Data, Severity::Medium, "CSV formula injection detected", input)
25    }
26}
27
28#[cfg(test)]
29mod tests {
30    use super::*;
31
32    #[test]
33    fn name_returns_attack_type() {
34        assert_eq!(CsvInjectionDetector.name(), "csv_injection");
35    }
36
37    #[test]
38    fn detects_formula_prefixes() {
39        for payload in [
40            "=cmd|' /C calc'!A0",
41            "+1+1",
42            "-2+3",
43            "@SUM(1+1)*cmd",
44            "\t=1",
45            "DDE;cmd",
46            "cmd|' /C calc'!A0",
47        ] {
48            let r = CsvInjectionDetector
49                .detect(payload)
50                .unwrap_or_else(|| panic!("expected detection for {:?}", payload));
51            assert_eq!(r.attack_type, "csv_injection");
52            assert_eq!(r.category, AttackCategory::Data);
53            assert_eq!(r.severity, Severity::Medium);
54            assert!(
55                !r.matched_pattern.is_empty(),
56                "matched_pattern empty for {:?}",
57                payload
58            );
59            assert!(
60                r.offset <= payload.len(),
61                "offset out of range for {:?}",
62                payload
63            );
64        }
65    }
66
67    #[test]
68    fn ignores_benign_inputs() {
69        for input in [
70            "Hello, this is a normal text input.",
71            "a=1+1",
72            "SUM(1+1)",
73            "cmd /C calc",
74            "not a formula",
75        ] {
76            assert!(
77                CsvInjectionDetector.detect(input).is_none(),
78                "false positive: {:?}",
79                input
80            );
81        }
82    }
83
84    #[test]
85    fn edge_cases() {
86        assert!(CsvInjectionDetector.detect("").is_none());
87        assert!(CsvInjectionDetector.detect("   ").is_none());
88        assert!(CsvInjectionDetector.detect("=cmd|' /C calc'!A0").is_none()); // fullwidth equals
89    }
90}