Skip to content

Commit 5b54bed

Browse files
Merge commit from fork
* Fixed parsing of node if in content and media permission querystring handlers to retrieve expected value when multiple are provided in the querystring. * Add HttpPost attributes to backoffice endpoints that should only accept post requests. * Bumped version to 13.6.1. * Narrow PermissionQueryString parsing to the releveant UmbracoObjectType * Add missed update from v10 --------- Co-authored-by: Sven Geusens <[email protected]>
1 parent 2ec6ff4 commit 5b54bed

8 files changed

+102
-32
lines changed

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

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

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

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

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

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
@@ -256,6 +256,7 @@ public IEnumerable<ContentItemDisplay> GetByIds([FromQuery] int[] ids)
256256
/// Permission check is done for letter 'R' which is for <see cref="ActionRights" /> which the user must have access to
257257
/// update
258258
/// </remarks>
259+
[HttpPost]
259260
public async Task<ActionResult<IEnumerable<AssignedUserGroupPermissions?>?>> PostSaveUserGroupPermissions(
260261
UserGroupPermissionsSave saveModel)
261262
{
@@ -902,6 +903,7 @@ private bool EnsureUniqueName(string? name, IContent? content, string modelName)
902903
[Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)]
903904
[FileUploadCleanupFilter]
904905
[ContentSaveValidation(skipUserAccessValidation:true)] // skip user access validation because we "only" require Settings access to create new blueprints from scratch
906+
[HttpPost]
905907
public async Task<ActionResult<ContentItemDisplay<ContentVariantDisplay>?>?> PostSaveBlueprint(
906908
[ModelBinder(typeof(BlueprintItemBinder))] ContentItemSave contentItem)
907909
{
@@ -939,6 +941,7 @@ private bool EnsureUniqueName(string? name, IContent? content, string modelName)
939941
[FileUploadCleanupFilter]
940942
[ContentSaveValidation]
941943
[OutgoingEditorModelEvent]
944+
[HttpPost]
942945
public async Task<ActionResult<ContentItemDisplay<ContentVariantScheduleDisplay>?>> PostSave(
943946
[ModelBinder(typeof(ContentItemBinder))] ContentItemSave contentItem)
944947
{
@@ -2089,6 +2092,7 @@ private string GetVariantName(string? culture, string? segment)
20892092
/// does not have Publish access to this node.
20902093
/// </remarks>
20912094
[Authorize(Policy = AuthorizationPolicies.ContentPermissionPublishById)]
2095+
[HttpPost]
20922096
public IActionResult PostPublishById(int id)
20932097
{
20942098
IContent? foundContent = GetObjectFromRequest(() => _contentService.GetById(id));
@@ -2120,6 +2124,7 @@ public IActionResult PostPublishById(int id)
21202124
/// does not have Publish access to this node.
21212125
/// </remarks>
21222126
[Authorize(Policy = AuthorizationPolicies.ContentPermissionPublishById)]
2127+
[HttpPost]
21232128
public IActionResult PostPublishByIdAndCulture(PublishContent model)
21242129
{
21252130
var languageCount = _allLangs.Value.Count();
@@ -2243,6 +2248,7 @@ public IActionResult EmptyRecycleBin()
22432248
/// </summary>
22442249
/// <param name="sorted"></param>
22452250
/// <returns></returns>
2251+
[HttpPost]
22462252
public async Task<IActionResult> PostSort(ContentSortOrder sorted)
22472253
{
22482254
if (sorted == null)
@@ -2294,6 +2300,7 @@ public async Task<IActionResult> PostSort(ContentSortOrder sorted)
22942300
/// </summary>
22952301
/// <param name="move"></param>
22962302
/// <returns></returns>
2303+
[HttpPost]
22972304
public async Task<IActionResult?> PostMove(MoveOrCopy move)
22982305
{
22992306
// Authorize...
@@ -2333,6 +2340,7 @@ public async Task<IActionResult> PostSort(ContentSortOrder sorted)
23332340
/// </summary>
23342341
/// <param name="copy"></param>
23352342
/// <returns></returns>
2343+
[HttpPost]
23362344
public async Task<ActionResult<IContent>?> PostCopy(MoveOrCopy copy)
23372345
{
23382346
// Authorize...
@@ -2372,6 +2380,7 @@ public async Task<IActionResult> PostSort(ContentSortOrder sorted)
23722380
/// <param name="model">The content and variants to unpublish</param>
23732381
/// <returns></returns>
23742382
[OutgoingEditorModelEvent]
2383+
[HttpPost]
23752384
public async Task<ActionResult<ContentItemDisplayWithSchedule?>> PostUnpublish(UnpublishContent model)
23762385
{
23772386
IContent? foundContent = _contentService.GetById(model.Id);
@@ -3096,6 +3105,7 @@ public ActionResult<IEnumerable<NotifySetting>> GetNotificationOptions(int conte
30963105
return notifications;
30973106
}
30983107

3108+
[HttpPost]
30993109
public IActionResult PostNotificationOptions(
31003110
int contentId,
31013111
[FromQuery(Name = "notifyOptions[]")] string[] notifyOptions)

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

+5
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,7 @@ public IActionResult DeleteById(int id)
386386
/// </summary>
387387
/// <param name="move"></param>
388388
/// <returns></returns>
389+
[HttpPost]
389390
public async Task<IActionResult> PostMove(MoveOrCopy move)
390391
{
391392
// Authorize...
@@ -436,6 +437,7 @@ public async Task<IActionResult> PostMove(MoveOrCopy move)
436437
[FileUploadCleanupFilter]
437438
[MediaItemSaveValidation]
438439
[OutgoingEditorModelEvent]
440+
[HttpPost]
439441
public ActionResult<MediaItemDisplay?>? PostSave(
440442
[ModelBinder(typeof(MediaItemBinder))] MediaItemSave contentItem)
441443
{
@@ -551,6 +553,7 @@ public IActionResult EmptyRecycleBin()
551553
/// </summary>
552554
/// <param name="sorted"></param>
553555
/// <returns></returns>
556+
[HttpPost]
554557
public async Task<IActionResult> PostSort(ContentSortOrder sorted)
555558
{
556559
if (sorted == null)
@@ -595,6 +598,7 @@ public async Task<IActionResult> PostSort(ContentSortOrder sorted)
595598
}
596599
}
597600

601+
[HttpPost]
598602
public async Task<ActionResult<MediaItemDisplay?>> PostAddFolder(PostedFolder folder)
599603
{
600604
ActionResult<int?>? parentIdResult = await GetParentIdAsIntAsync(folder.ParentId, true);
@@ -628,6 +632,7 @@ public async Task<IActionResult> PostSort(ContentSortOrder sorted)
628632
/// <remarks>
629633
/// We cannot validate this request with attributes (nicely) due to the nature of the multi-part for data.
630634
/// </remarks>
635+
[HttpPost]
631636
public async Task<IActionResult> PostAddFile([FromForm] string path, [FromForm] string currentFolder,
632637
[FromForm] string contentTypeAlias, List<IFormFile> file)
633638
{

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

+32-14
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
// Copyright (c) Umbraco.
22
// See LICENSE for more details.
33

4-
using System.Collections.Generic;
54
using System.Security.Claims;
6-
using System.Threading.Tasks;
75
using Microsoft.AspNetCore.Authorization;
86
using Microsoft.AspNetCore.Http;
97
using Microsoft.Extensions.Primitives;
@@ -34,7 +32,7 @@ public class ContentPermissionsQueryStringHandlerTests
3432
public async Task Node_Id_From_Requirement_With_Permission_Is_Authorized()
3533
{
3634
var authHandlerContext = CreateAuthorizationHandlerContext(NodeId);
37-
var mockHttpContextAccessor = CreateMockHttpContextAccessor();
35+
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue();
3836
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "A" });
3937

4038
await sut.HandleAsync(authHandlerContext);
@@ -46,7 +44,7 @@ public async Task Node_Id_From_Requirement_With_Permission_Is_Authorized()
4644
public async Task Node_Id_From_Requirement_Without_Permission_Is_Not_Authorized()
4745
{
4846
var authHandlerContext = CreateAuthorizationHandlerContext(NodeId);
49-
var mockHttpContextAccessor = CreateMockHttpContextAccessor();
47+
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue();
5048
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "B" });
5149

5250
await sut.HandleAsync(authHandlerContext);
@@ -59,7 +57,7 @@ public async Task Node_Id_From_Requirement_Without_Permission_Is_Not_Authorized(
5957
public async Task Node_Id_Missing_From_Requirement_And_QueryString_Is_Authorized()
6058
{
6159
var authHandlerContext = CreateAuthorizationHandlerContext();
62-
var mockHttpContextAccessor = CreateMockHttpContextAccessor("xxx");
60+
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue("xxx");
6361
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "A" });
6462

6563
await sut.HandleAsync(authHandlerContext);
@@ -71,7 +69,7 @@ public async Task Node_Id_Missing_From_Requirement_And_QueryString_Is_Authorized
7169
public async Task Node_Integer_Id_From_QueryString_With_Permission_Is_Authorized()
7270
{
7371
var authHandlerContext = CreateAuthorizationHandlerContext();
74-
var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: NodeId.ToString());
72+
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: NodeId.ToString());
7573
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "A" });
7674

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

90102
await sut.HandleAsync(authHandlerContext);
@@ -97,7 +109,7 @@ public async Task Node_Integer_Id_From_QueryString_Without_Permission_Is_Not_Aut
97109
public async Task Node_Udi_Id_From_QueryString_With_Permission_Is_Authorized()
98110
{
99111
var authHandlerContext = CreateAuthorizationHandlerContext();
100-
var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: s_nodeUdi.ToString());
112+
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: s_nodeUdi.ToString());
101113
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "A" });
102114

103115
await sut.HandleAsync(authHandlerContext);
@@ -110,7 +122,7 @@ public async Task Node_Udi_Id_From_QueryString_With_Permission_Is_Authorized()
110122
public async Task Node_Udi_Id_From_QueryString_Without_Permission_Is_Not_Authorized()
111123
{
112124
var authHandlerContext = CreateAuthorizationHandlerContext();
113-
var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: s_nodeUdi.ToString());
125+
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: s_nodeUdi.ToString());
114126
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "B" });
115127

