High severityNVD Advisory· Published Apr 8, 2025· Updated Apr 9, 2025
Umbraco has a Management API Vulnerability to Path Traversal With Authenticated Users
CVE-2025-32017
Description
Umbraco is a free and open source .NET content management system. Authenticated users to the Umbraco backoffice are able to craft management API request that exploit a path traversal vulnerability to upload files into a incorrect location. The issue affects Umbraco 14+ and is patched in 14.3.4 and 15.3.1.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
Umbraco.CmsNuGet | >= 14.0.0--preview004, < 14.3.4 | 14.3.4 |
Umbraco.CmsNuGet | >= 15.0.0-rc1, < 15.3.1 | 15.3.1 |
Affected products
1- Range: >= 14.0.0--preview004, < 14.3.4
Patches
2d3c1443b14b1Merge commit from fork
5 files changed · +112 −12
src/Umbraco.Cms.Api.Management/Controllers/TemporaryFile/TemporaryFileControllerBase.cs+4 −1 modified@@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Common.Builders; using Umbraco.Cms.Api.Management.Routing; @@ -17,6 +17,9 @@ protected IActionResult TemporaryFileStatusResult(TemporaryFileOperationStatus o .WithTitle("File extension not allowed") .WithDetail("The file extension is not allowed.") .Build()), + TemporaryFileOperationStatus.InvalidFileName => BadRequest(problemDetailsBuilder + .WithTitle("The provided file name is not valid") + .Build()), TemporaryFileOperationStatus.KeyAlreadyUsed => BadRequest(problemDetailsBuilder .WithTitle("Key already used") .WithDetail("The specified key is already used.")
src/Umbraco.Core/Services/OperationStatus/TemporaryFileUploadStatus.cs+2 −1 modified@@ -6,5 +6,6 @@ public enum TemporaryFileOperationStatus FileExtensionNotAllowed = 1, KeyAlreadyUsed = 2, NotFound = 3, - UploadBlocked + UploadBlocked = 4, + InvalidFileName = 5, }
src/Umbraco.Core/Services/TemporaryFileService.cs+19 −9 modified@@ -45,21 +45,19 @@ public TemporaryFileService( return Attempt.FailWithStatus<TemporaryFileModel?, TemporaryFileOperationStatus>(TemporaryFileOperationStatus.KeyAlreadyUsed, null); } - await using Stream dataStream = createModel.OpenReadStream(); dataStream.Seek(0, SeekOrigin.Begin); if (_fileStreamSecurityValidator.IsConsideredSafe(dataStream) is false) { return Attempt.FailWithStatus<TemporaryFileModel?, TemporaryFileOperationStatus>(TemporaryFileOperationStatus.UploadBlocked, null); } - temporaryFileModel = new TemporaryFileModel { Key = createModel.Key, FileName = createModel.FileName, OpenReadStream = createModel.OpenReadStream, - AvailableUntil = DateTime.Now.Add(_runtimeSettings.TemporaryFileLifeTime) + AvailableUntil = DateTime.Now.Add(_runtimeSettings.TemporaryFileLifeTime), }; await _temporaryFileRepository.SaveAsync(temporaryFileModel); @@ -68,17 +66,29 @@ public TemporaryFileService( } private TemporaryFileOperationStatus Validate(TemporaryFileModelBase temporaryFileModel) - => IsAllowedFileExtension(temporaryFileModel) == false - ? TemporaryFileOperationStatus.FileExtensionNotAllowed - : TemporaryFileOperationStatus.Success; - - private bool IsAllowedFileExtension(TemporaryFileModelBase temporaryFileModel) { - var extension = Path.GetExtension(temporaryFileModel.FileName)[1..]; + if (IsAllowedFileExtension(temporaryFileModel.FileName) == false) + { + return TemporaryFileOperationStatus.FileExtensionNotAllowed; + } + + if (IsValidFileName(temporaryFileModel.FileName) == false) + { + return TemporaryFileOperationStatus.InvalidFileName; + } + + return TemporaryFileOperationStatus.Success; + } + private bool IsAllowedFileExtension(string fileName) + { + var extension = Path.GetExtension(fileName)[1..]; return _contentSettings.IsFileAllowedForUpload(extension); } + private static bool IsValidFileName(string fileName) => + !string.IsNullOrEmpty(fileName) && fileName.IndexOfAny(Path.GetInvalidFileNameChars()) < 0; + public async Task<Attempt<TemporaryFileModel?, TemporaryFileOperationStatus>> DeleteAsync(Guid key) { TemporaryFileModel? model = await _temporaryFileRepository.GetAsync(key);
tests/Umbraco.Tests.Integration/Umbraco.Core/Services/TemporaryFileServiceTests.cs+86 −0 added@@ -0,0 +1,86 @@ +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Models.TemporaryFile; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Services; + +[TestFixture] +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerFixture)] +public class TemporaryFileServiceTests : UmbracoIntegrationTest +{ + private ITemporaryFileService TemporaryFileService => GetRequiredService<ITemporaryFileService>(); + + protected override void CustomTestSetup(IUmbracoBuilder builder) => + builder.Services.Configure<ContentSettings>(config => + config.AllowedUploadedFileExtensions = ["txt"]); + + [Test] + public async Task Can_Create_Get_And_Delete_Temporary_File() + { + var key = Guid.NewGuid(); + const string FileName = "test.txt"; + const string FileContents = "test"; + var model = new CreateTemporaryFileModel + { + FileName = FileName, + Key = key, + OpenReadStream = () => + { + var stream = new MemoryStream(); + var writer = new StreamWriter(stream); + writer.Write(FileContents); + writer.Flush(); + stream.Position = 0; + return stream; + } + }; + var createAttempt = await TemporaryFileService.CreateAsync(model); + Assert.IsTrue(createAttempt.Success); + + TemporaryFileModel? fileModel = await TemporaryFileService.GetAsync(key); + Assert.IsNotNull(fileModel); + Assert.AreEqual(key, fileModel.Key); + Assert.AreEqual(FileName, fileModel.FileName); + + using (var reader = new StreamReader(fileModel.OpenReadStream())) + { + string fileContents = reader.ReadToEnd(); + Assert.AreEqual(FileContents, fileContents); + } + + var deleteAttempt = await TemporaryFileService.DeleteAsync(key); + Assert.IsTrue(createAttempt.Success); + + fileModel = await TemporaryFileService.GetAsync(key); + Assert.IsNull(fileModel); + } + + [Test] + public async Task Cannot_Create_File_Outside_Of_Temporary_Files_Root() + { + var key = Guid.NewGuid(); + const string FileName = "../test.txt"; + var model = new CreateTemporaryFileModel + { + FileName = FileName, + Key = key, + OpenReadStream = () => + { + var stream = new MemoryStream(); + var writer = new StreamWriter(stream); + writer.Write(string.Empty); + writer.Flush(); + stream.Position = 0; + return stream; + } + }; + var createAttempt = await TemporaryFileService.CreateAsync(model); + Assert.IsFalse(createAttempt.Success); + Assert.AreEqual(TemporaryFileOperationStatus.InvalidFileName, createAttempt.Status); + } +}
version.json+1 −1 modified@@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", - "version": "14.3.3", + "version": "14.3.4", "assemblyVersion": { "precision": "build" },
06a2a500b358Merge commit from fork
4 files changed · +116 −11
src/Umbraco.Cms.Api.Management/Controllers/TemporaryFile/TemporaryFileControllerBase.cs+4 −1 modified@@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Common.Builders; using Umbraco.Cms.Api.Management.Routing; @@ -17,6 +17,9 @@ protected IActionResult TemporaryFileStatusResult(TemporaryFileOperationStatus o .WithTitle("File extension not allowed") .WithDetail("The file extension is not allowed.") .Build()), + TemporaryFileOperationStatus.InvalidFileName => BadRequest(problemDetailsBuilder + .WithTitle("The provided file name is not valid") + .Build()), TemporaryFileOperationStatus.KeyAlreadyUsed => BadRequest(problemDetailsBuilder .WithTitle("Key already used") .WithDetail("The specified key is already used.")
src/Umbraco.Core/Services/OperationStatus/TemporaryFileUploadStatus.cs+2 −1 modified@@ -6,5 +6,6 @@ public enum TemporaryFileOperationStatus FileExtensionNotAllowed = 1, KeyAlreadyUsed = 2, NotFound = 3, - UploadBlocked + UploadBlocked = 4, + InvalidFileName = 5, }
src/Umbraco.Core/Services/TemporaryFileService.cs+19 −9 modified@@ -45,21 +45,19 @@ public TemporaryFileService( return Attempt.FailWithStatus<TemporaryFileModel?, TemporaryFileOperationStatus>(TemporaryFileOperationStatus.KeyAlreadyUsed, null); } - await using Stream dataStream = createModel.OpenReadStream(); dataStream.Seek(0, SeekOrigin.Begin); if (_fileStreamSecurityValidator.IsConsideredSafe(dataStream) is false) { return Attempt.FailWithStatus<TemporaryFileModel?, TemporaryFileOperationStatus>(TemporaryFileOperationStatus.UploadBlocked, null); } - temporaryFileModel = new TemporaryFileModel { Key = createModel.Key, FileName = createModel.FileName, OpenReadStream = createModel.OpenReadStream, - AvailableUntil = DateTime.Now.Add(_runtimeSettings.TemporaryFileLifeTime) + AvailableUntil = DateTime.Now.Add(_runtimeSettings.TemporaryFileLifeTime), }; await _temporaryFileRepository.SaveAsync(temporaryFileModel); @@ -68,17 +66,29 @@ public TemporaryFileService( } private TemporaryFileOperationStatus Validate(TemporaryFileModelBase temporaryFileModel) - => IsAllowedFileExtension(temporaryFileModel) == false - ? TemporaryFileOperationStatus.FileExtensionNotAllowed - : TemporaryFileOperationStatus.Success; - - private bool IsAllowedFileExtension(TemporaryFileModelBase temporaryFileModel) { - var extension = Path.GetExtension(temporaryFileModel.FileName)[1..]; + if (IsAllowedFileExtension(temporaryFileModel.FileName) == false) + { + return TemporaryFileOperationStatus.FileExtensionNotAllowed; + } + + if (IsValidFileName(temporaryFileModel.FileName) == false) + { + return TemporaryFileOperationStatus.InvalidFileName; + } + + return TemporaryFileOperationStatus.Success; + } + private bool IsAllowedFileExtension(string fileName) + { + var extension = Path.GetExtension(fileName)[1..]; return _contentSettings.IsFileAllowedForUpload(extension); } + private static bool IsValidFileName(string fileName) => + !string.IsNullOrEmpty(fileName) && fileName.IndexOfAny(Path.GetInvalidFileNameChars()) < 0; + public async Task<Attempt<TemporaryFileModel?, TemporaryFileOperationStatus>> DeleteAsync(Guid key) { TemporaryFileModel? model = await _temporaryFileRepository.GetAsync(key);
tests/Umbraco.Tests.Integration/Umbraco.Core/Services/TemporaryFileServiceTests.cs+91 −0 added@@ -0,0 +1,91 @@ +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Models.TemporaryFile; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Attributes; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Services; + +[TestFixture] +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerFixture)] +public class TemporaryFileServiceTests : UmbracoIntegrationTest +{ + private ITemporaryFileService TemporaryFileService => GetRequiredService<ITemporaryFileService>(); + + public static void ConfigureAllowedUploadedFileExtensions(IUmbracoBuilder builder) + { + builder.Services.Configure<ContentSettings>(config => + config.AllowedUploadedFileExtensions = ["txt"]); + } + + [Test] + [ConfigureBuilder(ActionName = nameof(ConfigureAllowedUploadedFileExtensions))] + public async Task Can_Create_Get_And_Delete_Temporary_File() + { + var key = Guid.NewGuid(); + const string FileName = "test.txt"; + const string FileContents = "test"; + var model = new CreateTemporaryFileModel + { + FileName = FileName, + Key = key, + OpenReadStream = () => + { + var stream = new MemoryStream(); + var writer = new StreamWriter(stream); + writer.Write(FileContents); + writer.Flush(); + stream.Position = 0; + return stream; + } + }; + var createAttempt = await TemporaryFileService.CreateAsync(model); + Assert.IsTrue(createAttempt.Success); + + TemporaryFileModel? fileModel = await TemporaryFileService.GetAsync(key); + Assert.IsNotNull(fileModel); + Assert.AreEqual(key, fileModel.Key); + Assert.AreEqual(FileName, fileModel.FileName); + + using (var reader = new StreamReader(fileModel.OpenReadStream())) + { + string fileContents = reader.ReadToEnd(); + Assert.AreEqual(FileContents, fileContents); + } + + var deleteAttempt = await TemporaryFileService.DeleteAsync(key); + Assert.IsTrue(createAttempt.Success); + + fileModel = await TemporaryFileService.GetAsync(key); + Assert.IsNull(fileModel); + } + + [Test] + [ConfigureBuilder(ActionName = nameof(ConfigureAllowedUploadedFileExtensions))] + public async Task Cannot_Create_File_Outside_Of_Temporary_Files_Root() + { + var key = Guid.NewGuid(); + const string FileName = "../test.txt"; + var model = new CreateTemporaryFileModel + { + FileName = FileName, + Key = key, + OpenReadStream = () => + { + var stream = new MemoryStream(); + var writer = new StreamWriter(stream); + writer.Write(string.Empty); + writer.Flush(); + stream.Position = 0; + return stream; + } + }; + var createAttempt = await TemporaryFileService.CreateAsync(model); + Assert.IsFalse(createAttempt.Success); + Assert.AreEqual(TemporaryFileOperationStatus.InvalidFileName, createAttempt.Status); + } +}
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-q62r-8ppj-xvf4ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-32017ghsaADVISORY
- github.com/umbraco/Umbraco-CMS/commit/06a2a500b358ce15b1e228391eb60bd517c6e833ghsax_refsource_MISCWEB
- github.com/umbraco/Umbraco-CMS/commit/d3c1443b14b1076faf13d1bcecc42860fdf5fad8ghsax_refsource_MISCWEB
- github.com/umbraco/Umbraco-CMS/security/advisories/GHSA-q62r-8ppj-xvf4ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.