From da16fddce3c3a5e923142bd2c52a20318aaffc9e Mon Sep 17 00:00:00 2001 From: Cameron Date: Fri, 10 Apr 2026 14:58:57 -0400 Subject: [PATCH] Address path traversal and other security fixes --- src/auth.rs | 6 +++--- src/data/mod.rs | 2 +- src/files.rs | 15 ++++++++------- src/main.rs | 14 +++++++------- 4 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/auth.rs b/src/auth.rs index ffc58d8..40f367a 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -85,7 +85,7 @@ pub async fn login( HttpResponse::Ok().json(Token { token: &token }) } else { error!("Failed login attempt for user: '{}'", creds.username); - HttpResponse::NotFound().finish() + HttpResponse::Unauthorized().finish() } } @@ -128,7 +128,7 @@ mod tests { } #[actix_rt::test] - async fn test_login_reports_404_when_user_does_not_exist() { + async fn test_login_reports_401_when_user_does_not_exist() { let mut dao = TestUserDao::new(); dao.create_user("user", "password"); @@ -139,6 +139,6 @@ mod tests { let response = login::(j, web::Data::new(Mutex::new(dao))).await; - assert_eq!(response.status(), 404); + assert_eq!(response.status(), 401); } } diff --git a/src/data/mod.rs b/src/data/mod.rs index 13317de..6935819 100644 --- a/src/data/mod.rs +++ b/src/data/mod.rs @@ -85,7 +85,7 @@ impl FromRequest for Claims { ) .and_then(|header| { Claims::from_str(header) - .with_context(|| format!("Unable to decode token from: {}", header)) + .with_context(|| "Unable to decode token from Authorization header") }) .map_or_else( |e| { diff --git a/src/files.rs b/src/files.rs index 5e7d00d..f3cd8fa 100644 --- a/src/files.rs +++ b/src/files.rs @@ -932,6 +932,7 @@ pub async fn get_gps_summary( request: HttpRequest, req: Query, exif_dao: Data>>, + app_state: Data, ) -> Result { use crate::data::{GpsPhotoSummary, GpsPhotosResponse}; @@ -952,17 +953,17 @@ pub async fn get_gps_summary( // The database stores relative paths, so we use the path as-is // Normalize empty path or "/" to return all GPS photos let requested_path = if req.path.is_empty() || req.path == "/" { - "" + String::new() } else { - // Just do basic validation to prevent path traversal - if req.path.contains("..") { - warn!("Path traversal attempt: {}", req.path); + // Validate path using the same check as all other endpoints + if is_valid_full_path(&app_state.base_path, &req.path, false).is_none() { + warn!("Invalid path for GPS summary: {}", req.path); cx.span().set_status(Status::error("Invalid path")); - return Ok(HttpResponse::Forbidden().json(serde_json::json!({ + return Ok(HttpResponse::BadRequest().json(serde_json::json!({ "error": "Invalid path" }))); } - req.path.as_str() + req.path.clone() }; let recursive = req.recursive.unwrap_or(false); @@ -973,7 +974,7 @@ pub async fn get_gps_summary( // Query database for all photos with GPS let mut exif_dao_guard = exif_dao.lock().expect("Unable to get ExifDao"); - match exif_dao_guard.get_all_with_gps(&cx, requested_path, recursive) { + match exif_dao_guard.get_all_with_gps(&cx, &requested_path, recursive) { Ok(gps_data) => { let mut photos: Vec = gps_data .into_iter() diff --git a/src/main.rs b/src/main.rs index ec2be62..ab6a48d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -503,14 +503,10 @@ async fn stream_video( let playlist = &path.path; debug!("Playlist: {}", playlist); - // Extract video playlist dir to dotenv - if !playlist.starts_with(&app_state.video_path) - && is_valid_full_path(&app_state.base_path, playlist, false).is_some() + // Only serve files under video_path (HLS playlists) or base_path (source videos) + if playlist.starts_with(&app_state.video_path) + || is_valid_full_path(&app_state.base_path, playlist, false).is_some() { - span.set_status(Status::error(format!("playlist not valid {}", playlist))); - - HttpResponse::BadRequest().finish() - } else { match NamedFile::open(playlist) { Ok(file) => { span.set_status(Status::Ok); @@ -521,6 +517,9 @@ async fn stream_video( HttpResponse::NotFound().finish() } } + } else { + span.set_status(Status::error(format!("playlist not valid {}", playlist))); + HttpResponse::BadRequest().finish() } } @@ -1209,6 +1208,7 @@ fn main() -> std::io::Result<()> { .app_data::>>(Data::new(Mutex::new( SqliteKnowledgeDao::new(), ))) + .app_data(mp::form::MultipartFormConfig::default().total_limit(1024 * 1024 * 1024)) // 1GB upload limit .app_data(web::JsonConfig::default().error_handler(|err, req| { let detail = err.to_string(); log::warn!(