Skip to content

Commit 7888b9a

Browse files
Merge commit from fork
* Bumped version to 10.8.9. * Fixed parsing of node if in content and media permission querystring handlers to retrieve expected value when multiple are provided in the querystring. # Conflicts: # tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Authorization/ContentPermissionsQueryStringHandlerTests.cs # tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Authorization/MediaPermissionsQueryStringHandlerTests.cs * Add HttpPost attributes to backoffice endpoints that should only accept post requests. * Narrow PermissionQueryString parsing to the releveant UmbracoObjectType * Add missed update from v10 --------- Co-authored-by: Sven Geusens <[email protected]>
1 parent e31582b commit 7888b9a

9 files changed

+110
-33
lines changed

src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsQueryStringHandler.cs

+7-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ public class
1919
{
2020
private readonly ContentPermissions _contentPermissions;
2121

22+
protected override UmbracoObjectTypes KeyParsingFilterType => UmbracoObjectTypes.Document;
23+
2224
/// <summary>
2325
/// Initializes a new instance of the <see cref="ContentPermissionsQueryStringHandler" /> class.
2426
/// </summary>
@@ -47,7 +49,11 @@ protected override Task<bool> IsAuthorized(AuthorizationHandlerContext context,
4749
return Task.FromResult(true);
4850
}
4951

50-
var argument = routeVal.ToString();
52+
// Handle case where the incoming querystring could contain more than one value (e.g. ?id=1000&id=1001).
53+
// It's the first one that'll be processed by the protected method so we should verify that.
54+
var argument = routeVal.Count == 1
55+
? routeVal.ToString()
56+
: routeVal.FirstOrDefault()?.ToString() ?? string.Empty;
5157

5258
if (!TryParseNodeId(argument, out nodeId))
5359
{

src/Umbraco.Web.BackOffice/Authorization/MediaPermissionsQueryStringHandler.cs

+7-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ public class MediaPermissionsQueryStringHandler : PermissionsQueryStringHandler<
1818
{
1919
private readonly MediaPermissions _mediaPermissions;
2020

21+
protected override UmbracoObjectTypes KeyParsingFilterType => UmbracoObjectTypes.Media;
22+
2123
/// <summary>
2224
/// Initializes a new instance of the <see cref="MediaPermissionsQueryStringHandler" /> class.
2325
/// </summary>
@@ -44,7 +46,11 @@ protected override Task<bool> IsAuthorized(AuthorizationHandlerContext context,
4446
return Task.FromResult(true);
4547
}
4648

47-
var argument = routeVal.ToString();
49+
// Handle case where the incoming querystring could contain more than one value (e.g. ?id=1000&id=1001).
50+
// It's the first one that'll be processed by the protected method so we should verify that.
51+
var argument = routeVal.Count == 1
52+
? routeVal.ToString()
53+
: routeVal.FirstOrDefault()?.ToString() ?? string.Empty;
4854

4955
if (!TryParseNodeId(argument, out var nodeId))
5056
{

src/Umbraco.Web.BackOffice/Authorization/PermissionsQueryStringHandler.cs

+8-2
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,18 @@ public PermissionsQueryStringHandler(
4949
/// </summary>
5050
protected IEntityService EntityService { get; set; }
5151

52+
/// <summary>
53+
/// Defaults to Unknown so all types are allowed, since Keys are unique across all node types this works,
54+
/// but it if you are certain you are looking for a specific type this should be overwritten for DB query performance.
55+
/// </summary>
56+
protected virtual UmbracoObjectTypes KeyParsingFilterType => UmbracoObjectTypes.Unknown;
57+
5258
/// <summary>
5359
/// Attempts to parse a node ID from a string representation found in a querystring value.
5460
/// </summary>
5561
/// <param name="argument">Querystring value.</param>
5662
/// <param name="nodeId">Output parsed Id.</param>
57-
/// <returns>True of node ID could be parased, false it not.</returns>
63+
/// <returns>True of node ID could be parsed, false it not.</returns>
5864
protected bool TryParseNodeId(string argument, out int nodeId)
5965
{
6066
// If the argument is an int, it will parse and can be assigned to nodeId.
@@ -75,7 +81,7 @@ protected bool TryParseNodeId(string argument, out int nodeId)
7581

7682
if (Guid.TryParse(argument, out Guid key))
7783
{
78-
nodeId = EntityService.GetId(key, UmbracoObjectTypes.Document).Result;
84+
nodeId = EntityService.GetId(key, KeyParsingFilterType).Result;
7985
return true;
8086
}
8187

src/Umbraco.Web.BackOffice/Controllers/ContentController.cs

+10
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ public IEnumerable<ContentItemDisplay> GetByIds([FromQuery] int[] ids)
196196
/// Permission check is done for letter 'R' which is for <see cref="ActionRights" /> which the user must have access to
197197
/// update
198198
/// </remarks>
199+
[HttpPost]
199200
public async Task<ActionResult<IEnumerable<AssignedUserGroupPermissions?>?>> PostSaveUserGroupPermissions(
200201
UserGroupPermissionsSave saveModel)
201202
{
@@ -842,6 +843,7 @@ private bool EnsureUniqueName(string? name, IContent? content, string modelName)
842843
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)]
843844
[FileUploadCleanupFilter]
844845
[ContentSaveValidation(skipUserAccessValidation:true)] // skip user access validation because we "only" require Settings access to create new blueprints from scratch
846+
[HttpPost]
845847
public async Task<ActionResult<ContentItemDisplay<ContentVariantDisplay>?>?> PostSaveBlueprint(
846848
[ModelBinder(typeof(BlueprintItemBinder))] ContentItemSave contentItem)
847849
{
@@ -879,6 +881,7 @@ private bool EnsureUniqueName(string? name, IContent? content, string modelName)
879881
[FileUploadCleanupFilter]
880882
[ContentSaveValidation]
881883
[OutgoingEditorModelEvent]
884+
[HttpPost]
882885
public async Task<ActionResult<ContentItemDisplay<ContentVariantScheduleDisplay>?>> PostSave(
883886
[ModelBinder(typeof(ContentItemBinder))] ContentItemSave contentItem)
884887
{
@@ -1960,6 +1963,7 @@ private string GetVariantName(string? culture, string? segment)
19601963
/// does not have Publish access to this node.
19611964
/// </remarks>
19621965
[Authorize(Policy = AuthorizationPolicies.ContentPermissionPublishById)]
1966+
[HttpPost]
19631967
public IActionResult PostPublishById(int id)
19641968
{
19651969
IContent? foundContent = GetObjectFromRequest(() => _contentService.GetById(id));
@@ -1991,6 +1995,7 @@ public IActionResult PostPublishById(int id)
19911995
/// does not have Publish access to this node.
19921996
/// </remarks>
19931997
[Authorize(Policy = AuthorizationPolicies.ContentPermissionPublishById)]
1998+
[HttpPost]
19941999
public IActionResult PostPublishByIdAndCulture(PublishContent model)
19952000
{
19962001
var languageCount = _allLangs.Value.Count();
@@ -2114,6 +2119,7 @@ public IActionResult EmptyRecycleBin()
21142119
/// </summary>
21152120
/// <param name="sorted"></param>
21162121
/// <returns></returns>
2122+
[HttpPost]
21172123
public async Task<IActionResult> PostSort(ContentSortOrder sorted)
21182124
{
21192125
if (sorted == null)
@@ -2165,6 +2171,7 @@ public async Task<IActionResult> PostSort(ContentSortOrder sorted)
21652171
/// </summary>
21662172
/// <param name="move"></param>
21672173
/// <returns></returns>
2174+
[HttpPost]
21682175
public async Task<IActionResult?> PostMove(MoveOrCopy move)
21692176
{
21702177
// Authorize...
@@ -2199,6 +2206,7 @@ public async Task<IActionResult> PostSort(ContentSortOrder sorted)
21992206
/// </summary>
22002207
/// <param name="copy"></param>
22012208
/// <returns></returns>
2209+
[HttpPost]
22022210
public async Task<ActionResult<IContent>?> PostCopy(MoveOrCopy copy)
22032211
{
22042212
// Authorize...
@@ -2238,6 +2246,7 @@ public async Task<IActionResult> PostSort(ContentSortOrder sorted)
22382246
/// <param name="model">The content and variants to unpublish</param>
22392247
/// <returns></returns>
22402248
[OutgoingEditorModelEvent]
2249+
[HttpPost]
22412250
public async Task<ActionResult<ContentItemDisplayWithSchedule?>> PostUnpublish(UnpublishContent model)
22422251
{
22432252
IContent? foundContent = _contentService.GetById(model.Id);
@@ -2960,6 +2969,7 @@ public ActionResult<IEnumerable<NotifySetting>> GetNotificationOptions(int conte
29602969
return notifications;
29612970
}
29622971

2972+
[HttpPost]
29632973
public IActionResult PostNotificationOptions(
29642974
int contentId,
29652975
[FromQuery(Name = "notifyOptions[]")] string[] notifyOptions)

src/Umbraco.Web.BackOffice/Controllers/MediaController.cs

+5
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,7 @@ public IActionResult DeleteById(int id)
385385
/// </summary>
386386
/// <param name="move"></param>
387387
/// <returns></returns>
388+
[HttpPost]
388389
public async Task<IActionResult> PostMove(MoveOrCopy move)
389390
{
390391
// Authorize...
@@ -432,6 +433,7 @@ public async Task<IActionResult> PostMove(MoveOrCopy move)
432433
[FileUploadCleanupFilter]
433434
[MediaItemSaveValidation]
434435
[OutgoingEditorModelEvent]
436+
[HttpPost]
435437
public ActionResult<MediaItemDisplay?>? PostSave(
436438
[ModelBinder(typeof(MediaItemBinder))] MediaItemSave contentItem)
437439
{
@@ -547,6 +549,7 @@ public IActionResult EmptyRecycleBin()
547549
/// </summary>
548550
/// <param name="sorted"></param>
549551
/// <returns></returns>
552+
[HttpPost]
550553
public async Task<IActionResult> PostSort(ContentSortOrder sorted)
551554
{
552555
if (sorted == null)
@@ -592,6 +595,7 @@ public async Task<IActionResult> PostSort(ContentSortOrder sorted)
592595
}
593596
}
594597

598+
[HttpPost]
595599
public async Task<ActionResult<MediaItemDisplay?>> PostAddFolder(PostedFolder folder)
596600
{
597601
ActionResult<int?>? parentIdResult = await GetParentIdAsIntAsync(folder.ParentId, true);
@@ -625,6 +629,7 @@ public async Task<IActionResult> PostSort(ContentSortOrder sorted)
625629
/// <remarks>
626630
/// We cannot validate this request with attributes (nicely) due to the nature of the multi-part for data.
627631
/// </remarks>
632+
[HttpPost]
628633
public async Task<IActionResult> PostAddFile([FromForm] string path, [FromForm] string currentFolder,
629634
[FromForm] string contentTypeAlias, List<IFormFile> file)
630635
{

tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Authorization/ContentPermissionsQueryStringHandlerTests.cs

+32-14
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@
22
// See LICENSE for more details.
33

44
using System;
5-
using System.Collections.Generic;
65
using System.Security.Claims;
7-
using System.Threading.Tasks;
86
using Microsoft.AspNetCore.Authorization;
97
using Microsoft.AspNetCore.Http;
108
using Microsoft.Extensions.Primitives;
@@ -35,7 +33,7 @@ public class ContentPermissionsQueryStringHandlerTests
3533
public async Task Node_Id_From_Requirement_With_Permission_Is_Authorized()
3634
{
3735
var authHandlerContext = CreateAuthorizationHandlerContext(NodeId);
38-
var mockHttpContextAccessor = CreateMockHttpContextAccessor();
36+
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue();
3937
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "A" });
4038

4139
await sut.HandleAsync(authHandlerContext);
@@ -47,7 +45,7 @@ public async Task Node_Id_From_Requirement_With_Permission_Is_Authorized()
4745
public async Task Node_Id_From_Requirement_Without_Permission_Is_Not_Authorized()
4846
{
4947
var authHandlerContext = CreateAuthorizationHandlerContext(NodeId);
50-
var mockHttpContextAccessor = CreateMockHttpContextAccessor();
48+
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue();
5149
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "B" });
5250

5351
await sut.HandleAsync(authHandlerContext);
@@ -60,7 +58,7 @@ public async Task Node_Id_From_Requirement_Without_Permission_Is_Not_Authorized(
6058
public async Task Node_Id_Missing_From_Requirement_And_QueryString_Is_Authorized()
6159
{
6260
var authHandlerContext = CreateAuthorizationHandlerContext();
63-
var mockHttpContextAccessor = CreateMockHttpContextAccessor("xxx");
61+
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue("xxx");
6462
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "A" });
6563

6664
await sut.HandleAsync(authHandlerContext);
@@ -72,7 +70,7 @@ public async Task Node_Id_Missing_From_Requirement_And_QueryString_Is_Authorized
7270
public async Task Node_Integer_Id_From_QueryString_With_Permission_Is_Authorized()
7371
{
7472
var authHandlerContext = CreateAuthorizationHandlerContext();
75-
var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: NodeId.ToString());
73+
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: NodeId.ToString());
7674
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "A" });
7775

7876
await sut.HandleAsync(authHandlerContext);
@@ -85,7 +83,21 @@ public async Task Node_Integer_Id_From_QueryString_With_Permission_Is_Authorized
8583
public async Task Node_Integer_Id_From_QueryString_Without_Permission_Is_Not_Authorized()
8684
{
8785
var authHandlerContext = CreateAuthorizationHandlerContext();
88-
var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: NodeId.ToString());
86+
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: NodeId.ToString());
87+
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "B" });
88+
89+
await sut.HandleAsync(authHandlerContext);
90+
91+
Assert.IsFalse(authHandlerContext.HasSucceeded);
92+
AssertContentCached(mockHttpContextAccessor);
93+
}
94+
95+
[Test]
96+
public async Task Node_Integer_Id_From_QueryString_Without_Permission_Is_Not_Authorized_Even_When_Additional_Parameter_For_Id_With_Permission_Is_Provided()
97+
{
98+
// Provides initially failing test and verifies fix for advisory https://github.com/umbraco/Umbraco-CMS/security/advisories/GHSA-wx5h-wqfq-v698
99+
var authHandlerContext = CreateAuthorizationHandlerContext();
100+
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValues(queryStringValues: new[] { NodeId.ToString(), 1001.ToString() });
89101
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "B" });
90102

91103
await sut.HandleAsync(authHandlerContext);
@@ -98,7 +110,7 @@ public async Task Node_Integer_Id_From_QueryString_Without_Permission_Is_Not_Aut
98110
public async Task Node_Udi_Id_From_QueryString_With_Permission_Is_Authorized()
99111
{
100112
var authHandlerContext = CreateAuthorizationHandlerContext();
101-
var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: s_nodeUdi.ToString());
113+
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: s_nodeUdi.ToString());
102114
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "A" });
103115

104116
await sut.HandleAsync(authHandlerContext);
@@ -111,7 +123,7 @@ public async Task Node_Udi_Id_From_QueryString_With_Permission_Is_Authorized()
111123
public async Task Node_Udi_Id_From_QueryString_Without_Permission_Is_Not_Authorized()
112124
{
113125
var authHandlerContext = CreateAuthorizationHandlerContext();
114-
var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: s_nodeUdi.ToString());
126+
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: s_nodeUdi.ToString());
115127
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "B" });
116128

