🔒 Implement comprehensive security and DDoS protection

### Security Features:
- **Rate Limiting**: Redis-based IP tracking with configurable limits
  - General: 100 requests/minute (5min block)
  - Upload: 10 requests/5min (10min block)
  - Auth: 20 requests/15min (30min block)
- **Request Validation**: Path length, header count, suspicious patterns
- **Attack Detection**: Admin paths, script injections, bot patterns
- **Enhanced JWT**: Format validation, length checks, character filtering
- **IP Tracking**: X-Forwarded-For and X-Real-IP support

### Security Headers:
- X-Content-Type-Options: nosniff
- X-Frame-Options: DENY
- X-XSS-Protection: 1; mode=block
- Content-Security-Policy with strict rules
- Strict-Transport-Security with includeSubDomains

### CORS Hardening:
- Limited to specific domains: discours.io, new.discours.io
- Restricted methods: GET, POST, OPTIONS only
- Essential headers only

### Infrastructure:
- Security middleware for all requests
- Local cache + Redis for performance
- Comprehensive logging and monitoring
- Progressive blocking for repeat offenders

### Documentation:
- Complete security guide (docs/security.md)
- Configuration examples
- Incident response procedures
- Monitoring recommendations

Version bump to 0.6.0 for major security enhancement.
This commit is contained in:
2025-09-02 11:40:43 +03:00
parent d3bee5144f
commit 82668768d0
14 changed files with 803 additions and 124 deletions

View File

@@ -7,16 +7,9 @@ use crate::handlers::serve_file::serve_file;
use crate::lookup::{find_file_by_pattern, get_mime_type};
use crate::s3_utils::{check_file_exists, load_file_from_s3, upload_to_s3};
use crate::thumbnail::{find_closest_width, parse_file_path, thumbdata_save};
use super::common::{check_etag_cache, create_cached_response};
/// Создает HTTP ответ с оптимальными заголовками кэширования
fn create_cached_response(content_type: &str, data: Vec<u8>, file_etag: &str) -> HttpResponse {
HttpResponse::Ok()
.content_type(content_type)
.insert_header(("etag", file_etag))
.insert_header(("cache-control", "public, max-age=31536000, immutable")) // 1 год
.insert_header(("access-control-allow-origin", "*"))
.body(data)
}
// Удалена дублирующая функция, используется из common модуля
/// Обработчик для скачивания файла и генерации миниатюры, если она недоступна.
#[allow(clippy::collapsible_if)]
@@ -28,12 +21,6 @@ pub async fn proxy_handler(
let start_time = std::time::Instant::now();
info!("GET {} [START]", requested_res);
// Возвращаем 404 для .well-known путей (для Let's Encrypt ACME)
if requested_res.starts_with(".well-known/") {
warn!("ACME challenge path requested: {}", requested_res);
return Err(ErrorNotFound("Not found"));
}
let normalized_path = if requested_res.ends_with("/webp") {
info!("Converting to WebP format: {}", requested_res);
requested_res.replace("/webp", "")
@@ -41,13 +28,7 @@ pub async fn proxy_handler(
requested_res.to_string()
};
// Проверяем If-None-Match заголовок для кэширования
let client_etag = req
.headers()
.get("if-none-match")
.and_then(|h| h.to_str().ok());
// парсим GET запрос
// Парсим GET запрос
let (base_filename, requested_width, extension) = parse_file_path(&normalized_path);
let ext = extension.as_str().to_lowercase();
let filekey = format!("{}.{}", base_filename, &ext);
@@ -57,13 +38,11 @@ pub async fn proxy_handler(
base_filename, requested_width, ext
);
// Генерируем ETag для кэширования
// Генерируем ETag для кэширования и проверяем кэш
let file_etag = format!("\"{}\"", &filekey);
if let Some(etag) = client_etag {
if etag == file_etag {
info!("Cache hit for {}, returning 304", filekey);
return Ok(HttpResponse::NotModified().finish());
}
if let Some(response) = check_etag_cache(&req, &file_etag) {
info!("Cache hit for {}, returning 304", filekey);
return Ok(response);
}
let content_type = match get_mime_type(&ext) {
Some(mime) => mime.to_string(),