Skip to content

Commit d3c1443

Browse files
authored
Merge commit from fork
* Prevent path traveral vulnerability with upload of temporary files. * Used BadRequest instead of NotFound for invalid file name response.
1 parent d9fb6df commit d3c1443

File tree

5 files changed

+112
-12
lines changed

5 files changed

+112
-12
lines changed

src/Umbraco.Cms.Api.Management/Controllers/TemporaryFile/TemporaryFileControllerBase.cs

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using Microsoft.AspNetCore.Http;
1+
using Microsoft.AspNetCore.Http;
22
using Microsoft.AspNetCore.Mvc;
33
using Umbraco.Cms.Api.Common.Builders;
44
using Umbraco.Cms.Api.Management.Routing;
@@ -17,6 +17,9 @@ protected IActionResult TemporaryFileStatusResult(TemporaryFileOperationStatus o
1717
.WithTitle("File extension not allowed")
1818
.WithDetail("The file extension is not allowed.")
1919
.Build()),
20+
TemporaryFileOperationStatus.InvalidFileName => BadRequest(problemDetailsBuilder
21+
.WithTitle("The provided file name is not valid")
22+
.Build()),
2023
TemporaryFileOperationStatus.KeyAlreadyUsed => BadRequest(problemDetailsBuilder
2124
.WithTitle("Key already used")
2225
.WithDetail("The specified key is already used.")

src/Umbraco.Core/Services/OperationStatus/TemporaryFileUploadStatus.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@ public enum TemporaryFileOperationStatus
66
FileExtensionNotAllowed = 1,
77
KeyAlreadyUsed = 2,
88
NotFound = 3,
9-
UploadBlocked
9+
UploadBlocked = 4,
10+
InvalidFileName = 5,
1011
}

src/Umbraco.Core/Services/TemporaryFileService.cs

+19-9
Original file line numberDiff line numberDiff line change
@@ -45,21 +45,19 @@ public TemporaryFileService(
4545
return Attempt.FailWithStatus<TemporaryFileModel?, TemporaryFileOperationStatus>(TemporaryFileOperationStatus.KeyAlreadyUsed, null);
4646
}
4747

48-
4948
await using Stream dataStream = createModel.OpenReadStream();
5049
dataStream.Seek(0, SeekOrigin.Begin);
5150
if (_fileStreamSecurityValidator.IsConsideredSafe(dataStream) is false)
5251
{
5352
return Attempt.FailWithStatus<TemporaryFileModel?, TemporaryFileOperationStatus>(TemporaryFileOperationStatus.UploadBlocked, null);
5453
}
5554

56-
5755
temporaryFileModel = new TemporaryFileModel
5856
{
5957
Key = createModel.Key,
6058
FileName = createModel.FileName,
6159
OpenReadStream = createModel.OpenReadStream,
62-
AvailableUntil = DateTime.Now.Add(_runtimeSettings.TemporaryFileLifeTime)
60+
AvailableUntil = DateTime.Now.Add(_runtimeSettings.TemporaryFileLifeTime),
6361
};
6462

6563
await _temporaryFileRepository.SaveAsync(temporaryFileModel);
@@ -68,17 +66,29 @@ public TemporaryFileService(
6866
}
6967

7068
private TemporaryFileOperationStatus Validate(TemporaryFileModelBase temporaryFileModel)
71-
=> IsAllowedFileExtension(temporaryFileModel) == false
72-
? TemporaryFileOperationStatus.FileExtensionNotAllowed
73-
: TemporaryFileOperationStatus.Success;
74-
75-
private bool IsAllowedFileExtension(TemporaryFileModelBase temporaryFileModel)
7669
{
77-
var extension = Path.GetExtension(temporaryFileModel.FileName)[1..];
70+
if (IsAllowedFileExtension(temporaryFileModel.FileName) == false)
71+
{
72+
return TemporaryFileOperationStatus.FileExtensionNotAllowed;
73+
}
74+
75+
if (IsValidFileName(temporaryFileModel.FileName) == false)
76+
{
77+
return TemporaryFileOperationStatus.InvalidFileName;
78+
}
79+
80+
return TemporaryFileOperationStatus.Success;
81+
}
7882

