pub struct SqlSanitizer;
impl SqlSanitizer {
pub fn sanitize(sql: &str) -> String {
let mut result = sql.to_string();
let sensitive_keywords = [
"password",
"passwd",
"pwd",
"token",
"api_key",
"apikey",
"secret",
"access_key",
"private_key",
];
for keyword in &sensitive_keywords {
result = Self::sanitize_keyword(&result, keyword);
}
result = Self::sanitize_base64_literals(&result);
result
}
fn sanitize_keyword(sql: &str, keyword: &str) -> String {
let lower = sql.to_lowercase();
let mut result = String::with_capacity(sql.len());
let mut i = 0;
let bytes = sql.as_bytes();
let lower_bytes = lower.as_bytes();
while i < bytes.len() {
if i + keyword.len() <= lower_bytes.len()
&& &lower_bytes[i..i + keyword.len()] == keyword.as_bytes()
{
let kw_end = i + keyword.len();
let after_kw = &sql[kw_end..];
if let Some((lit_start, lit_end, quote)) = Self::find_string_literal_after(after_kw)
{
result.push_str(&sql[i..kw_end]);
result.push_str(&sql[kw_end..kw_end + lit_start]);
result.push(quote);
result.push_str("***");
result.push(quote);
i = kw_end + lit_end;
continue;
}
}
result.push(bytes[i] as char);
i += 1;
}
result
}
fn find_string_literal_after(s: &str) -> Option<(usize, usize, char)> {
let bytes = s.as_bytes();
let mut i = 0;
while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t' || bytes[i] == b'\n') {
i += 1;
}
if i >= bytes.len() {
return None;
}
if bytes[i] != b'=' {
return None;
}
i += 1;
while i < bytes.len() && (bytes[i] == b' ' || bytes[i] == b'\t' || bytes[i] == b'\n') {
i += 1;
}
if i >= bytes.len() {
return None;
}
let quote = bytes[i];
if quote != b'\'' && quote != b'"' {
return None;
}
let literal_start = i;
i += 1;
while i < bytes.len() {
if bytes[i] == quote {
if i + 1 < bytes.len() && bytes[i + 1] == quote {
i += 2;
continue;
}
let literal_end = i + 1;
return Some((literal_start, literal_end, quote as char));
}
i += 1;
}
None
}
fn sanitize_base64_literals(sql: &str) -> String {
let bytes = sql.as_bytes();
let mut result = String::with_capacity(sql.len());
let mut i = 0;
while i < bytes.len() {
let quote = bytes[i];
if quote == b'\'' || quote == b'"' {
let literal_start = i;
i += 1;
let content_start = i;
let mut content_end = i;
while i < bytes.len() {
if bytes[i] == quote {
if i + 1 < bytes.len() && bytes[i + 1] == quote {
i += 2;
continue;
}
content_end = i;
break;
}
i += 1;
}
if i < bytes.len() {
let content = &sql[content_start..content_end];
if content.len() > 40 && Self::is_base64(content) {
result.push(quote as char);
result.push_str("***");
result.push(quote as char);
i += 1;
continue;
}
result.push_str(&sql[literal_start..i + 1]);
i += 1;
continue;
}
result.push_str(&sql[literal_start..]);
break;
}
result.push(quote as char);
i += 1;
}
result
}
fn is_base64(s: &str) -> bool {
if s.is_empty() {
return false;
}
let mut non_base64 = 0;
for c in s.chars() {
if !c.is_ascii_alphanumeric() && c != '+' && c != '/' && c != '=' {
non_base64 += 1;
}
}
let base64_ratio = 1.0 - (non_base64 as f64 / s.len() as f64);
base64_ratio > 0.95
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sanitize_password() {
let sql = "SELECT * FROM users WHERE password = 'secret123'";
let result = SqlSanitizer::sanitize(sql);
assert!(result.contains("'***'"));
assert!(!result.contains("secret123"));
}
#[test]
fn test_sanitize_token() {
let sql = "SELECT * FROM api_tokens WHERE token = 'abc123xyz'";
let result = SqlSanitizer::sanitize(sql);
assert!(result.contains("'***'"));
assert!(!result.contains("abc123xyz"));
}
#[test]
fn test_sanitize_api_key() {
let sql = "SELECT * FROM config WHERE api_key = 'sk-xxxx'";
let result = SqlSanitizer::sanitize(sql);
assert!(result.contains("'***'"));
assert!(!result.contains("sk-xxxx"));
}
#[test]
fn test_sanitize_base64_token() {
let long_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpmeJf36POk6yJV_adQssw5c";
let sql = format!("SELECT * FROM data WHERE value = '{}'", long_token);
let result = SqlSanitizer::sanitize(&sql);
assert!(result.contains("'***'"));
assert!(!result.contains(long_token));
}
#[test]
fn test_no_sanitize_normal_values() {
let sql = "SELECT * FROM users WHERE name = 'John' AND age = 30";
let result = SqlSanitizer::sanitize(sql);
assert!(result.contains("'John'"));
assert!(!result.contains("'***'"));
}
#[test]
fn test_sanitize_multiple_sensitive() {
let sql = "SELECT * FROM users WHERE password = 'pw1' AND token = 'tk1' AND secret = 'sc1'";
let result = SqlSanitizer::sanitize(sql);
assert!(!result.contains("pw1"));
assert!(!result.contains("tk1"));
assert!(!result.contains("sc1"));
let star_count = result.matches("'***'").count();
assert_eq!(star_count, 3);
}
#[test]
fn test_sanitize_preserves_structure() {
let sql = "SELECT id, name FROM users WHERE password = 'secret' LIMIT 10";
let result = SqlSanitizer::sanitize(sql);
assert!(result.starts_with("SELECT id, name FROM users WHERE password = "));
assert!(result.ends_with(" LIMIT 10"));
}
#[test]
fn test_sanitize_double_quotes() {
let sql = "SELECT * FROM users WHERE password = \"secret123\"";
let result = SqlSanitizer::sanitize(sql);
assert!(result.contains("\"***\""));
assert!(!result.contains("secret123"));
}
#[test]
fn test_sanitize_no_string_literal() {
let sql = "SELECT * FROM users WHERE password = ?";
let result = SqlSanitizer::sanitize(sql);
assert_eq!(result, sql);
}
}