- Enforce axum best practices across all 13 workspace crates (max 200 LOC/file, no comments, no unwrap, clean architecture) - Fix domain→infrastructure dependency inversions in imphnen-iam and imphnen-dimentorin - Extract imphnen-storage (MinIO) and imphnen-email (Lettre) as standalone crates - Centralize all config in ENV struct: CDN_URL, CORS_ALLOWED_ORIGINS - Centralize SMTP through imphnen-email; remove dead HackathonConfig - Centralize database: QR crate now shares main DB pool (single DATABASE_URL) - Rename QR users table to qr_users to avoid collision with main users table - Merge imphnen-qr into imphnen-cms/src/qr (13 crates, down from 14) - Restructure imphnen-hackathon flat modules into clean architecture - Remove all stale env vars from .env.example (SurrealDB, QR_JWT, Hackathon infra) - Fix Dockerfile to include all current workspace crates - Bump all crate versions 0.2.0 → 0.3.0 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
92 lines
2.5 KiB
Rust
92 lines
2.5 KiB
Rust
use regex::Regex;
|
|
use std::sync::LazyLock;
|
|
|
|
static SQL_INJECTION_PATTERNS: LazyLock<Regex> = LazyLock::new(|| {
|
|
Regex::new(r"(?i)(union|select|insert|update|delete|drop|create|alter|truncate|vacuum|analyze|reindex|cluster|copy|exec|script|javascript|onerror|onload|with|from|where|join|group by|order by|limit|offset|having|distinct|into|values|union all|union distinct|::|%|:=|current_user|session_user|user|version|current_date|current_time|now|pg_sleep|pg_user|pg_database|pg_tables|pg_columns|chr|ascii|substring|position|strpos|concat|concat_ws|string_agg|array_agg|array_to_string|string_to_array)").expect("valid sql injection regex")
|
|
});
|
|
|
|
static PATH_TRAVERSAL_REGEX: LazyLock<Regex> =
|
|
LazyLock::new(|| Regex::new(r"\.\.(/|\\)").expect("valid path traversal regex"));
|
|
|
|
pub fn sanitize_html(input: &str) -> String {
|
|
input
|
|
.chars()
|
|
.map(|c| match c {
|
|
'<' => "<".to_string(),
|
|
'>' => ">".to_string(),
|
|
'"' => """.to_string(),
|
|
'\'' => "'".to_string(),
|
|
'&' => "&".to_string(),
|
|
_ => c.to_string(),
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
pub fn sanitize_dangerous_patterns(input: &str) -> String {
|
|
let without_sql_injection =
|
|
SQL_INJECTION_PATTERNS.replace_all(input, "[FILTERED]");
|
|
let without_postgres_specific =
|
|
without_sql_injection.replace(";--", ";[FILTERED]");
|
|
without_postgres_specific.to_owned()
|
|
}
|
|
|
|
pub fn contains_path_traversal(input: &str) -> bool {
|
|
PATH_TRAVERSAL_REGEX.is_match(input)
|
|
}
|
|
|
|
pub fn sanitize_filename(input: &str) -> String {
|
|
input
|
|
.chars()
|
|
.map(|c| match c {
|
|
'/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_',
|
|
c if c.is_control() => '_',
|
|
c => c,
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
pub fn sanitize_user_text(input: &str) -> String {
|
|
let without_html = sanitize_html(input);
|
|
sanitize_dangerous_patterns(&without_html)
|
|
}
|
|
|
|
pub fn normalize_whitespace(input: &str) -> String {
|
|
input
|
|
.split_whitespace()
|
|
.collect::<Vec<_>>()
|
|
.join(" ")
|
|
.trim()
|
|
.to_string()
|
|
}
|
|
|
|
pub fn sanitize_email(email: &str) -> Option<String> {
|
|
let trimmed = email.trim().to_lowercase();
|
|
|
|
if trimmed.contains('@') && trimmed.contains('.') {
|
|
Some(trimmed)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
pub fn sanitize_url(url: &str) -> Option<String> {
|
|
let trimmed = url.trim();
|
|
|
|
let lower = trimmed.to_lowercase();
|
|
if lower.starts_with("javascript:")
|
|
|| lower.starts_with("data:")
|
|
|| lower.starts_with("vbscript:")
|
|
{
|
|
return None;
|
|
}
|
|
|
|
if lower.starts_with("http://")
|
|
|| lower.starts_with("https://")
|
|
|| lower.starts_with("/")
|
|
{
|
|
Some(trimmed.to_string())
|
|
} else {
|
|
None
|
|
}
|
|
}
|