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
This commit is contained in:
Generated
+1
-1
@@ -1750,7 +1750,7 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "oxicloud"
|
name = "oxicloud"
|
||||||
version = "0.4.0"
|
version = "0.4.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"argon2",
|
"argon2",
|
||||||
|
|||||||
@@ -64,7 +64,19 @@ impl FileHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if name == "file" {
|
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
|
let content_type = field
|
||||||
.content_type()
|
.content_type()
|
||||||
.unwrap_or("application/octet-stream")
|
.unwrap_or("application/octet-stream")
|
||||||
|
|||||||
@@ -279,7 +279,9 @@ const fileOps = {
|
|||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('folder_id', targetFolderId);
|
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;
|
const displayName = file.webkitRelativePath || file.name;
|
||||||
|
|
||||||
|
|||||||
+18
-2
@@ -291,7 +291,15 @@ const ui = {
|
|||||||
e._oxiHandled = true; // Mark as handled for document-level fallback
|
e._oxiHandled = true; // Mark as handled for document-level fallback
|
||||||
dropzone.classList.remove('active');
|
dropzone.classList.remove('active');
|
||||||
if (e.dataTransfer.files.length > 0) {
|
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(() => {
|
setTimeout(() => {
|
||||||
dropzone.style.display = 'none';
|
dropzone.style.display = 'none';
|
||||||
@@ -327,7 +335,15 @@ const ui = {
|
|||||||
if (e._oxiHandled) return;
|
if (e._oxiHandled) return;
|
||||||
|
|
||||||
if (e.dataTransfer.files.length > 0) {
|
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(() => {
|
setTimeout(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user