use actix_web::HttpRequest; use log::warn; use std::collections::HashMap; use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; use tokio::sync::RwLock; /// Простая защита от злоупотреблений для upload endpoint #[derive(Debug, Clone)] pub struct UploadProtection { /// Максимальное количество загрузок в минуту с одного IP pub max_uploads_per_minute: u32, /// Локальный кэш для подсчета загрузок pub upload_counts: Arc>>, } /// Конфигурация безопасности для простого storage proxy #[derive(Debug, Clone)] pub struct SecurityConfig { /// Максимальный размер тела запроса (байты) pub max_payload_size: usize, /// Таймаут запроса (секунды) pub request_timeout_seconds: u64, /// Максимальная длина пути pub max_path_length: usize, /// Максимальное количество заголовков pub max_headers_count: usize, /// Максимальная длина значения заголовка pub max_header_value_length: usize, /// Защита от злоупотреблений upload pub upload_protection: UploadProtection, } impl Default for SecurityConfig { fn default() -> Self { Self { max_payload_size: 500 * 1024 * 1024, // 500MB request_timeout_seconds: 300, // 5 минут max_path_length: 1000, max_headers_count: 50, max_header_value_length: 8192, upload_protection: UploadProtection { max_uploads_per_minute: 10, // 10 загрузок в минуту upload_counts: Arc::new(RwLock::new(HashMap::new())), }, } } } impl SecurityConfig { /// Валидирует запрос на базовые параметры безопасности pub fn validate_request(&self, req: &HttpRequest) -> Result<(), actix_web::Error> { let path = req.path(); // Проверка длины пути if path.len() > self.max_path_length { warn!("Path too long: {} chars", path.len()); return Err(actix_web::error::ErrorBadRequest("Path too long")); } // Проверка количества заголовков if req.headers().len() > self.max_headers_count { warn!("Too many headers: {}", req.headers().len()); return Err(actix_web::error::ErrorBadRequest("Too many headers")); } // Проверка длины значений заголовков for (name, value) in req.headers() { if let Ok(value_str) = value.to_str() { if value_str.len() > self.max_header_value_length { warn!( "Header value too long: {} = {} chars", name, value_str.len() ); return Err(actix_web::error::ErrorBadRequest("Header value too long")); } } } // Проверка на подозрительные символы в пути if path.contains("..") || path.contains('\0') || path.contains('\r') || path.contains('\n') { warn!("Suspicious characters in path: {}", path); return Err(actix_web::error::ErrorBadRequest( "Invalid characters in path", )); } // Проверка на подозрительные паттерны if self.check_suspicious_patterns(path) { return Err(actix_web::error::ErrorBadRequest("Suspicious path pattern")); } Ok(()) } /// Проверяет путь на подозрительные паттерны (молча отклоняет сканы) pub fn check_suspicious_patterns(&self, path: &str) -> bool { let suspicious_patterns = [ // WordPress scanning patterns "/wp-admin", "/wp-includes/", "/wp-content/", "/wp-login.php", "/wp-config.php", "/xmlrpc.php", "/wlwmanifest.xml", "/wp-json/", "/wordpress/", "wp-includes", // Добавлено для любых подпапок "wlwmanifest", // Добавлено без слеша // Admin panels "/admin", "/phpmyadmin", "/cpanel", "/plesk", // Config & sensitive files "/.env", "/config", "/.git", "/backup", "/db", "/sql", "/.htaccess", "/web.config", // XSS & injection patterns "script>", " Result<(), actix_web::Error> { let current_time = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() .as_secs(); let mut counts = self.upload_protection.upload_counts.write().await; // Очищаем старые записи (старше минуты) counts.retain(|_, (_, timestamp)| current_time - *timestamp < 60); // Проверяем текущий IP let current_count = counts.get(ip).map(|(count, _)| *count).unwrap_or(0); let first_upload_time = counts .get(ip) .map(|(_, time)| *time) .unwrap_or(current_time); if current_time - first_upload_time < 60 { // В пределах минуты if current_count >= self.upload_protection.max_uploads_per_minute { warn!( "Upload limit exceeded for IP {}: {} uploads in minute", ip, current_count ); return Err(actix_web::error::ErrorTooManyRequests( "Upload limit exceeded", )); } counts.insert(ip.to_string(), (current_count + 1, first_upload_time)); } else { // Новая минута, сбрасываем счетчик counts.insert(ip.to_string(), (1, current_time)); } Ok(()) } /// Извлекает IP адрес клиента pub fn extract_client_ip(req: &HttpRequest) -> String { // Проверяем X-Forwarded-For (для прокси) if let Some(forwarded) = req.headers().get("x-forwarded-for") { if let Ok(forwarded_str) = forwarded.to_str() { if let Some(first_ip) = forwarded_str.split(',').next() { return first_ip.trim().to_string(); } } } // Проверяем X-Real-IP if let Some(real_ip) = req.headers().get("x-real-ip") { if let Ok(real_ip_str) = real_ip.to_str() { return real_ip_str.to_string(); } } // Fallback на connection info req.connection_info() .peer_addr() .unwrap_or("unknown") .to_string() } }