Skip to main content

security_rust/injection/
sql_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"(?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    ]
26});
27
28pub struct SqlInjectionDetector;
29
30impl Detector for SqlInjectionDetector {
31    fn name(&self) -> &'static str {
32        "sql_injection"
33    }
34
35    fn detect(&self, input: &str) -> Option<DetectionResult> {
36        regex_detect(&PATTERNS, self.name(), AttackCategory::Injection, Severity::Critical, "SQL injection detected", input)
37    }
38}
39
40#[cfg(test)]
41mod tests {
42    use super::*;
43
44    fn det() -> SqlInjectionDetector {
45        SqlInjectionDetector
46    }
47
48    fn assert_hit(input: &str) {
49        crate::test_helpers::assert_detected(
50            &det(),
51            input,
52            AttackCategory::Injection,
53            Severity::Critical,
54        );
55    }
56
57    #[test]
58    fn name_is_sql_injection() {
59        assert_eq!(det().name(), "sql_injection");
60    }
61
62    #[test]
63    fn detects_common_payloads() {
64        for input in [
65            "1 UNION SELECT password FROM users",
66            "1; SELECT pg_sleep(5)",
67            "admin' OR '1'='1",
68            "SELECT * FROM users WHERE id=1",
69            "id=1 /*!50000union select*/",
70            "1; WAITFOR DELAY '0:0:5'",
71            "username' OR 1=1 --",
72        ] {
73            assert_hit(input);
74        }
75    }
76
77    #[test]
78    fn benign_inputs_not_detected() {
79        for input in [
80            "Hello, this is a normal text input. Nothing suspicious here.",
81            "Please choose an option below",
82            "I will sleep well tonight",
83            "The benchmark results look great",
84            "Drop me a line when you arrive",
85            "The information desk is on the second floor",
86        ] {
87            assert!(det().detect(input).is_none(), "false positive: {input}");
88        }
89    }
90
91    #[test]
92    fn edge_cases() {
93        assert!(det().detect("").is_none());
94        assert!(det().detect(" \t\n ").is_none());
95        assert!(det().detect("你好世界 こんにちは").is_none());
96        // near misses: keyword present but not the payload form
97        assert!(det().detect("UNOIN SILE CT *").is_none());
98        assert!(det().detect("select from users").is_none());
99        assert!(det().detect("sleep 5").is_none());
100    }
101
102    #[test]
103    fn obfuscated_variants_detected() {
104        for input in [
105            "1 UnIoN SeLeCt password",
106            "Sleep(5)",
107            "1; SELECT Pg_Sleep(10)",
108            "' or '1'='1",
109        ] {
110            assert_hit(input);
111        }
112    }
113}