#[inline]
#[must_use]
pub fn is_valid_sql_expression(s: &str) -> bool {
if s.is_empty() || s.len() > 1000 {
return false;
}
if s.contains("--") || s.contains("/*") || s.contains("*/") {
return false;
}
if s.contains(';') {
return false;
}
if s.contains('`') {
return false;
}
let lower = s.to_ascii_lowercase();
const DANGEROUS_KEYWORDS: &[&str] = &[
"select",
"insert",
"update",
"delete",
"drop",
"truncate",
"alter",
"create",
"grant",
"revoke",
"exec",
"execute",
"union",
"into",
"from",
"where",
"having",
"group",
"order",
"limit",
"offset",
"fetch",
"returning",
"sleep",
"benchmark",
"waitfor",
"pg_sleep",
"dbms_lock",
"load_file",
"into_outfile",
"into_dumpfile",
"chr",
"char",
"ascii",
"unicode",
"hex",
"unhex",
"convert",
"cast",
"encode",
"decode",
];
for keyword in DANGEROUS_KEYWORDS {
if contains_sql_keyword(&lower, keyword) {
return false;
}
}
if lower.contains("pg_")
|| lower.contains("sqlite_")
|| lower.contains("information_schema")
|| lower.contains("sys.")
{
return false;
}
if lower.contains("0x") || lower.contains("\\x") {
return false;
}
true
}
#[inline]
fn contains_sql_keyword(haystack: &str, keyword: &str) -> bool {
let bytes = haystack.as_bytes();
let kw_bytes = keyword.as_bytes();
let kw_len = kw_bytes.len();
if kw_len == 0 || bytes.len() < kw_len {
return false;
}
for i in 0..=(bytes.len() - kw_len) {
if &bytes[i..i + kw_len] == kw_bytes {
let before_ok =
i == 0 || (!bytes[i - 1].is_ascii_alphanumeric() && bytes[i - 1] != b'_');
let after_ok = i + kw_len == bytes.len()
|| (!bytes[i + kw_len].is_ascii_alphanumeric() && bytes[i + kw_len] != b'_');
if before_ok && after_ok {
return true;
}
}
}
false
}
#[inline]
pub fn assert_valid_sql_expression(s: &str, context: &str) {
assert!(
is_valid_sql_expression(s),
"Invalid SQL expression for {context}: '{s}' contains dangerous patterns \
(comments, semicolons, or SQL keywords)"
);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_sql_expressions() {
assert!(is_valid_sql_expression("first_name || ' ' || last_name"));
assert!(is_valid_sql_expression("quantity * price"));
assert!(is_valid_sql_expression("COALESCE(nickname, name)"));
assert!(is_valid_sql_expression("age + 1"));
assert!(is_valid_sql_expression("CASE WHEN x > 0 THEN y ELSE z END"));
assert!(is_valid_sql_expression("price * 1.1"));
assert!(is_valid_sql_expression("UPPER(name)"));
assert!(is_valid_sql_expression("LENGTH(description)"));
assert!(is_valid_sql_expression("last_updated")); assert!(is_valid_sql_expression("created_at")); assert!(is_valid_sql_expression("selected_items")); assert!(is_valid_sql_expression("deleted_at")); assert!(is_valid_sql_expression("order_total")); assert!(is_valid_sql_expression("group_name")); assert!(is_valid_sql_expression("from_date")); assert!(is_valid_sql_expression("where_clause")); }
#[test]
fn test_invalid_sql_expressions() {
assert!(!is_valid_sql_expression(""));
assert!(!is_valid_sql_expression("name -- comment"));
assert!(!is_valid_sql_expression("/* comment */ name"));
assert!(!is_valid_sql_expression("name */ attack"));
assert!(!is_valid_sql_expression("1; DROP TABLE users"));
assert!(!is_valid_sql_expression("name;"));
assert!(!is_valid_sql_expression("`table`"));
assert!(!is_valid_sql_expression("(SELECT password)"));
assert!(!is_valid_sql_expression("INSERT INTO x"));
assert!(!is_valid_sql_expression("DELETE FROM x"));
assert!(!is_valid_sql_expression("DROP TABLE x"));
assert!(!is_valid_sql_expression("UPDATE SET y=1"));
assert!(!is_valid_sql_expression("UNION ALL"));
assert!(!is_valid_sql_expression("x FROM y"));
assert!(!is_valid_sql_expression("x WHERE y"));
assert!(!is_valid_sql_expression("pg_catalog.pg_tables"));
assert!(!is_valid_sql_expression("sqlite_master"));
assert!(!is_valid_sql_expression("information_schema.tables"));
assert!(!is_valid_sql_expression("0x48454C4C4F"));
assert!(!is_valid_sql_expression("\\x48454C4C4F"));
assert!(!is_valid_sql_expression("SLEEP(10)"));
assert!(!is_valid_sql_expression("pg_sleep(5)"));
assert!(!is_valid_sql_expression("BENCHMARK(1000000, SHA1('test'))"));
assert!(!is_valid_sql_expression("WAITFOR DELAY '0:0:5'"));
assert!(!is_valid_sql_expression("LOAD_FILE('/etc/passwd')"));
}
#[test]
#[should_panic(expected = "Invalid SQL expression")]
fn test_assert_valid_expression_panics() {
assert_valid_sql_expression("1; DROP TABLE users", "computed field");
}
#[test]
fn test_sqli_classic_or_true() {
assert!(!is_valid_sql_expression("' OR 1=1--")); assert!(!is_valid_sql_expression("1; OR 1=1")); }
#[test]
fn test_sqli_drop_table() {
assert!(!is_valid_sql_expression("'; DROP TABLE users--"));
assert!(!is_valid_sql_expression("'; DROP TABLE users;--"));
assert!(!is_valid_sql_expression("1; DROP TABLE users"));
assert!(!is_valid_sql_expression("DROP TABLE users"));
assert!(!is_valid_sql_expression("drop table users"));
assert!(!is_valid_sql_expression("DrOp TaBlE users"));
}
#[test]
fn test_sqli_union_attacks() {
assert!(!is_valid_sql_expression("' UNION SELECT * FROM users--"));
assert!(!is_valid_sql_expression(
"' UNION ALL SELECT password FROM users--"
));
assert!(!is_valid_sql_expression("1 UNION SELECT 1,2,3"));
assert!(!is_valid_sql_expression(
"UNION SELECT username,password FROM admin"
));
assert!(!is_valid_sql_expression("' union select null,null,null--"));
}
#[test]
fn test_sqli_comment_injection() {
assert!(!is_valid_sql_expression("admin'--")); assert!(!is_valid_sql_expression("admin'/*")); assert!(!is_valid_sql_expression("*/; DROP TABLE users--")); assert!(!is_valid_sql_expression("1/**/OR/**/1=1")); }
#[test]
fn test_sqli_stacked_queries() {
assert!(!is_valid_sql_expression(
"; INSERT INTO users VALUES('hacker')"
));
assert!(!is_valid_sql_expression("; UPDATE users SET role='admin'"));
assert!(!is_valid_sql_expression("; DELETE FROM users"));
assert!(!is_valid_sql_expression("1; SELECT * FROM passwords"));
assert!(!is_valid_sql_expression("'; TRUNCATE TABLE logs;--"));
}
#[test]
fn test_sqli_time_based_blind() {
assert!(!is_valid_sql_expression("SLEEP(5)"));
assert!(!is_valid_sql_expression("1 AND SLEEP(5)"));
assert!(!is_valid_sql_expression("pg_sleep(5)"));
assert!(!is_valid_sql_expression("1; SELECT pg_sleep(10)"));
assert!(!is_valid_sql_expression("BENCHMARK(10000000,SHA1('test'))"));
assert!(!is_valid_sql_expression("WAITFOR DELAY '0:0:5'"));
assert!(!is_valid_sql_expression("dbms_lock.sleep(5)"));
}
#[test]
fn test_sqli_file_operations() {
assert!(!is_valid_sql_expression("LOAD_FILE('/etc/passwd')"));
assert!(!is_valid_sql_expression("load_file('/etc/shadow')"));
assert!(!is_valid_sql_expression(
"INTO OUTFILE '/var/www/shell.php'"
));
assert!(!is_valid_sql_expression("INTO DUMPFILE '/tmp/data'"));
assert!(!is_valid_sql_expression("into_outfile('/tmp/x')"));
assert!(!is_valid_sql_expression("into_dumpfile('/tmp/x')"));
}
#[test]
fn test_sqli_system_catalog_access() {
assert!(!is_valid_sql_expression("pg_tables"));
assert!(!is_valid_sql_expression("pg_catalog.pg_tables"));
assert!(!is_valid_sql_expression("sqlite_master"));
assert!(!is_valid_sql_expression("information_schema.tables"));
assert!(!is_valid_sql_expression("sys.tables"));
assert!(!is_valid_sql_expression("SELECT FROM information_schema"));
}
#[test]
fn test_sqli_hex_encoding() {
assert!(!is_valid_sql_expression("0x27")); assert!(!is_valid_sql_expression("0x4F5220313D31")); assert!(!is_valid_sql_expression("\\x27"));
assert!(!is_valid_sql_expression("CHAR(0x27)"));
}
#[test]
fn test_sqli_keyword_boundary_detection() {
assert!(is_valid_sql_expression("order_id")); assert!(is_valid_sql_expression("reorder_count")); assert!(is_valid_sql_expression("group_name")); assert!(is_valid_sql_expression("ungroup")); assert!(is_valid_sql_expression("from_date")); assert!(is_valid_sql_expression("wherefrom")); assert!(is_valid_sql_expression("selected_items")); assert!(is_valid_sql_expression("preselect")); assert!(is_valid_sql_expression("delete_flag")); assert!(is_valid_sql_expression("undelete")); assert!(is_valid_sql_expression("update_time")); assert!(is_valid_sql_expression("last_updated"));
assert!(!is_valid_sql_expression("ORDER BY name"));
assert!(!is_valid_sql_expression("GROUP BY id"));
assert!(!is_valid_sql_expression("FROM users"));
assert!(!is_valid_sql_expression("WHERE id=1"));
assert!(!is_valid_sql_expression("SELECT *"));
assert!(!is_valid_sql_expression("DELETE FROM"));
assert!(!is_valid_sql_expression("UPDATE SET"));
}
#[test]
fn test_sqli_case_variations() {
assert!(!is_valid_sql_expression("SELECT"));
assert!(!is_valid_sql_expression("select"));
assert!(!is_valid_sql_expression("SeLeCt"));
assert!(!is_valid_sql_expression("sElEcT"));
assert!(!is_valid_sql_expression("UNION"));
assert!(!is_valid_sql_expression("union"));
assert!(!is_valid_sql_expression("UnIoN"));
assert!(!is_valid_sql_expression("DROP"));
assert!(!is_valid_sql_expression("drop"));
assert!(!is_valid_sql_expression("DrOp"));
}
#[test]
fn test_sqli_whitespace_variations() {
assert!(!is_valid_sql_expression("SELECT\t*"));
assert!(!is_valid_sql_expression("SELECT\n*"));
assert!(!is_valid_sql_expression(" SELECT "));
assert!(!is_valid_sql_expression("DROP\t\tTABLE"));
}
#[test]
fn test_sqli_expression_length_limit() {
let long_expr = "a".repeat(1001);
assert!(!is_valid_sql_expression(&long_expr));
let at_limit = "a".repeat(1000);
assert!(is_valid_sql_expression(&at_limit));
}
#[test]
fn test_valid_safe_expressions() {
assert!(is_valid_sql_expression("first_name || ' ' || last_name"));
assert!(is_valid_sql_expression("price * quantity"));
assert!(is_valid_sql_expression("price * 1.15")); assert!(is_valid_sql_expression(
"COALESCE(nickname, first_name, 'Anonymous')"
));
assert!(is_valid_sql_expression("UPPER(TRIM(name))"));
assert!(is_valid_sql_expression("LENGTH(description)"));
assert!(is_valid_sql_expression("ABS(balance)"));
assert!(is_valid_sql_expression("ROUND(price, 2)"));
assert!(is_valid_sql_expression("LOWER(email)"));
assert!(is_valid_sql_expression("created_at + INTERVAL '1 day'"));
assert!(is_valid_sql_expression("age >= 18"));
assert!(is_valid_sql_expression("status = 'active'"));
assert!(is_valid_sql_expression("NOT is_deleted"));
assert!(is_valid_sql_expression("(price > 0) AND (quantity > 0)"));
}
}