From a4709426d982051d5336ee4c94b138b3ca23a94b Mon Sep 17 00:00:00 2001 From: Dionisio Date: Mon, 16 Feb 2026 17:58:50 +0100 Subject: [PATCH] fix(upload): sanitize multipart filename for folder uploads (#121) Browsers send the full relative path (e.g. 'Screenshots/file.png') as the multipart filename when uploading folders via webkitRelativePath. The File entity rejects names containing '/' or '\', causing all files in a folder upload to fail with 'Invalid file name'. Three fixes: - Backend: strip path components from multipart filename in file_handler, keeping only the basename. Also prevents path-traversal attacks. - Frontend (fileOperations.js): explicitly pass file.name as the third argument to FormData.append() in uploadFolderFiles() to override the browser's relative path. - Frontend (ui.js): detect folder drops in drag-and-drop handlers by checking webkitRelativePath, and route them to uploadFolderFiles() instead of uploadFiles() so subfolders are created first. Closes #121 --- Cargo.lock | 2 +- src/interfaces/api/handlers/file_handler.rs | 14 +++++++++++++- static/js/fileOperations.js | 4 +++- static/js/ui.js | 20 ++++++++++++++++++-- 4 files changed, 35 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 52b61d5b..11adb533 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1750,7 +1750,7 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "oxicloud" -version = "0.4.0" +version = "0.4.1" dependencies = [ "anyhow", "argon2", diff --git a/src/interfaces/api/handlers/file_handler.rs b/src/interfaces/api/handlers/file_handler.rs index 02e98bb3..50ca4884 100644 --- a/src/interfaces/api/handlers/file_handler.rs +++ b/src/interfaces/api/handlers/file_handler.rs @@ -64,7 +64,19 @@ impl FileHandler { } if name == "file" { - let filename = field.file_name().unwrap_or("unnamed").to_string(); + let raw_filename = field.file_name().unwrap_or("unnamed").to_string(); + // Browsers send the full relative path (e.g. "Screenshots/file.png") + // as the filename for folder uploads via webkitRelativePath. + // Strip path components to get the basename only. + // This also prevents path-traversal attacks. + let filename = raw_filename + .rsplit('/') + .next() + .unwrap_or(&raw_filename) + .rsplit('\\') + .next() + .unwrap_or(&raw_filename) + .to_string(); let content_type = field .content_type() .unwrap_or("application/octet-stream") diff --git a/static/js/fileOperations.js b/static/js/fileOperations.js index 189c82b7..b2aa6aac 100644 --- a/static/js/fileOperations.js +++ b/static/js/fileOperations.js @@ -279,7 +279,9 @@ const fileOps = { const formData = new FormData(); formData.append('folder_id', targetFolderId); - formData.append('file', file); + // Use file.name as the explicit filename to prevent the browser + // from sending the full webkitRelativePath as the filename + formData.append('file', file, file.name); const displayName = file.webkitRelativePath || file.name; diff --git a/static/js/ui.js b/static/js/ui.js index 487a53c7..0f1195d8 100644 --- a/static/js/ui.js +++ b/static/js/ui.js @@ -291,7 +291,15 @@ const ui = { e._oxiHandled = true; // Mark as handled for document-level fallback dropzone.classList.remove('active'); if (e.dataTransfer.files.length > 0) { - fileOps.uploadFiles(e.dataTransfer.files); + // Detect folder drops: files from folder drops have webkitRelativePath set + const hasRelativePaths = Array.from(e.dataTransfer.files).some( + f => f.webkitRelativePath && f.webkitRelativePath.includes('/') + ); + if (hasRelativePaths) { + fileOps.uploadFolderFiles(e.dataTransfer.files); + } else { + fileOps.uploadFiles(e.dataTransfer.files); + } } setTimeout(() => { dropzone.style.display = 'none'; @@ -327,7 +335,15 @@ const ui = { if (e._oxiHandled) return; if (e.dataTransfer.files.length > 0) { - fileOps.uploadFiles(e.dataTransfer.files); + // Detect folder drops: files from folder drops have webkitRelativePath set + const hasRelativePaths = Array.from(e.dataTransfer.files).some( + f => f.webkitRelativePath && f.webkitRelativePath.includes('/') + ); + if (hasRelativePaths) { + fileOps.uploadFolderFiles(e.dataTransfer.files); + } else { + fileOps.uploadFiles(e.dataTransfer.files); + } } setTimeout(() => {