116128
await sut.HandleAsync(authHandlerContext);
@@ -123,7 +135,7 @@ public async Task Node_Udi_Id_From_QueryString_Without_Permission_Is_Not_Authori
123135
public async Task Node_Guid_Id_From_QueryString_With_Permission_Is_Authorized()
124136
{
125137
var authHandlerContext = CreateAuthorizationHandlerContext();
126-
var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: s_nodeGuid.ToString());
138+
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: s_nodeGuid.ToString());
127139
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "A" });
128140

129141
await sut.HandleAsync(authHandlerContext);
@@ -136,7 +148,7 @@ public async Task Node_Guid_Id_From_QueryString_With_Permission_Is_Authorized()
136148
public async Task Node_Guid_Id_From_QueryString_Without_Permission_Is_Not_Authorized()
137149
{
138150
var authHandlerContext = CreateAuthorizationHandlerContext();
139-
var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: s_nodeGuid.ToString());
151+
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: s_nodeGuid.ToString());
140152
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "B" });
141153

142154
await sut.HandleAsync(authHandlerContext);
@@ -149,7 +161,7 @@ public async Task Node_Guid_Id_From_QueryString_Without_Permission_Is_Not_Author
149161
public async Task Node_Invalid_Id_From_QueryString_Is_Authorized()
150162
{
151163
var authHandlerContext = CreateAuthorizationHandlerContext();
152-
var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: "invalid");
164+
var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: "invalid");
153165
var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "A" });
154166

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

