fix(security): scope root folder listing to authenticated user

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
This commit is contained in:
Dionisio
2026-02-13 22:31:05 +01:00
parent ce9971b9e4
commit 05135529ce
3 changed files with 85 additions and 39 deletions
+61 -8
View File
@@ -13,7 +13,7 @@ use crate::application::dtos::pagination::PaginationRequestDto;
use crate::common::errors::ErrorKind; use crate::common::errors::ErrorKind;
use crate::application::ports::inbound::FolderUseCase; use crate::application::ports::inbound::FolderUseCase;
use crate::common::di::AppState as GlobalAppState; use crate::common::di::AppState as GlobalAppState;
use crate::interfaces::middleware::auth::OptionalAuthUser; use crate::interfaces::middleware::auth::{OptionalAuthUser, AuthUser};
type AppState = Arc<FolderService>; type AppState = Arc<FolderService>;
@@ -59,10 +59,12 @@ impl FolderHandler {
} }
/// Lists root folders (no parent ID) /// Lists root folders (no parent ID)
/// Non-admin users only see their own home folder.
pub async fn list_root_folders( pub async fn list_root_folders(
State(service): State<AppState>, State(service): State<AppState>,
auth_user: AuthUser,
) -> impl IntoResponse { ) -> 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 /// Lists contents of a specific folder by its ID
@@ -76,9 +78,12 @@ impl FolderHandler {
/// Lists root folders with pagination support /// Lists root folders with pagination support
pub async fn list_root_folders_paginated( pub async fn list_root_folders_paginated(
State(service): State<AppState>, State(service): State<AppState>,
pagination: Query<PaginationRequestDto>, auth_user: AuthUser,
_pagination: Query<PaginationRequestDto>,
) -> impl IntoResponse { ) -> 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 /// Lists contents of a specific folder with pagination
@@ -90,16 +95,25 @@ impl FolderHandler {
Self::list_folders_paginated(State(service), pagination, Some(&id)).await 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 /// Lists folders, optionally filtered by parent ID
pub async fn list_folders( pub async fn list_folders(
State(service): State<AppState>, State(service): State<AppState>,
parent_id: Option<&str>, parent_id: Option<&str>,
) -> impl IntoResponse { ) -> impl IntoResponse {
// Parent ID is already a &str
match service.list_folders(parent_id).await { match service.list_folders(parent_id).await {
Ok(folders) => { Ok(folders) => {
// Always return an array even if empty
(StatusCode::OK, Json(folders)).into_response() (StatusCode::OK, Json(folders)).into_response()
}, },
Err(err) => { Err(err) => {
@@ -108,7 +122,46 @@ impl FolderHandler {
_ => StatusCode::INTERNAL_SERVER_ERROR, _ => 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<AppState>,
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!({ (status, Json(serde_json::json!({
"error": err.to_string() "error": err.to_string()
}))).into_response() }))).into_response()
+23 -30
View File
@@ -754,8 +754,10 @@ async function loadFiles(options = {}) {
return false; return false;
} }
// Skip other users' folders when at root // Skip other users' folders when at root (both naming conventions)
if (!app.currentPath && folder.name.startsWith('My Folder - ') && !folder.name.includes(username)) { if (!app.currentPath &&
(folder.name.startsWith('My Folder - ') || folder.name.startsWith('Mi Carpeta - ')) &&
!folder.name.includes(username)) {
return false; return false;
} }
@@ -1835,26 +1837,30 @@ async function findUserHomeFolder(username) {
console.log(`Found ${folderList.length} folders at root`); console.log(`Found ${folderList.length} folders at root`);
// Look for a folder with a name pattern that matches the user's home folder // Look for a folder with a name pattern that matches the user's home folder
// Only exact match "Mi Carpeta - username" // Match both naming conventions (English and Spanish)
const homeFolderPattern = `Mi Carpeta - ${username}`; 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 => { const visibleFolders = folderList.filter(folder => {
// Skip system folders (starting with dot) // Skip system folders (starting with dot)
if (folder.name.startsWith('.')) { if (folder.name.startsWith('.')) {
return false; return false;
} }
// Skip other users' folders // Skip other users' home folders (both naming conventions)
if (folder.name.startsWith('Mi Carpeta - ') && !folder.name.includes(username)) { if ((folder.name.startsWith('Mi Carpeta - ') || folder.name.startsWith('My Folder - '))
&& !folder.name.includes(username)) {
return false; return false;
} }
return true; return true;
}); });
// Find the user's home folder from filtered list // Find the user's home folder from filtered list (try both patterns)
let homeFolder = visibleFolders.find(folder => folder.name === homeFolderPattern); let homeFolder = visibleFolders.find(folder =>
folder.name === homeFolderPatternEn || folder.name === homeFolderPatternEs
);
if (homeFolder) { if (homeFolder) {
console.log(`Found user's home folder: ${homeFolder.name} (${homeFolder.id})`); console.log(`Found user's home folder: ${homeFolder.name} (${homeFolder.id})`);
@@ -1870,28 +1876,15 @@ async function findUserHomeFolder(username) {
loadFiles(); loadFiles();
return; // Success! Exit function return; // Success! Exit function
} else { } 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, // SECURITY: Never fall back to another user's folder.
// use the first folder as the user's home // If user's own folder doesn't exist, show root (empty state).
if (folderList.length > 0) { console.log('User home folder not found, showing root');
const fallbackFolder = folderList[0]; app.currentPath = '';
console.log(`Using first folder as fallback: ${fallbackFolder.name} (${fallbackFolder.id})`); ui.updateBreadcrumb('');
loadFiles();
app.userHomeFolderId = fallbackFolder.id; return;
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
}
} }
// If we get here, we've successfully processed the response // If we get here, we've successfully processed the response
+1 -1
View File
@@ -1,5 +1,5 @@
// OxiCloud Service Worker // OxiCloud Service Worker
const CACHE_NAME = 'oxicloud-cache-v4'; const CACHE_NAME = 'oxicloud-cache-v5';
const ASSETS_TO_CACHE = [ const ASSETS_TO_CACHE = [
'/', '/',
'/index.html', '/index.html',