Skip to main content

security_rust/injection/
sql_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"(?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        // `--` requires trailing space/EOL/`+` (MySQL URL-encoded space) so SSI's `"-->` stays clean
26        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        // near misses: keyword present but not the payload form
117        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}