171-
private static Mock<IHttpContextAccessor> CreateMockHttpContextAccessor(
183+
private static Mock<IHttpContextAccessor> CreateMockHttpContextAccessorWithQueryStringValue(
172184
string queryStringName = QueryStringName,
173185
string queryStringValue = "")
186+
=> CreateMockHttpContextAccessorWithQueryStringValues(queryStringName, [queryStringValue]);
187+
188+
private static Mock<IHttpContextAccessor> CreateMockHttpContextAccessorWithQueryStringValues(
189+
string queryStringName = QueryStringName,
190+
string[]? queryStringValues = null)
174191
{
192+
queryStringValues ??= [];
175193
var mockHttpContextAccessor = new Mock<IHttpContextAccessor>();
176194
var mockHttpContext = new Mock<HttpContext>();
177195
var mockHttpRequest = new Mock<HttpRequest>();
178-
var queryParams = new Dictionary<string, StringValues> { { queryStringName, queryStringValue } };
196+
var queryParams = new Dictionary<string, StringValues> { { queryStringName, new StringValues(queryStringValues) } };
179197
mockHttpRequest.SetupGet(x => x.Query).Returns(new QueryCollection(queryParams));
180198
mockHttpContext.SetupGet(x => x.Request).Returns(mockHttpRequest.Object);
181199
mockHttpContext.SetupGet(x => x.Items).Returns(new Dictionary<object, object>());

0 commit comments

Comments
 (0)