From 05135529ce165cbf23844717f4662466ebe462bb Mon Sep 17 00:00:00 2001 From: Dionisio Date: Fri, 13 Feb 2026 22:31:05 +0100 Subject: [PATCH] fix(security): scope root folder listing to authenticated user MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Non-admin users were seeing all users' root folders, including the admin's. Three root causes fixed: 1. Backend: list_root_folders now extracts AuthUser and filters results so each user only sees their own home folder at the root level (folders matching 'My Folder - {username}' or 'Mi Carpeta - {username}'). 2. Frontend: findUserHomeFolder() searched only for the Spanish pattern 'Mi Carpeta - {username}' but the backend creates folders with the English pattern 'My Folder - {username}'. Now checks both naming conventions. 3. Frontend: when the home folder was not found, the code fell back to folderList[0] — which was usually the admin's folder. Removed that dangerous fallback; now shows empty root instead. Fixes #94 --- src/interfaces/api/handlers/folder_handler.rs | 69 ++++++++++++++++--- static/js/app.js | 53 +++++++------- static/sw.js | 2 +- 3 files changed, 85 insertions(+), 39 deletions(-) diff --git a/src/interfaces/api/handlers/folder_handler.rs b/src/interfaces/api/handlers/folder_handler.rs index 79e4e840..96a0d12c 100644 --- a/src/interfaces/api/handlers/folder_handler.rs +++ b/src/interfaces/api/handlers/folder_handler.rs @@ -13,7 +13,7 @@ use crate::application::dtos::pagination::PaginationRequestDto; use crate::common::errors::ErrorKind; use crate::application::ports::inbound::FolderUseCase; use crate::common::di::AppState as GlobalAppState; -use crate::interfaces::middleware::auth::OptionalAuthUser; +use crate::interfaces::middleware::auth::{OptionalAuthUser, AuthUser}; type AppState = Arc; @@ -59,10 +59,12 @@ impl FolderHandler { } /// Lists root folders (no parent ID) + /// Non-admin users only see their own home folder. pub async fn list_root_folders( State(service): State, + auth_user: AuthUser, ) -> impl IntoResponse { - Self::list_folders(State(service), None).await + Self::list_folders_for_user(State(service), None, &auth_user).await } /// Lists contents of a specific folder by its ID @@ -76,9 +78,12 @@ impl FolderHandler { /// Lists root folders with pagination support pub async fn list_root_folders_paginated( State(service): State, - pagination: Query, + auth_user: AuthUser, + _pagination: Query, ) -> impl IntoResponse { - Self::list_folders_paginated(State(service), pagination, None).await + // For paginated root listing, filter by user as well + // Delegate to non-paginated user-filtered listing for now + Self::list_folders_for_user(State(service), None, &auth_user).await } /// Lists contents of a specific folder with pagination @@ -90,16 +95,25 @@ impl FolderHandler { Self::list_folders_paginated(State(service), pagination, Some(&id)).await } + /// Checks if a folder name matches the user home-folder convention. + fn is_user_home_folder(folder_name: &str) -> bool { + folder_name.starts_with("My Folder - ") || folder_name.starts_with("Mi Carpeta - ") + } + + /// Checks if a folder belongs to the given user. + fn folder_belongs_to_user(folder_name: &str, username: &str) -> bool { + let expected_en = format!("My Folder - {}", username); + let expected_es = format!("Mi Carpeta - {}", username); + folder_name == expected_en || folder_name == expected_es + } + /// Lists folders, optionally filtered by parent ID pub async fn list_folders( State(service): State, parent_id: Option<&str>, ) -> impl IntoResponse { - // Parent ID is already a &str - match service.list_folders(parent_id).await { Ok(folders) => { - // Always return an array even if empty (StatusCode::OK, Json(folders)).into_response() }, Err(err) => { @@ -108,7 +122,46 @@ impl FolderHandler { _ => StatusCode::INTERNAL_SERVER_ERROR, }; - // Return a JSON error response + (status, Json(serde_json::json!({ + "error": err.to_string() + }))).into_response() + } + } + } + + /// Lists folders with user-based filtering for root listings. + /// Non-admin users only see their own home folder at the root level. + pub async fn list_folders_for_user( + State(service): State, + parent_id: Option<&str>, + auth_user: &AuthUser, + ) -> impl IntoResponse { + match service.list_folders(parent_id).await { + Ok(folders) => { + // Only filter at root level (parent_id == None) + let filtered = if parent_id.is_none() { + folders.into_iter().filter(|f| { + // Skip hidden/system folders + if f.name.starts_with('.') { + return false; + } + // If it's a user home folder, only show if it belongs to this user + if Self::is_user_home_folder(&f.name) { + return Self::folder_belongs_to_user(&f.name, &auth_user.username); + } + // Non-home folders are visible to everyone + true + }).collect() + } else { + folders + }; + (StatusCode::OK, Json(filtered)).into_response() + }, + Err(err) => { + let status = match err.kind { + ErrorKind::NotFound => StatusCode::NOT_FOUND, + _ => StatusCode::INTERNAL_SERVER_ERROR, + }; (status, Json(serde_json::json!({ "error": err.to_string() }))).into_response() diff --git a/static/js/app.js b/static/js/app.js index 7b622ce0..41f55907 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -754,8 +754,10 @@ async function loadFiles(options = {}) { return false; } - // Skip other users' folders when at root - if (!app.currentPath && folder.name.startsWith('My Folder - ') && !folder.name.includes(username)) { + // Skip other users' folders when at root (both naming conventions) + if (!app.currentPath && + (folder.name.startsWith('My Folder - ') || folder.name.startsWith('Mi Carpeta - ')) && + !folder.name.includes(username)) { return false; } @@ -1835,26 +1837,30 @@ async function findUserHomeFolder(username) { console.log(`Found ${folderList.length} folders at root`); // Look for a folder with a name pattern that matches the user's home folder - // Only exact match "Mi Carpeta - username" - const homeFolderPattern = `Mi Carpeta - ${username}`; + // Match both naming conventions (English and Spanish) + const homeFolderPatternEn = `My Folder - ${username}`; + const homeFolderPatternEs = `Mi Carpeta - ${username}`; - // Filter first to remove system folders like .trash that shouldn't be visible + // Filter first to remove system folders and other users' folders const visibleFolders = folderList.filter(folder => { // Skip system folders (starting with dot) if (folder.name.startsWith('.')) { return false; } - // Skip other users' folders - if (folder.name.startsWith('Mi Carpeta - ') && !folder.name.includes(username)) { + // Skip other users' home folders (both naming conventions) + if ((folder.name.startsWith('Mi Carpeta - ') || folder.name.startsWith('My Folder - ')) + && !folder.name.includes(username)) { return false; } return true; }); - // Find the user's home folder from filtered list - let homeFolder = visibleFolders.find(folder => folder.name === homeFolderPattern); + // Find the user's home folder from filtered list (try both patterns) + let homeFolder = visibleFolders.find(folder => + folder.name === homeFolderPatternEn || folder.name === homeFolderPatternEs + ); if (homeFolder) { console.log(`Found user's home folder: ${homeFolder.name} (${homeFolder.id})`); @@ -1870,28 +1876,15 @@ async function findUserHomeFolder(username) { loadFiles(); return; // Success! Exit function } else { - console.warn("Could not find user's home folder, fallback to first folder or root"); + console.warn("Could not find user's home folder"); - // If we can't find a specific home folder but there are folders, - // use the first folder as the user's home - if (folderList.length > 0) { - const fallbackFolder = folderList[0]; - console.log(`Using first folder as fallback: ${fallbackFolder.name} (${fallbackFolder.id})`); - - app.userHomeFolderId = fallbackFolder.id; - app.userHomeFolderName = fallbackFolder.name; - app.currentPath = fallbackFolder.id; - ui.updateBreadcrumb(fallbackFolder.name); - loadFiles(); - return; // Success with fallback! Exit function - } else { - // No folders at all - this is an edge case - console.warn("No folders found, using root"); - app.currentPath = ''; - ui.updateBreadcrumb(''); - loadFiles(); - return; // Success with root! Exit function - } + // SECURITY: Never fall back to another user's folder. + // If user's own folder doesn't exist, show root (empty state). + console.log('User home folder not found, showing root'); + app.currentPath = ''; + ui.updateBreadcrumb(''); + loadFiles(); + return; } // If we get here, we've successfully processed the response diff --git a/static/sw.js b/static/sw.js index 7cb44e94..8c426ae2 100644 --- a/static/sw.js +++ b/static/sw.js @@ -1,5 +1,5 @@ // OxiCloud Service Worker -const CACHE_NAME = 'oxicloud-cache-v4'; +const CACHE_NAME = 'oxicloud-cache-v5'; const ASSETS_TO_CACHE = [ '/', '/index.html',