VYPR
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.

PackageAffected versionsPatched versions
Umbraco.CmsNuGet
>= 14.0.0--preview004, < 14.3.414.3.4
Umbraco.CmsNuGet
>= 15.0.0-rc1, < 15.3.115.3.1

Affected products

1

Patches

2
d3c1443b14b1

Merge commit from fork

https://github.com/umbraco/Umbraco-CMSAndy ButlandApr 8, 2025via ghsa
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"
       },
    
06a2a500b358

Merge commit from fork

https://github.com/umbraco/Umbraco-CMSAndy ButlandApr 8, 2025via ghsa
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

News mentions

0

No linked articles in our index yet.