diff --git a/src/main.rs b/src/main.rs index dde8449..33ccb1b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,6 +20,7 @@ use std::{ use walkdir::{DirEntry, WalkDir}; use actix_files::NamedFile; +use actix_cors::Cors; use actix_multipart as mp; use actix_web::{ App, HttpRequest, HttpResponse, HttpServer, Responder, delete, get, middleware, post, put, @@ -430,8 +431,34 @@ async fn get_video_part( let mut file_part = PathBuf::new(); file_part.push(app_state.video_path.clone()); file_part.push(part); - // TODO: Do we need to guard against directory attacks here? - match NamedFile::open(&file_part) { + + // Guard against directory traversal attacks + let canonical_base = match std::fs::canonicalize(&app_state.video_path) { + Ok(path) => path, + Err(e) => { + error!("Failed to canonicalize video path: {:?}", e); + span.set_status(Status::error("Invalid video path configuration")); + return HttpResponse::InternalServerError().finish(); + } + }; + + let canonical_file = match std::fs::canonicalize(&file_part) { + Ok(path) => path, + Err(_) => { + warn!("Video part not found or invalid: {:?}", file_part); + span.set_status(Status::error(format!("Video part not found '{}'", part))); + return HttpResponse::NotFound().finish(); + } + }; + + // Ensure the resolved path is still within the video directory + if !canonical_file.starts_with(&canonical_base) { + warn!("Directory traversal attempt detected: {:?}", part); + span.set_status(Status::error("Invalid video path")); + return HttpResponse::Forbidden().finish(); + } + + match NamedFile::open(&canonical_file) { Ok(file) => { span.set_status(Status::Ok); file.into_response(&request) @@ -714,8 +741,30 @@ fn main() -> std::io::Result<()> { let favorites_dao = SqliteFavoriteDao::new(); let tag_dao = SqliteTagDao::default(); let exif_dao = SqliteExifDao::new(); + let cors = Cors::default() + .allowed_origin_fn(|origin, _req_head| { + // Allow all origins in development, or check against CORS_ALLOWED_ORIGINS env var + if let Ok(allowed_origins) = env::var("CORS_ALLOWED_ORIGINS") { + allowed_origins.split(',').any(|allowed| { + origin.as_bytes() == allowed.trim().as_bytes() + }) + } else { + // Default: allow all origins if not configured + true + } + }) + .allowed_methods(vec!["GET", "POST", "PUT", "DELETE", "OPTIONS"]) + .allowed_headers(vec![ + actix_web::http::header::AUTHORIZATION, + actix_web::http::header::ACCEPT, + actix_web::http::header::CONTENT_TYPE, + ]) + .supports_credentials() + .max_age(3600); + App::new() .wrap(middleware::Logger::default()) + .wrap(cors) .service(web::resource("/login").route(web::post().to(login::))) .service( web::resource("/photos")