From 1079025ec2c2d5106e65e216bb4431840c127d78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ana=C3=AFs=20Saraiva?= <53227802+Kisakay@users.noreply.github.com> Date: Tue, 25 Aug 2026 12:44:03 +0200 Subject: [PATCH] removed original name --- backend/src/routes/delete.rs | 2 +- backend/src/routes/download.rs | 4 +- backend/src/routes/upload.rs | 12 ++--- backend/src/security.rs | 60 ++++++++++++++-------- backend/src/storage.rs | 2 +- frontend/src/App.svelte | 3 +- frontend/src/components/FileList.svelte | 2 +- frontend/src/components/FileRow.svelte | 2 +- frontend/src/components/ResultPanel.svelte | 2 +- frontend/src/lib/api.js | 20 +++++++- frontend/src/lib/formats.js | 4 +- 11 files changed, 73 insertions(+), 40 deletions(-) diff --git a/backend/src/routes/delete.rs b/backend/src/routes/delete.rs index 786b583..8570e5f 100644 --- a/backend/src/routes/delete.rs +++ b/backend/src/routes/delete.rs @@ -37,6 +37,6 @@ pub async fn delete( } state.storage.delete(&id).await; - tracing::info!("deleted id={id} name={:?}", meta.original_name); + tracing::info!("deleted id={id} name={:?}", meta.download_name); json(&serde_json::json!({ "ok": true }), StatusCode::OK) } diff --git a/backend/src/routes/download.rs b/backend/src/routes/download.rs index 9e99e1e..5c162a8 100644 --- a/backend/src/routes/download.rs +++ b/backend/src/routes/download.rs @@ -43,7 +43,7 @@ pub async fn handle_download( .query() .map(|q| q.split('&').any(|p| p == "e=1")) .unwrap_or(false); - return render_password_form(&key, &meta.original_name, error); + return render_password_form(&key, &meta.download_name, error); } let file_path = state.storage.file_path(&key); @@ -65,7 +65,7 @@ pub async fn handle_download( } else { "inline" }; - let filename = security::download_file_name(&key, &meta.original_name); + let filename = security::renamed_file_name(&key, &meta.mime_type); let builder = Response::builder() .status(StatusCode::OK) diff --git a/backend/src/routes/upload.rs b/backend/src/routes/upload.rs index 536f65f..8cf9e39 100644 --- a/backend/src/routes/upload.rs +++ b/backend/src/routes/upload.rs @@ -48,7 +48,6 @@ pub async fn upload( let mut multipart = multer::Multipart::with_constraints(body.into_data_stream(), boundary, constraints); - let mut filename = String::new(); let mut mime = "application/octet-stream".to_string(); let mut size: u64 = 0; let mut has_file = false; @@ -68,7 +67,6 @@ pub async fn upload( if field.file_name().is_some() { if !has_file { has_file = true; - filename = field.file_name().unwrap_or("").to_string(); let ct = field .content_type() .map(|m| m.as_ref().to_string()) @@ -172,7 +170,8 @@ pub async fn upload( return json_error(StatusCode::BAD_REQUEST, "no file part in the upload"); } - let original_name = security::sanitize_original_name(&filename); + // The original client filename is discarded. The file's identity is the + // server-generated id plus a mime-derived extension. let password_hash = if !password.trim().is_empty() { security::hash_password(&password) } else { @@ -180,11 +179,11 @@ pub async fn upload( }; let now = chrono::Utc::now(); - let download_name = security::download_file_name(&id, &original_name); + let download_name = security::renamed_file_name(&id, &mime); let meta = FileMeta { id: id.clone(), - original_name: original_name.clone(), + download_name: download_name.clone(), size, mime_type: mime.clone(), download_key_hash: security::hash_secret(&id), @@ -206,14 +205,13 @@ pub async fn upload( None => format!("/{download_name}"), }; - tracing::info!("upload ok id={id} size={size} name={original_name:?}"); + tracing::info!("upload ok id={id} size={size} name={download_name:?}"); json( &serde_json::json!({ "id": id, "key": id, "url": url, - "originalName": original_name, "downloadName": download_name, "size": size, "mimeType": mime, diff --git a/backend/src/security.rs b/backend/src/security.rs index 139ec87..9ce0352 100644 --- a/backend/src/security.rs +++ b/backend/src/security.rs @@ -177,21 +177,6 @@ pub fn parse_auth_token(token: &str, secret: &str) -> Option { Some(key.to_string()) } -pub fn sanitize_original_name(name: &str) -> String { - let base = name.replace('\\', "/"); - let base = base.rsplit('/').next().unwrap_or(""); - let cleaned: String = base - .chars() - .map(|c| if (c as u32) < 0x20 || c == '\u{7f}' { '_' } else { c }) - .collect(); - let cleaned = cleaned.trim(); - if cleaned.is_empty() { - "file".to_string() - } else { - cleaned.to_string() - } -} - pub fn sanitize_mime(value: &str) -> String { let cleaned: String = value .chars() @@ -235,15 +220,48 @@ pub fn content_disposition(name: &str, disposition: &str) -> String { ) } -pub fn download_file_name(key: &str, original_name: &str) -> String { - match original_name.rfind('.') { - Some(dot) if dot > 0 && dot < original_name.len() - 1 => { - format!("{key}.{}", &original_name[dot + 1..]) - } - _ => key.to_string(), +/// Derive a short, safe file extension from a MIME type. Used to build the +/// public download name from the server-generated id, so the client-provided +/// original filename is never relied upon (or preserved). +pub fn extension_from_mime(mime_type: &str) -> &'static str { + let mime = mime_type.split(';').next().unwrap_or("").trim().to_ascii_lowercase(); + match mime.as_str() { + "image/jpeg" => "jpg", + "image/png" => "png", + "image/gif" => "gif", + "image/webp" => "webp", + "image/bmp" => "bmp", + "image/tiff" => "tiff", + "image/svg+xml" => "svg", + "video/mp4" => "mp4", + "video/webm" => "webm", + "video/quicktime" => "mov", + "video/x-msvideo" => "avi", + "audio/mpeg" => "mp3", + "audio/ogg" => "ogg", + "audio/wav" => "wav", + "audio/aac" => "aac", + "audio/flac" => "flac", + "application/pdf" => "pdf", + "application/zip" => "zip", + "application/gzip" => "gz", + "application/x-tar" => "tar", + "application/json" => "json", + "text/plain" => "txt", + "text/csv" => "csv", + "text/html" => "html", + _ => "", } } +/// Build the public/on-disk download name from the server-generated id plus a +/// mime-derived extension. This is the renamed identity of the file — the +/// original client filename is deliberately discarded. +pub fn renamed_file_name(key: &str, mime_type: &str) -> String { + let ext = extension_from_mime(mime_type); + if ext.is_empty() { key.to_string() } else { format!("{key}.{ext}") } +} + /// Whether a file should render inline in the browser rather than be forced as /// a download. Restricted to inert media types so the XSS protection is kept. pub fn should_inline(mime_type: &str) -> bool { diff --git a/backend/src/storage.rs b/backend/src/storage.rs index fd51095..2c8566d 100644 --- a/backend/src/storage.rs +++ b/backend/src/storage.rs @@ -10,7 +10,7 @@ use crate::security::is_valid_key; #[serde(rename_all = "camelCase")] pub struct FileMeta { pub id: String, - pub original_name: String, + pub download_name: String, pub size: u64, pub mime_type: String, pub download_key_hash: String, diff --git a/frontend/src/App.svelte b/frontend/src/App.svelte index 019549b..268fa8d 100644 --- a/frontend/src/App.svelte +++ b/frontend/src/App.svelte @@ -53,8 +53,7 @@ id: data.id, key: data.key, url: data.url, - originalName: data.originalName, - downloadName: data.downloadName || data.originalName, + downloadName: data.downloadName || '', mimeType: data.mimeType || '', size: data.size, createdAt: new Date().toISOString(), diff --git a/frontend/src/components/FileList.svelte b/frontend/src/components/FileList.svelte index c134eda..22cffde 100644 --- a/frontend/src/components/FileList.svelte +++ b/frontend/src/components/FileList.svelte @@ -8,7 +8,7 @@ async function onDelete(item) { const ok = window.confirm( - `Delete "${item.originalName || item.downloadName}" permanently? This cannot be undone.` + `Delete "${item.downloadName || 'file'}" permanently? This cannot be undone.` ); if (!ok) return; try { diff --git a/frontend/src/components/FileRow.svelte b/frontend/src/components/FileRow.svelte index 1749692..b4f7683 100644 --- a/frontend/src/components/FileRow.svelte +++ b/frontend/src/components/FileRow.svelte @@ -14,7 +14,7 @@ let copied = false; - $: name = item.originalName || item.downloadName || 'file'; + $: name = item.downloadName || 'file'; async function copy() { await copyText(downloadUrl(item)); diff --git a/frontend/src/components/ResultPanel.svelte b/frontend/src/components/ResultPanel.svelte index 40c6633..6a0b56f 100644 --- a/frontend/src/components/ResultPanel.svelte +++ b/frontend/src/components/ResultPanel.svelte @@ -8,7 +8,7 @@ let copied = false; $: url = result ? downloadUrl(result) : ''; - $: name = result ? result.originalName || result.downloadName || 'file' : ''; + $: name = result ? result.downloadName || 'file' : ''; async function copy() { await copyText(url); diff --git a/frontend/src/lib/api.js b/frontend/src/lib/api.js index 58dcc83..5726c78 100644 --- a/frontend/src/lib/api.js +++ b/frontend/src/lib/api.js @@ -1,9 +1,27 @@ +// Generate a short, unguessable lowercase-alphanumeric name (client side), +// keeping the original extension so the backend can derive a cosmetic name. +function randomRename(name) { + const charset = 'abcdefghijklmnopqrstuvwxyz0123456789'; + let id = ''; + const bytes = crypto.getRandomValues(new Uint8Array(8)); + for (let i = 0; i < 8; i += 1) id += charset[bytes[i] % charset.length]; + const dot = name.lastIndexOf('.'); + const ext = dot > 0 && dot < name.length - 1 ? name.slice(dot) : ''; + return `${id}${ext}`; +} + export function uploadFile(file, password, onProgress) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); const form = new FormData(); + + // Rename the file client side before uploading: guard against the + // original name leaking and collisions. The backend re-asserts its own + // random id as the authoritative stored name regardless. + const renamed = new File([file], randomRename(file.name), { type: file.type }); + if (password) form.append('password', password); - form.append('file', file); + form.append('file', renamed); xhr.upload.addEventListener('progress', (e) => { if (e.lengthComputable && onProgress) onProgress(e.loaded, e.total); diff --git a/frontend/src/lib/formats.js b/frontend/src/lib/formats.js index 0a8645b..f1773d9 100644 --- a/frontend/src/lib/formats.js +++ b/frontend/src/lib/formats.js @@ -9,7 +9,7 @@ export function forcedDownloadUrl(item) { } export function displayName(item) { - return item.downloadName || item.originalName || 'file'; + return item.downloadName || 'file'; } export function fileExtension(item) { @@ -20,7 +20,7 @@ export function fileExtension(item) { } export function linkLabel(item) { - return item.originalName || item.downloadName || 'file'; + return item.downloadName || 'file'; } export function buildLinkFormats(item) {