From 0be7f1bdb5c9f174ce9634cc647bcfcedf9778a2 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Mon, 21 Jul 2025 09:14:07 -0400 Subject: [PATCH] add new option to FileManager component to anonymize filenames during upload --- .../Modules/Controls/FileManager.razor | 5 ++- Oqtane.Client/UI/Interop.cs | 6 ++-- Oqtane.Server/Controllers/FileController.cs | 36 ++++++++++--------- Oqtane.Server/wwwroot/js/interop.js | 10 ++++-- 4 files changed, 34 insertions(+), 23 deletions(-) diff --git a/Oqtane.Client/Modules/Controls/FileManager.razor b/Oqtane.Client/Modules/Controls/FileManager.razor index 1e5fa1c5..9581c58a 100644 --- a/Oqtane.Client/Modules/Controls/FileManager.razor +++ b/Oqtane.Client/Modules/Controls/FileManager.razor @@ -157,6 +157,9 @@ [Parameter] public bool UploadMultiple { get; set; } = false; // optional - enable multiple file uploads - default false + [Parameter] + public bool AnonymizeUploadFilenames { get; set; } = false; // optional - indicate if file names should be anonymized on upload - default false + [Parameter] public int ChunkSize { get; set; } = 1; // optional - size of file chunks to upload in MB @@ -408,7 +411,7 @@ } // upload files - var success = await interop.UploadFiles(posturl, folder, _guid, SiteState.AntiForgeryToken, jwt, chunksize, tokenSource.Token); + var success = await interop.UploadFiles(posturl, folder, _guid, SiteState.AntiForgeryToken, jwt, chunksize, AnonymizeUploadFilenames, tokenSource.Token); // reset progress indicators if (ShowProgress) diff --git a/Oqtane.Client/UI/Interop.cs b/Oqtane.Client/UI/Interop.cs index 8d547da7..3a56783f 100644 --- a/Oqtane.Client/UI/Interop.cs +++ b/Oqtane.Client/UI/Interop.cs @@ -224,17 +224,17 @@ namespace Oqtane.UI public Task UploadFiles(string posturl, string folder, string id, string antiforgerytoken, string jwt) { - UploadFiles(posturl, folder, id, antiforgerytoken, jwt, 1); + UploadFiles(posturl, folder, id, antiforgerytoken, jwt, 1, false); return Task.CompletedTask; } - public ValueTask UploadFiles(string posturl, string folder, string id, string antiforgerytoken, string jwt, int chunksize, CancellationToken cancellationToken = default) + public ValueTask UploadFiles(string posturl, string folder, string id, string antiforgerytoken, string jwt, int chunksize, bool anonymizeuploadfilenames, CancellationToken cancellationToken = default) { try { return _jsRuntime.InvokeAsync( "Oqtane.Interop.uploadFiles", cancellationToken, - posturl, folder, id, antiforgerytoken, jwt, chunksize); + posturl, folder, id, antiforgerytoken, jwt, chunksize, anonymizeuploadfilenames); } catch { diff --git a/Oqtane.Server/Controllers/FileController.cs b/Oqtane.Server/Controllers/FileController.cs index f0b72f22..3db7ecae 100644 --- a/Oqtane.Server/Controllers/FileController.cs +++ b/Oqtane.Server/Controllers/FileController.cs @@ -444,9 +444,14 @@ namespace Oqtane.Controllers } // ensure filename is valid - if (!formfile.FileName.IsPathOrFileValid() || !HasValidFileExtension(formfile.FileName)) + string fileName = formfile.FileName; + if (Path.GetExtension(fileName).Contains(':')) { - _logger.Log(LogLevel.Error, this, LogFunction.Security, "File Upload File Name Is Invalid Or Contains Invalid Extension {File}", formfile.FileName); + fileName = fileName.Substring(0, fileName.LastIndexOf(':')); // remove invalid suffix from extension + } + if (!fileName.IsPathOrFileValid() || !HasValidFileExtension(fileName)) + { + _logger.Log(LogLevel.Error, this, LogFunction.Security, "File Upload File Name Is Invalid Or Contains Invalid Extension {File}", fileName); return StatusCode((int)HttpStatusCode.Forbidden); } @@ -458,8 +463,8 @@ namespace Oqtane.Controllers return StatusCode((int)HttpStatusCode.Forbidden); } - // create file name using header values - string fileName = formfile.FileName + ".part_" + partCount.ToString("000") + "_" + totalParts.ToString("000"); + // create file name using header part values + fileName += ".part_" + partCount.ToString("000") + "_" + totalParts.ToString("000"); string folderPath = ""; try @@ -532,13 +537,13 @@ namespace Oqtane.Controllers string parts = Path.GetExtension(filename)?.Replace(token, ""); // returns "001_999" int totalparts = int.Parse(parts?.Substring(parts.IndexOf("_") + 1)); - filename = Path.GetFileNameWithoutExtension(filename); // base filename + filename = Path.GetFileNameWithoutExtension(filename); // base filename including original file extension string[] fileparts = Directory.GetFiles(folder, filename + token + "*"); // list of all file parts // if all of the file parts exist (note that file parts can arrive out of order) if (fileparts.Length == totalparts && CanAccessFiles(fileparts)) { - // merge file parts into temp file (in case another user is trying to get the file) + // merge file parts into temp file (in case another user is trying to read the file) bool success = true; using (var stream = new FileStream(Path.Combine(folder, filename + ".tmp"), FileMode.Create)) { @@ -559,25 +564,22 @@ namespace Oqtane.Controllers } // clean up file parts - foreach (var file in Directory.GetFiles(folder, "*" + token + "*")) + foreach (var file in fileparts) { - if (fileparts.Contains(file)) + try { - try - { - System.IO.File.Delete(file); - } - catch - { - // unable to delete part - ignore - } + System.IO.File.Delete(file); + } + catch + { + // unable to delete part - ignore } } // rename temp file if (success) { - // remove file if it already exists (as well as any thumbnails which may exist) + // remove existing file (as well as any thumbnails) foreach (var file in Directory.GetFiles(folder, Path.GetFileNameWithoutExtension(filename) + ".*")) { if (Path.GetExtension(file) != ".tmp") diff --git a/Oqtane.Server/wwwroot/js/interop.js b/Oqtane.Server/wwwroot/js/interop.js index 191d9823..fecc4c99 100644 --- a/Oqtane.Server/wwwroot/js/interop.js +++ b/Oqtane.Server/wwwroot/js/interop.js @@ -311,7 +311,7 @@ Oqtane.Interop = { } return files; }, - uploadFiles: async function (posturl, folder, id, antiforgerytoken, jwt, chunksize) { + uploadFiles: async function (posturl, folder, id, antiforgerytoken, jwt, chunksize, anonymizeuploadfilenames) { var success = true; var fileinput = document.getElementById('FileInput_' + id); var progressinfo = document.getElementById('ProgressInfo_' + id); @@ -344,16 +344,22 @@ Oqtane.Interop = { const totalParts = Math.ceil(file.size / chunkSize); let partCount = 0; + let filename = file.name; + if (anonymizeuploadfilenames) { + filename = crypto.randomUUID() + '.' + filename.split('.').pop(); + } + const uploadPart = () => { const start = partCount * chunkSize; const end = Math.min(start + chunkSize, file.size); const chunk = file.slice(start, end); return new Promise((resolve, reject) => { + let formdata = new FormData(); formdata.append('__RequestVerificationToken', antiforgerytoken); formdata.append('folder', folder); - formdata.append('formfile', chunk, file.name); + formdata.append('formfile', chunk, filename); var credentials = 'same-origin'; var headers = new Headers();