security_rust/data/
csv_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"(?m)^[=+\-@]").unwrap(),
13 Regex::new(r#"(?m)[,;\t][ \t]*"?[ \t]*=[^ \t]"#).unwrap(),
20 Regex::new(r"(?im)^\s*DDE").unwrap(),
21 Regex::new(r"(?im)^\s*cmd\s*\|").unwrap(),
22 Regex::new(r"(?im)^\s*@SUM\s*\(").unwrap(),
23 ]
24});
25
26pub struct CsvInjectionDetector;
27
28impl Detector for CsvInjectionDetector {
29 fn name(&self) -> &'static str {
30 "csv_injection"
31 }
32
33 fn detect(&self, input: &str) -> Option<DetectionResult> {
34 regex_detect(
35 &PATTERNS,
36 self.name(),
37 AttackCategory::Data,
38 Severity::Medium,
39 "CSV formula injection detected",
40 input,
41 )
42 }
43}
44
45#[cfg(test)]
46mod tests {
47 use super::*;
48
49 #[test]
50 fn name_returns_attack_type() {
51 assert_eq!(CsvInjectionDetector.name(), "csv_injection");
52 }
53
54 #[test]
55 fn detects_formula_prefixes() {
56 for payload in [
57 "=cmd|' /C calc'!A0",
58 "+1+1",
59 "-2+3",
60 "@SUM(1+1)*cmd",
61 "\t=1",
62 "列1\t=1",
63 "admin,=1+1",
64 "x;=HYPERLINK(\"http://evil.com\")",
65 ",\"=cmd|' /C calc'!A0\"",
66 "DDE;cmd",
67 "cmd|' /C calc'!A0",
68 ] {
69 let r = CsvInjectionDetector
70 .detect(payload)
71 .unwrap_or_else(|| panic!("expected detection for {:?}", payload));
72 assert_eq!(r.attack_type, "csv_injection");
73 assert_eq!(r.category, AttackCategory::Data);
74 assert_eq!(r.severity, Severity::Medium);
75 assert!(
76 !r.matched_pattern.is_empty(),
77 "matched_pattern empty for {:?}",
78 payload
79 );
80 assert!(
81 r.offset <= payload.len(),
82 "offset out of range for {:?}",
83 payload
84 );
85 }
86 }
87
88 #[test]
89 fn ignores_benign_inputs() {
90 for input in [
91 "Hello, this is a normal text input.",
92 "a=1+1",
93 "SUM(1+1)",
94 "cmd /C calc",
95 "not a formula",
96 "\t\n\r",
98 "a\tb",
99 "列1\t列2\t列3",
100 "hello world",
101 "line one\r\n\r\nline two",
102 "a, b, c",
103 "2024-01-01",
104 "1, -2, -3",
106 "me, @alice",
107 ] {
108 assert!(
109 CsvInjectionDetector.detect(input).is_none(),
110 "false positive: {:?}",
111 input
112 );
113 }
114 }
115
116 #[test]
117 fn edge_cases() {
118 assert!(CsvInjectionDetector.detect("").is_none());
119 assert!(CsvInjectionDetector.detect(" ").is_none());
120 assert!(CsvInjectionDetector.detect("=cmd|' /C calc'!A0").is_none()); }
122}