83+
private bool IsAllowedFileExtension(string fileName)
84+
{
85+
var extension = Path.GetExtension(fileName)[1..];
7986
return _contentSettings.IsFileAllowedForUpload(extension);
8087
}
8188

89+
private static bool IsValidFileName(string fileName) =>
90+
!string.IsNullOrEmpty(fileName) && fileName.IndexOfAny(Path.GetInvalidFileNameChars()) < 0;
91+
8292
public async Task<Attempt<TemporaryFileModel?, TemporaryFileOperationStatus>> DeleteAsync(Guid key)
8393
{
8494
TemporaryFileModel? model = await _temporaryFileRepository.GetAsync(key);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
using NUnit.Framework;
3+
using Umbraco.Cms.Core.Configuration.Models;
4+
using Umbraco.Cms.Core.Models.TemporaryFile;
5+
using Umbraco.Cms.Core.Services;
6+
using Umbraco.Cms.Core.Services.OperationStatus;
7+
using Umbraco.Cms.Tests.Common.Testing;
8+
using Umbraco.Cms.Tests.Integration.Testing;
9+
10+
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Services;
11+
12+
[TestFixture]
13+
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerFixture)]
14+
public class TemporaryFileServiceTests : UmbracoIntegrationTest
15+
{
16+
private ITemporaryFileService TemporaryFileService => GetRequiredService<ITemporaryFileService>();
17+
18+
protected override void CustomTestSetup(IUmbracoBuilder builder) =>
19+
builder.Services.Configure<ContentSettings>(config =>
20+
config.AllowedUploadedFileExtensions = ["txt"]);
21+
22+
[Test]
23+
public async Task Can_Create_Get_And_Delete_Temporary_File()
24+
{
25+
var key = Guid.NewGuid();
26+
const string FileName = "test.txt";
27+
const string FileContents = "test";
28+
var model = new CreateTemporaryFileModel
29+
{
30+
FileName = FileName,
31+
Key = key,
32+
OpenReadStream = () =>
33+
{
34+
var stream = new MemoryStream();
35+
var writer = new StreamWriter(stream);
36+
writer.Write(FileContents);
37+
writer.Flush();
38+
stream.Position = 0;
39+
return stream;
40+
}
41+
};
42+
var createAttempt = await TemporaryFileService.CreateAsync(model);
43+
Assert.IsTrue(createAttempt.Success);
44+
45+
TemporaryFileModel? fileModel = await TemporaryFileService.GetAsync(key);
46+
Assert.IsNotNull(fileModel);
47+
Assert.AreEqual(key, fileModel.Key);
48+
Assert.AreEqual(FileName, fileModel.FileName);
49+
50+
using (var reader = new StreamReader(fileModel.OpenReadStream()))
51+
{
52+
string fileContents = reader.ReadToEnd();
53+
Assert.AreEqual(FileContents, fileContents);
54+
}
55+
56+
var deleteAttempt = await TemporaryFileService.DeleteAsync(key);
57+
Assert.IsTrue(createAttempt.Success);
58+
59+
fileModel = await TemporaryFileService.GetAsync(key);
60+
Assert.IsNull(fileModel);
61+
}
62+
63+
[Test]
64+
public async Task Cannot_Create_File_Outside_Of_Temporary_Files_Root()
65+
{
66+
var key = Guid.NewGuid();
67+
const string FileName = "../test.txt";
68+
var model = new CreateTemporaryFileModel
69+
{
70+
FileName = FileName,
71+
Key = key,
72+
OpenReadStream = () =>
73+
{
74+
var stream = new MemoryStream();
75+
var writer = new StreamWriter(stream);
76+
writer.Write(string.Empty);
77+
writer.Flush();
78+
stream.Position = 0;
79+
return stream;
80+
}
81+
};
82+
var createAttempt = await TemporaryFileService.CreateAsync(model);
83+
Assert.IsFalse(createAttempt.Success);
84+
Assert.AreEqual(TemporaryFileOperationStatus.InvalidFileName, createAttempt.Status);
85+
}
86+
}

version.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json",
3-
"version": "14.3.3",
3+
"version": "14.3.4",
44
"assemblyVersion": {
55
"precision": "build"
66
},

0 commit comments

Comments
 (0)