117129
await sut.HandleAsync(authHandlerContext);
@@ -124,7 +136,7 @@ public async Task Node_Udi_Id_From_QueryString_Without_Permission_Is_Not_Authori
124136
public async Task Node_Guid_Id_From_QueryString_With_Permission_Is_Authorized()
125137
{
126138
var authHandlerContext = CreateAuthorizationHandlerContext();
127-
var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: s_nodeGuid.ToString());
139+
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: s_nodeGuid.ToString());
128140
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "A" });
129141

130142
await sut.HandleAsync(authHandlerContext);
@@ -137,7 +149,7 @@ public async Task Node_Guid_Id_From_QueryString_With_Permission_Is_Authorized()
137149
public async Task Node_Guid_Id_From_QueryString_Without_Permission_Is_Not_Authorized()
138150
{
139151
var authHandlerContext = CreateAuthorizationHandlerContext();
140-
var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: s_nodeGuid.ToString());
152+
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: s_nodeGuid.ToString());
141153
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "B" });
142154

143155
await sut.HandleAsync(authHandlerContext);
@@ -150,7 +162,7 @@ public async Task Node_Guid_Id_From_QueryString_Without_Permission_Is_Not_Author
150162
public async Task Node_Invalid_Id_From_QueryString_Is_Authorized()
151163
{
152164
var authHandlerContext = CreateAuthorizationHandlerContext();
153-
var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: "invalid");
165+
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: "invalid");
154166
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "A" });
155167

