use std::sync::OnceLock;
use regex::RegexSet;
static SQLI_PATTERNS: OnceLock<RegexSet> = OnceLock::new();
static SQLI_DESCRIPTIONS: &[&str] = &[
"UNION SELECT injection",
"OR boolean tautology (1=1)",
"AND boolean tautology (1=1)",
"SQL comment injection (--)",
"SQL comment injection (/*)",
"DROP TABLE statement",
"DELETE FROM statement",
"INSERT INTO statement",
"UPDATE SET statement",
"String termination with SQL keyword",
"Hex-encoded SQL keyword",
"BENCHMARK blind SQLi",
"SLEEP blind SQLi",
"WAITFOR DELAY blind SQLi",
"EXEC/EXECUTE statement",
"LOAD_FILE / INTO OUTFILE",
"CONCAT/CHAR function injection",
"HAVING/GROUP BY injection",
"OR empty-string tautology (OR ''='')",
];
fn patterns() -> &'static RegexSet {
SQLI_PATTERNS.get_or_init(|| {
RegexSet::new([
r"(?i)union[\s(]*(all[\s(]+)?select[\s(]",
r#"(?i)\bor\s+['"]?\w+['"]?\s*=\s*['"]?\w+['"]?"#,
r#"(?i)\band\s+[\d'"]+\s*=\s*[\d'"]+"#,
r#"(?i)['"]\s*;\s*--"#,
r"/\*.*?\*/",
r"(?i)\bdrop\s+(table|database|index)\b",
r"(?i)\bdelete\s+from\b",
r"(?i)\binsert\s+into\b",
r"(?i)\bupdate\b.+?\bset\b",
r#"(?i)['"];\s*(select|insert|update|delete|drop|alter|create|exec)\b"#,
r"(?i)0x[0-9a-f]{8,}",
r"(?i)\bbenchmark\s*\(",
r"(?i)\bsleep\s*\(",
r"(?i)\bwaitfor\s+delay\b",
r"(?i)\bexec(ute)?\s*\(",
r"(?i)\b(load_file|into\s+outfile|into\s+dumpfile)\b",
r"(?i)\b(concat|char|ascii|substring|mid)\s*\(",
r"(?i)\b(having|group\s+by)\s+.+?(=|<|>|like)",
r#"(?i)\bor\s+['"]{2}\s*=\s*['"]{1}"#,
])
.expect("SQLI regex patterns must compile")
})
}
pub fn check_sqli(input: &str) -> Option<String> {
let set = patterns();
let matches: Vec<_> = set.matches(input).into_iter().collect();
if matches.is_empty() {
None
} else {
let idx = matches[0];
Some(
SQLI_DESCRIPTIONS
.get(idx)
.unwrap_or(&"SQL injection")
.to_string(),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detects_union_select() {
assert!(check_sqli("1 UNION SELECT * FROM users").is_some());
assert!(check_sqli("1 union all select username, password from users").is_some());
}
#[test]
fn detects_or_tautology() {
assert!(check_sqli("admin' OR 1=1 --").is_some());
assert!(check_sqli("admin' or '1'='1").is_some());
}
#[test]
fn detects_and_tautology() {
assert!(check_sqli("1 AND 1=1").is_some());
}
#[test]
fn detects_comment_injection() {
assert!(check_sqli("admin'; -- comment").is_some());
assert!(check_sqli("foo /* comment */ bar").is_some());
}
#[test]
fn detects_drop_table() {
assert!(check_sqli("'; DROP TABLE users; --").is_some());
assert!(check_sqli("DROP DATABASE production").is_some());
}
#[test]
fn detects_delete_from() {
assert!(check_sqli("DELETE FROM users WHERE 1=1").is_some());
}
#[test]
fn detects_insert_into() {
assert!(check_sqli("INSERT INTO users(name) VALUES('hacker')").is_some());
}
#[test]
fn detects_update_set() {
assert!(check_sqli("UPDATE users SET password='hacked'").is_some());
}
#[test]
fn detects_string_termination_with_sql() {
assert!(check_sqli("'; select * from users").is_some());
assert!(check_sqli("\"; drop table users").is_some());
}
#[test]
fn detects_hex_encoding() {
assert!(check_sqli("0x61646d696e20").is_some());
}
#[test]
fn detects_benchmark_sleep() {
assert!(check_sqli("1 AND BENCHMARK(10000000,SHA1('test'))").is_some());
assert!(check_sqli("1 OR SLEEP(5)").is_some());
}
#[test]
fn detects_waitfor_delay() {
assert!(check_sqli("'; WAITFOR DELAY '0:0:5'--").is_some());
}
#[test]
fn detects_exec_execute() {
assert!(check_sqli("EXEC('SELECT 1')").is_some());
assert!(check_sqli("execute('malicious')").is_some());
}
#[test]
fn detects_load_file() {
assert!(check_sqli("LOAD_FILE('/etc/passwd')").is_some());
assert!(check_sqli("INTO OUTFILE '/tmp/data'").is_some());
}
#[test]
fn detects_concat_char() {
assert!(check_sqli("CONCAT(0x7e, version(), 0x7e)").is_some());
assert!(check_sqli("CHAR(97,100,109,105,110)").is_some());
}
#[test]
fn allows_normal_search_query() {
assert!(check_sqli("best restaurants near me").is_none());
}
#[test]
fn allows_normal_product_description() {
assert!(check_sqli("This product is great and has a 5-star rating").is_none());
}
#[test]
fn allows_normal_url_path() {
assert!(check_sqli("/api/users/123/profile").is_none());
}
#[test]
fn allows_json_payload() {
assert!(check_sqli(r#"{"name":"John","age":30}"#).is_none());
}
#[test]
fn allows_normal_text_with_quotes() {
assert!(check_sqli("It's a wonderful day").is_none());
}
}