security_rust/injection/
sql_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"(?i)UNION\s+(?:ALL\s+)?SELECT").unwrap(),
10 Regex::new(r"(?i)SELECT\s+.*\s+FROM\s+").unwrap(),
11 Regex::new(r"(?i)/\*!.*?\*/").unwrap(),
12 Regex::new(r"(?i)sleep\s*\(").unwrap(),
13 Regex::new(r"(?i)benchmark\s*\(").unwrap(),
14 Regex::new(r"(?i)pg_sleep\s*\(").unwrap(),
15 Regex::new(r"(?i)information_schema").unwrap(),
16 Regex::new(r"(?i)exec\s+(?:sp_|xp_)").unwrap(),
17 Regex::new(r"(?i)WAITFOR\s+DELAY").unwrap(),
18 Regex::new(r"(?i)'\s*OR\s*'1'\s*=\s*'1").unwrap(),
19 Regex::new(r"(?i)'\s*OR\s*1\s*=\s*1\s*--").unwrap(),
20 Regex::new(r"(?i)LOAD_FILE\s*\(").unwrap(),
21 Regex::new(r"(?i)INTO\s+(?:OUT|DUMP)FILE").unwrap(),
22 Regex::new(r"(?i)OUTFILE\s+").unwrap(),
23 Regex::new(r"(?i)DROP\s+TABLE").unwrap(),
24 Regex::new(r"(?i)INSERT\s+INTO").unwrap(),
25 Regex::new(r#"(?i)(?:'|"|\))\s*(?:(?:--(?:\s|$|\+))|#|/\*)"#).unwrap(),
27 Regex::new(r"(?i)(?:/\*.*?\*/|--|#)\s*(?:or|and|union|select)\b").unwrap(),
28 ]
29});
30
31pub struct SqlInjectionDetector;
32
33impl Detector for SqlInjectionDetector {
34 fn name(&self) -> &'static str {
35 "sql_injection"
36 }
37
38 fn detect(&self, input: &str) -> Option<DetectionResult> {
39 regex_detect(
40 &PATTERNS,
41 self.name(),
42 AttackCategory::Injection,
43 Severity::Critical,
44 "SQL injection detected",
45 input,
46 )
47 }
48}
49
50#[cfg(test)]
51mod tests {
52 use super::*;
53
54 fn det() -> SqlInjectionDetector {
55 SqlInjectionDetector
56 }
57
58 fn assert_hit(input: &str) {
59 crate::test_helpers::assert_detected(
60 &det(),
61 input,
62 AttackCategory::Injection,
63 Severity::Critical,
64 );
65 }
66
67 #[test]
68 fn name_is_sql_injection() {
69 assert_eq!(det().name(), "sql_injection");
70 }
71
72 #[test]
73 fn detects_common_payloads() {
74 for input in [
75 "1 UNION SELECT password FROM users",
76 "1; SELECT pg_sleep(5)",
77 "admin' OR '1'='1",
78 "SELECT * FROM users WHERE id=1",
79 "id=1 /*!50000union select*/",
80 "1; WAITFOR DELAY '0:0:5'",
81 "username' OR 1=1 --",
82 "admin'--",
83 "1') --",
84 "x'#comment",
85 "-- or 1=1",
86 "/*x*/ union select",
87 ] {
88 assert_hit(input);
89 }
90 }
91
92 #[test]
93 fn benign_inputs_not_detected() {
94 for input in [
95 "Hello, this is a normal text input. Nothing suspicious here.",
96 "Please choose an option below",
97 "I will sleep well tonight",
98 "The benchmark results look great",
99 "Drop me a line when you arrive",
100 "The information desk is on the second floor",
101 "q=2024--2025",
102 "q=donation=5",
103 "穿越之霸道总裁爱上我--重生之都市修仙",
104 "chapter 2024--2025 更新",
105 "donation=5&q=test",
106 ] {
107 assert!(det().detect(input).is_none(), "false positive: {input}");
108 }
109 }
110
111 #[test]
112 fn edge_cases() {
113 assert!(det().detect("").is_none());
114 assert!(det().detect(" \t\n ").is_none());
115 assert!(det().detect("你好世界 こんにちは").is_none());
116 assert!(det().detect("UNOIN SILE CT *").is_none());
118 assert!(det().detect("select from users").is_none());
119 assert!(det().detect("sleep 5").is_none());
120 }
121
122 #[test]
123 fn obfuscated_variants_detected() {
124 for input in [
125 "1 UnIoN SeLeCt password",
126 "Sleep(5)",
127 "1; SELECT Pg_Sleep(10)",
128 "' or '1'='1",
129 ] {
130 assert_hit(input);
131 }
132 }
133}