156168
await sut.HandleAsync(authHandlerContext);
@@ -169,14 +181,20 @@ private static AuthorizationHandlerContext CreateAuthorizationHandlerContext(int
169181
return new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement }, user, resource);
170182
}
171183

172-
private static Mock<IHttpContextAccessor> CreateMockHttpContextAccessor(
184+
private static Mock<IHttpContextAccessor> CreateMockHttpContextAccessorWithQueryStringValue(
173185
string queryStringName = QueryStringName,
174186
string queryStringValue = "")
187+
=> CreateMockHttpContextAccessorWithQueryStringValues(queryStringName, new[] { queryStringValue });
188+
189+
private static Mock<IHttpContextAccessor> CreateMockHttpContextAccessorWithQueryStringValues(
190+
string queryStringName = QueryStringName,
191+
string[]? queryStringValues = null)
175192
{
193+
queryStringValues ??= Array.Empty<string>();
176194
var mockHttpContextAccessor = new Mock<IHttpContextAccessor>();
177195
var mockHttpContext = new Mock<HttpContext>();
178196
var mockHttpRequest = new Mock<HttpRequest>();
179-
var queryParams = new Dictionary<string, StringValues> { { queryStringName, queryStringValue } };
197+
var queryParams = new Dictionary<string, StringValues> { { queryStringName, new StringValues(queryStringValues) } };
180198
mockHttpRequest.SetupGet(x => x.Query).Returns(new QueryCollection(queryParams));
181199
mockHttpContext.SetupGet(x => x.Request).Returns(mockHttpRequest.Object);
182200
mockHttpContext.SetupGet(x => x.Items).Returns(new Dictionary<object, object>());

0 commit comments

Comments
 (0)