Moderate severityNVD Advisory· Published Mar 11, 2025· Updated Mar 11, 2025
Umbraco Allows a Restricted Editor User to Delete Media Item or Access Unauthorized Content
CVE-2025-27602
Description
Umbraco is a free and open source .NET content management system. In versions of Umbraco's web backoffice program prior to versions 10.8.9 and 13.7.1, via manipulation of backoffice API URLs, it's possible for authenticated backoffice users to retrieve or delete content or media held within folders the editor does not have access to. The issue is patched in versions 10.8.9 and 13.7.1. No known workarounds are available.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
Umbraco.Cms.Web.BackofficeNuGet | < 10.8.9 | 10.8.9 |
Umbraco.Cms.Web.BackofficeNuGet | >= 11.0.0-rc1, < 13.7.1 | 13.7.1 |
Affected products
1- Range: < 10.8.9
Patches
25b54bed40668Merge commit from fork
8 files changed · +102 −32
src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsQueryStringHandler.cs+7 −1 modified@@ -20,6 +20,8 @@ public class { private readonly ContentPermissions _contentPermissions; + protected override UmbracoObjectTypes KeyParsingFilterType => UmbracoObjectTypes.Document; + /// <summary> /// Initializes a new instance of the <see cref="ContentPermissionsQueryStringHandler" /> class. /// </summary> @@ -48,7 +50,11 @@ protected override Task<bool> IsAuthorized(AuthorizationHandlerContext context, return Task.FromResult(true); } - var argument = routeVal.ToString(); + // Handle case where the incoming querystring could contain more than one value (e.g. ?id=1000&id=1001). + // It's the first one that'll be processed by the protected method so we should verify that. + var argument = routeVal.Count == 1 + ? routeVal.ToString() + : routeVal.FirstOrDefault()?.ToString() ?? string.Empty; if (!TryParseNodeId(argument, out nodeId)) {
src/Umbraco.Web.BackOffice/Authorization/MediaPermissionsQueryStringHandler.cs+7 −1 modified@@ -18,6 +18,8 @@ public class MediaPermissionsQueryStringHandler : PermissionsQueryStringHandler< { private readonly MediaPermissions _mediaPermissions; + protected override UmbracoObjectTypes KeyParsingFilterType => UmbracoObjectTypes.Media; + /// <summary> /// Initializes a new instance of the <see cref="MediaPermissionsQueryStringHandler" /> class. /// </summary> @@ -44,7 +46,11 @@ protected override Task<bool> IsAuthorized(AuthorizationHandlerContext context, return Task.FromResult(true); } - var argument = routeVal.ToString(); + // Handle case where the incoming querystring could contain more than one value (e.g. ?id=1000&id=1001). + // It's the first one that'll be processed by the protected method so we should verify that. + var argument = routeVal.Count == 1 + ? routeVal.ToString() + : routeVal.FirstOrDefault()?.ToString() ?? string.Empty; if (!TryParseNodeId(argument, out var nodeId)) {
src/Umbraco.Web.BackOffice/Authorization/PermissionsQueryStringHandler.cs+8 −2 modified@@ -49,12 +49,18 @@ public PermissionsQueryStringHandler( /// </summary> protected IEntityService EntityService { get; set; } + /// <summary> + /// Defaults to Unknown so all types are allowed, since Keys are unique across all node types this works, + /// but it if you are certain you are looking for a specific type this should be overwritten for DB query performance. + /// </summary> + protected virtual UmbracoObjectTypes KeyParsingFilterType => UmbracoObjectTypes.Unknown; + /// <summary> /// Attempts to parse a node ID from a string representation found in a querystring value. /// </summary> /// <param name="argument">Querystring value.</param> /// <param name="nodeId">Output parsed Id.</param> - /// <returns>True of node ID could be parased, false it not.</returns> + /// <returns>True of node ID could be parsed, false it not.</returns> protected bool TryParseNodeId(string argument, out int nodeId) { // 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) if (Guid.TryParse(argument, out Guid key)) { - nodeId = EntityService.GetId(key, UmbracoObjectTypes.Document).Result; + nodeId = EntityService.GetId(key, KeyParsingFilterType).Result; return true; }
src/Umbraco.Web.BackOffice/Controllers/ContentController.cs+10 −0 modified@@ -256,6 +256,7 @@ public IEnumerable<ContentItemDisplay> GetByIds([FromQuery] int[] ids) /// Permission check is done for letter 'R' which is for <see cref="ActionRights" /> which the user must have access to /// update /// </remarks> + [HttpPost] public async Task<ActionResult<IEnumerable<AssignedUserGroupPermissions?>?>> PostSaveUserGroupPermissions( UserGroupPermissionsSave saveModel) { @@ -902,6 +903,7 @@ private bool EnsureUniqueName(string? name, IContent? content, string modelName) [Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)] [FileUploadCleanupFilter] [ContentSaveValidation(skipUserAccessValidation:true)] // skip user access validation because we "only" require Settings access to create new blueprints from scratch + [HttpPost] public async Task<ActionResult<ContentItemDisplay<ContentVariantDisplay>?>?> PostSaveBlueprint( [ModelBinder(typeof(BlueprintItemBinder))] ContentItemSave contentItem) { @@ -939,6 +941,7 @@ private bool EnsureUniqueName(string? name, IContent? content, string modelName) [FileUploadCleanupFilter] [ContentSaveValidation] [OutgoingEditorModelEvent] + [HttpPost] public async Task<ActionResult<ContentItemDisplay<ContentVariantScheduleDisplay>?>> PostSave( [ModelBinder(typeof(ContentItemBinder))] ContentItemSave contentItem) { @@ -2089,6 +2092,7 @@ private string GetVariantName(string? culture, string? segment) /// does not have Publish access to this node. /// </remarks> [Authorize(Policy = AuthorizationPolicies.ContentPermissionPublishById)] + [HttpPost] public IActionResult PostPublishById(int id) { IContent? foundContent = GetObjectFromRequest(() => _contentService.GetById(id)); @@ -2120,6 +2124,7 @@ public IActionResult PostPublishById(int id) /// does not have Publish access to this node. /// </remarks> [Authorize(Policy = AuthorizationPolicies.ContentPermissionPublishById)] + [HttpPost] public IActionResult PostPublishByIdAndCulture(PublishContent model) { var languageCount = _allLangs.Value.Count(); @@ -2243,6 +2248,7 @@ public IActionResult EmptyRecycleBin() /// </summary> /// <param name="sorted"></param> /// <returns></returns> + [HttpPost] public async Task<IActionResult> PostSort(ContentSortOrder sorted) { if (sorted == null) @@ -2294,6 +2300,7 @@ public async Task<IActionResult> PostSort(ContentSortOrder sorted) /// </summary> /// <param name="move"></param> /// <returns></returns> + [HttpPost] public async Task<IActionResult?> PostMove(MoveOrCopy move) { // Authorize... @@ -2333,6 +2340,7 @@ public async Task<IActionResult> PostSort(ContentSortOrder sorted) /// </summary> /// <param name="copy"></param> /// <returns></returns> + [HttpPost] public async Task<ActionResult<IContent>?> PostCopy(MoveOrCopy copy) { // Authorize... @@ -2372,6 +2380,7 @@ public async Task<IActionResult> PostSort(ContentSortOrder sorted) /// <param name="model">The content and variants to unpublish</param> /// <returns></returns> [OutgoingEditorModelEvent] + [HttpPost] public async Task<ActionResult<ContentItemDisplayWithSchedule?>> PostUnpublish(UnpublishContent model) { IContent? foundContent = _contentService.GetById(model.Id); @@ -3096,6 +3105,7 @@ public ActionResult<IEnumerable<NotifySetting>> GetNotificationOptions(int conte return notifications; } + [HttpPost] public IActionResult PostNotificationOptions( int contentId, [FromQuery(Name = "notifyOptions[]")] string[] notifyOptions)
src/Umbraco.Web.BackOffice/Controllers/MediaController.cs+5 −0 modified@@ -386,6 +386,7 @@ public IActionResult DeleteById(int id) /// </summary> /// <param name="move"></param> /// <returns></returns> + [HttpPost] public async Task<IActionResult> PostMove(MoveOrCopy move) { // Authorize... @@ -436,6 +437,7 @@ public async Task<IActionResult> PostMove(MoveOrCopy move) [FileUploadCleanupFilter] [MediaItemSaveValidation] [OutgoingEditorModelEvent] + [HttpPost] public ActionResult<MediaItemDisplay?>? PostSave( [ModelBinder(typeof(MediaItemBinder))] MediaItemSave contentItem) { @@ -551,6 +553,7 @@ public IActionResult EmptyRecycleBin() /// </summary> /// <param name="sorted"></param> /// <returns></returns> + [HttpPost] public async Task<IActionResult> PostSort(ContentSortOrder sorted) { if (sorted == null) @@ -595,6 +598,7 @@ public async Task<IActionResult> PostSort(ContentSortOrder sorted) } } + [HttpPost] public async Task<ActionResult<MediaItemDisplay?>> PostAddFolder(PostedFolder folder) { ActionResult<int?>? parentIdResult = await GetParentIdAsIntAsync(folder.ParentId, true); @@ -628,6 +632,7 @@ public async Task<IActionResult> PostSort(ContentSortOrder sorted) /// <remarks> /// We cannot validate this request with attributes (nicely) due to the nature of the multi-part for data. /// </remarks> + [HttpPost] public async Task<IActionResult> PostAddFile([FromForm] string path, [FromForm] string currentFolder, [FromForm] string contentTypeAlias, List<IFormFile> file) {
tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Authorization/ContentPermissionsQueryStringHandlerTests.cs+32 −14 modified@@ -1,9 +1,7 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using System.Security.Claims; -using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; @@ -34,7 +32,7 @@ public class ContentPermissionsQueryStringHandlerTests public async Task Node_Id_From_Requirement_With_Permission_Is_Authorized() { var authHandlerContext = CreateAuthorizationHandlerContext(NodeId); - var mockHttpContextAccessor = CreateMockHttpContextAccessor(); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(); var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "A" }); await sut.HandleAsync(authHandlerContext); @@ -46,7 +44,7 @@ public async Task Node_Id_From_Requirement_With_Permission_Is_Authorized() public async Task Node_Id_From_Requirement_Without_Permission_Is_Not_Authorized() { var authHandlerContext = CreateAuthorizationHandlerContext(NodeId); - var mockHttpContextAccessor = CreateMockHttpContextAccessor(); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(); var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "B" }); await sut.HandleAsync(authHandlerContext); @@ -59,7 +57,7 @@ public async Task Node_Id_From_Requirement_Without_Permission_Is_Not_Authorized( public async Task Node_Id_Missing_From_Requirement_And_QueryString_Is_Authorized() { var authHandlerContext = CreateAuthorizationHandlerContext(); - var mockHttpContextAccessor = CreateMockHttpContextAccessor("xxx"); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue("xxx"); var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "A" }); await sut.HandleAsync(authHandlerContext); @@ -71,7 +69,7 @@ public async Task Node_Id_Missing_From_Requirement_And_QueryString_Is_Authorized public async Task Node_Integer_Id_From_QueryString_With_Permission_Is_Authorized() { var authHandlerContext = CreateAuthorizationHandlerContext(); - var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: NodeId.ToString()); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: NodeId.ToString()); var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "A" }); await sut.HandleAsync(authHandlerContext); @@ -84,7 +82,21 @@ public async Task Node_Integer_Id_From_QueryString_With_Permission_Is_Authorized public async Task Node_Integer_Id_From_QueryString_Without_Permission_Is_Not_Authorized() { var authHandlerContext = CreateAuthorizationHandlerContext(); - var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: NodeId.ToString()); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: NodeId.ToString()); + var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "B" }); + + await sut.HandleAsync(authHandlerContext); + + Assert.IsFalse(authHandlerContext.HasSucceeded); + AssertContentCached(mockHttpContextAccessor); + } + + [Test] + public async Task Node_Integer_Id_From_QueryString_Without_Permission_Is_Not_Authorized_Even_When_Additional_Parameter_For_Id_With_Permission_Is_Provided() + { + // Provides initially failing test and verifies fix for advisory https://github.com/umbraco/Umbraco-CMS/security/advisories/GHSA-wx5h-wqfq-v698 + var authHandlerContext = CreateAuthorizationHandlerContext(); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValues(queryStringValues: [NodeId.ToString(), 1001.ToString()]); var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "B" }); await sut.HandleAsync(authHandlerContext); @@ -97,7 +109,7 @@ public async Task Node_Integer_Id_From_QueryString_Without_Permission_Is_Not_Aut public async Task Node_Udi_Id_From_QueryString_With_Permission_Is_Authorized() { var authHandlerContext = CreateAuthorizationHandlerContext(); - var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: s_nodeUdi.ToString()); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: s_nodeUdi.ToString()); var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "A" }); await sut.HandleAsync(authHandlerContext); @@ -110,7 +122,7 @@ public async Task Node_Udi_Id_From_QueryString_With_Permission_Is_Authorized() public async Task Node_Udi_Id_From_QueryString_Without_Permission_Is_Not_Authorized() { var authHandlerContext = CreateAuthorizationHandlerContext(); - var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: s_nodeUdi.ToString()); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: s_nodeUdi.ToString()); var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "B" }); await sut.HandleAsync(authHandlerContext); @@ -123,7 +135,7 @@ public async Task Node_Udi_Id_From_QueryString_Without_Permission_Is_Not_Authori public async Task Node_Guid_Id_From_QueryString_With_Permission_Is_Authorized() { var authHandlerContext = CreateAuthorizationHandlerContext(); - var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: s_nodeGuid.ToString()); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: s_nodeGuid.ToString()); var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "A" }); await sut.HandleAsync(authHandlerContext); @@ -136,7 +148,7 @@ public async Task Node_Guid_Id_From_QueryString_With_Permission_Is_Authorized() public async Task Node_Guid_Id_From_QueryString_Without_Permission_Is_Not_Authorized() { var authHandlerContext = CreateAuthorizationHandlerContext(); - var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: s_nodeGuid.ToString()); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: s_nodeGuid.ToString()); var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "B" }); await sut.HandleAsync(authHandlerContext); @@ -149,7 +161,7 @@ public async Task Node_Guid_Id_From_QueryString_Without_Permission_Is_Not_Author public async Task Node_Invalid_Id_From_QueryString_Is_Authorized() { var authHandlerContext = CreateAuthorizationHandlerContext(); - var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: "invalid"); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: "invalid"); var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "A" }); await sut.HandleAsync(authHandlerContext); @@ -168,14 +180,20 @@ private static AuthorizationHandlerContext CreateAuthorizationHandlerContext(int return new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement }, user, resource); } - private static Mock<IHttpContextAccessor> CreateMockHttpContextAccessor( + private static Mock<IHttpContextAccessor> CreateMockHttpContextAccessorWithQueryStringValue( string queryStringName = QueryStringName, string queryStringValue = "") + => CreateMockHttpContextAccessorWithQueryStringValues(queryStringName, [queryStringValue]); + + private static Mock<IHttpContextAccessor> CreateMockHttpContextAccessorWithQueryStringValues( + string queryStringName = QueryStringName, + string[]? queryStringValues = null) { + queryStringValues ??= []; var mockHttpContextAccessor = new Mock<IHttpContextAccessor>(); var mockHttpContext = new Mock<HttpContext>(); var mockHttpRequest = new Mock<HttpRequest>(); - var queryParams = new Dictionary<string, StringValues> { { queryStringName, queryStringValue } }; + var queryParams = new Dictionary<string, StringValues> { { queryStringName, new StringValues(queryStringValues) } }; mockHttpRequest.SetupGet(x => x.Query).Returns(new QueryCollection(queryParams)); mockHttpContext.SetupGet(x => x.Request).Returns(mockHttpRequest.Object); mockHttpContext.SetupGet(x => x.Items).Returns(new Dictionary<object, object>());
tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Authorization/MediaPermissionsQueryStringHandlerTests.cs+32 −13 modified@@ -1,9 +1,7 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; using System.Security.Claims; -using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; @@ -33,7 +31,7 @@ public class MediaPermissionsQueryStringHandlerTests public async Task Node_Id_Missing_From_QueryString_Is_Authorized() { var authHandlerContext = CreateAuthorizationHandlerContext(); - var mockHttpContextAccessor = CreateMockHttpContextAccessor("xxx"); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue("xxx"); var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId); await sut.HandleAsync(authHandlerContext); @@ -45,7 +43,7 @@ public async Task Node_Id_Missing_From_QueryString_Is_Authorized() public async Task Node_Integer_Id_From_QueryString_With_Permission_Is_Authorized() { var authHandlerContext = CreateAuthorizationHandlerContext(); - var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: NodeId.ToString()); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: NodeId.ToString()); var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId); await sut.HandleAsync(authHandlerContext); @@ -58,7 +56,21 @@ public async Task Node_Integer_Id_From_QueryString_With_Permission_Is_Authorized public async Task Node_Integer_Id_From_QueryString_Without_Permission_Is_Not_Authorized() { var authHandlerContext = CreateAuthorizationHandlerContext(); - var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: NodeId.ToString()); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: NodeId.ToString()); + var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, 1001); + + await sut.HandleAsync(authHandlerContext); + + Assert.IsFalse(authHandlerContext.HasSucceeded); + AssertMediaCached(mockHttpContextAccessor); + } + + [Test] + public async Task Node_Integer_Id_From_QueryString_Without_Permission_Is_Not_Authorized_Even_When_Additional_Parameter_For_Id_With_Permission_Is_Provided() + { + // Provides initially failing test and verifies fix for advisory https://github.com/umbraco/Umbraco-CMS/security/advisories/GHSA-wx5h-wqfq-v698 + var authHandlerContext = CreateAuthorizationHandlerContext(); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValues(queryStringValues: [NodeId.ToString(), 1001.ToString()]); var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, 1001); await sut.HandleAsync(authHandlerContext); @@ -71,7 +83,7 @@ public async Task Node_Integer_Id_From_QueryString_Without_Permission_Is_Not_Aut public async Task Node_Udi_Id_From_QueryString_With_Permission_Is_Authorized() { var authHandlerContext = CreateAuthorizationHandlerContext(); - var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: s_nodeUdi.ToString()); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: s_nodeUdi.ToString()); var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId); await sut.HandleAsync(authHandlerContext); @@ -84,7 +96,7 @@ public async Task Node_Udi_Id_From_QueryString_With_Permission_Is_Authorized() public async Task Node_Udi_Id_From_QueryString_Without_Permission_Is_Not_Authorized() { var authHandlerContext = CreateAuthorizationHandlerContext(); - var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: s_nodeUdi.ToString()); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: s_nodeUdi.ToString()); var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, 1001); await sut.HandleAsync(authHandlerContext); @@ -97,7 +109,7 @@ public async Task Node_Udi_Id_From_QueryString_Without_Permission_Is_Not_Authori public async Task Node_Guid_Id_From_QueryString_With_Permission_Is_Authorized() { var authHandlerContext = CreateAuthorizationHandlerContext(); - var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: s_nodeGuid.ToString()); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: s_nodeGuid.ToString()); var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId); await sut.HandleAsync(authHandlerContext); @@ -110,7 +122,7 @@ public async Task Node_Guid_Id_From_QueryString_With_Permission_Is_Authorized() public async Task Node_Guid_Id_From_QueryString_Without_Permission_Is_Not_Authorized() { var authHandlerContext = CreateAuthorizationHandlerContext(); - var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: s_nodeGuid.ToString()); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: s_nodeGuid.ToString()); var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, 1001); await sut.HandleAsync(authHandlerContext); @@ -123,7 +135,7 @@ public async Task Node_Guid_Id_From_QueryString_Without_Permission_Is_Not_Author public async Task Node_Invalid_Id_From_QueryString_Is_Authorized() { var authHandlerContext = CreateAuthorizationHandlerContext(); - var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: "invalid"); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: "invalid"); var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId); await sut.HandleAsync(authHandlerContext); @@ -139,14 +151,21 @@ private static AuthorizationHandlerContext CreateAuthorizationHandlerContext() return new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement }, user, resource); } - private static Mock<IHttpContextAccessor> CreateMockHttpContextAccessor( + private static Mock<IHttpContextAccessor> CreateMockHttpContextAccessorWithQueryStringValue( string queryStringName = QueryStringName, string queryStringValue = "") + => CreateMockHttpContextAccessorWithQueryStringValues(queryStringName, [queryStringValue]); + + private static Mock<IHttpContextAccessor> CreateMockHttpContextAccessorWithQueryStringValues( + string queryStringName = QueryStringName, + string[]? queryStringValues = null) { + queryStringValues ??= []; + var mockHttpContextAccessor = new Mock<IHttpContextAccessor>(); var mockHttpContext = new Mock<HttpContext>(); var mockHttpRequest = new Mock<HttpRequest>(); - var queryParams = new Dictionary<string, StringValues> { { queryStringName, queryStringValue } }; + var queryParams = new Dictionary<string, StringValues> { { queryStringName, new StringValues(queryStringValues) } }; mockHttpRequest.SetupGet(x => x.Query).Returns(new QueryCollection(queryParams)); mockHttpContext.SetupGet(x => x.Request).Returns(mockHttpRequest.Object); mockHttpContext.SetupGet(x => x.Items).Returns(new Dictionary<object, object>()); @@ -178,7 +197,7 @@ private static Mock<IEntityService> CreateMockEntityService() mockEntityService .Setup(x => x.GetId( It.Is<Guid>(y => y == s_nodeGuid), - It.Is<UmbracoObjectTypes>(y => y == UmbracoObjectTypes.Document))) + It.Is<UmbracoObjectTypes>(y => y == UmbracoObjectTypes.Media))) .Returns(Attempt<int>.Succeed(NodeId)); return mockEntityService; }
tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Authorization/MediaPermissionsResourceHandlerTests.cs+1 −1 modified@@ -44,7 +44,7 @@ public async Task Resource_With_Media_With_Permission_Is_Authorized() } [Test] - public async Task Resource_With_Node_Id_Withou_Permission_Is_Not_Authorized() + public async Task Resource_With_Node_Id_Without_Permission_Is_Not_Authorized() { var authHandlerContext = CreateAuthorizationHandlerContext(NodeId, true); var sut = CreateHandler(NodeId, 1001);
7888b9a4ce5aMerge commit from fork
9 files changed · +110 −33
src/Umbraco.Web.BackOffice/Authorization/ContentPermissionsQueryStringHandler.cs+7 −1 modified@@ -19,6 +19,8 @@ public class { private readonly ContentPermissions _contentPermissions; + protected override UmbracoObjectTypes KeyParsingFilterType => UmbracoObjectTypes.Document; + /// <summary> /// Initializes a new instance of the <see cref="ContentPermissionsQueryStringHandler" /> class. /// </summary> @@ -47,7 +49,11 @@ protected override Task<bool> IsAuthorized(AuthorizationHandlerContext context, return Task.FromResult(true); } - var argument = routeVal.ToString(); + // Handle case where the incoming querystring could contain more than one value (e.g. ?id=1000&id=1001). + // It's the first one that'll be processed by the protected method so we should verify that. + var argument = routeVal.Count == 1 + ? routeVal.ToString() + : routeVal.FirstOrDefault()?.ToString() ?? string.Empty; if (!TryParseNodeId(argument, out nodeId)) {
src/Umbraco.Web.BackOffice/Authorization/MediaPermissionsQueryStringHandler.cs+7 −1 modified@@ -18,6 +18,8 @@ public class MediaPermissionsQueryStringHandler : PermissionsQueryStringHandler< { private readonly MediaPermissions _mediaPermissions; + protected override UmbracoObjectTypes KeyParsingFilterType => UmbracoObjectTypes.Media; + /// <summary> /// Initializes a new instance of the <see cref="MediaPermissionsQueryStringHandler" /> class. /// </summary> @@ -44,7 +46,11 @@ protected override Task<bool> IsAuthorized(AuthorizationHandlerContext context, return Task.FromResult(true); } - var argument = routeVal.ToString(); + // Handle case where the incoming querystring could contain more than one value (e.g. ?id=1000&id=1001). + // It's the first one that'll be processed by the protected method so we should verify that. + var argument = routeVal.Count == 1 + ? routeVal.ToString() + : routeVal.FirstOrDefault()?.ToString() ?? string.Empty; if (!TryParseNodeId(argument, out var nodeId)) {
src/Umbraco.Web.BackOffice/Authorization/PermissionsQueryStringHandler.cs+8 −2 modified@@ -49,12 +49,18 @@ public PermissionsQueryStringHandler( /// </summary> protected IEntityService EntityService { get; set; } + /// <summary> + /// Defaults to Unknown so all types are allowed, since Keys are unique across all node types this works, + /// but it if you are certain you are looking for a specific type this should be overwritten for DB query performance. + /// </summary> + protected virtual UmbracoObjectTypes KeyParsingFilterType => UmbracoObjectTypes.Unknown; + /// <summary> /// Attempts to parse a node ID from a string representation found in a querystring value. /// </summary> /// <param name="argument">Querystring value.</param> /// <param name="nodeId">Output parsed Id.</param> - /// <returns>True of node ID could be parased, false it not.</returns> + /// <returns>True of node ID could be parsed, false it not.</returns> protected bool TryParseNodeId(string argument, out int nodeId) { // 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) if (Guid.TryParse(argument, out Guid key)) { - nodeId = EntityService.GetId(key, UmbracoObjectTypes.Document).Result; + nodeId = EntityService.GetId(key, KeyParsingFilterType).Result; return true; }
src/Umbraco.Web.BackOffice/Controllers/ContentController.cs+10 −0 modified@@ -196,6 +196,7 @@ public IEnumerable<ContentItemDisplay> GetByIds([FromQuery] int[] ids) /// Permission check is done for letter 'R' which is for <see cref="ActionRights" /> which the user must have access to /// update /// </remarks> + [HttpPost] public async Task<ActionResult<IEnumerable<AssignedUserGroupPermissions?>?>> PostSaveUserGroupPermissions( UserGroupPermissionsSave saveModel) { @@ -842,6 +843,7 @@ private bool EnsureUniqueName(string? name, IContent? content, string modelName) [Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentTypes)] [FileUploadCleanupFilter] [ContentSaveValidation(skipUserAccessValidation:true)] // skip user access validation because we "only" require Settings access to create new blueprints from scratch + [HttpPost] public async Task<ActionResult<ContentItemDisplay<ContentVariantDisplay>?>?> PostSaveBlueprint( [ModelBinder(typeof(BlueprintItemBinder))] ContentItemSave contentItem) { @@ -879,6 +881,7 @@ private bool EnsureUniqueName(string? name, IContent? content, string modelName) [FileUploadCleanupFilter] [ContentSaveValidation] [OutgoingEditorModelEvent] + [HttpPost] public async Task<ActionResult<ContentItemDisplay<ContentVariantScheduleDisplay>?>> PostSave( [ModelBinder(typeof(ContentItemBinder))] ContentItemSave contentItem) { @@ -1960,6 +1963,7 @@ private string GetVariantName(string? culture, string? segment) /// does not have Publish access to this node. /// </remarks> [Authorize(Policy = AuthorizationPolicies.ContentPermissionPublishById)] + [HttpPost] public IActionResult PostPublishById(int id) { IContent? foundContent = GetObjectFromRequest(() => _contentService.GetById(id)); @@ -1991,6 +1995,7 @@ public IActionResult PostPublishById(int id) /// does not have Publish access to this node. /// </remarks> [Authorize(Policy = AuthorizationPolicies.ContentPermissionPublishById)] + [HttpPost] public IActionResult PostPublishByIdAndCulture(PublishContent model) { var languageCount = _allLangs.Value.Count(); @@ -2114,6 +2119,7 @@ public IActionResult EmptyRecycleBin() /// </summary> /// <param name="sorted"></param> /// <returns></returns> + [HttpPost] public async Task<IActionResult> PostSort(ContentSortOrder sorted) { if (sorted == null) @@ -2165,6 +2171,7 @@ public async Task<IActionResult> PostSort(ContentSortOrder sorted) /// </summary> /// <param name="move"></param> /// <returns></returns> + [HttpPost] public async Task<IActionResult?> PostMove(MoveOrCopy move) { // Authorize... @@ -2199,6 +2206,7 @@ public async Task<IActionResult> PostSort(ContentSortOrder sorted) /// </summary> /// <param name="copy"></param> /// <returns></returns> + [HttpPost] public async Task<ActionResult<IContent>?> PostCopy(MoveOrCopy copy) { // Authorize... @@ -2238,6 +2246,7 @@ public async Task<IActionResult> PostSort(ContentSortOrder sorted) /// <param name="model">The content and variants to unpublish</param> /// <returns></returns> [OutgoingEditorModelEvent] + [HttpPost] public async Task<ActionResult<ContentItemDisplayWithSchedule?>> PostUnpublish(UnpublishContent model) { IContent? foundContent = _contentService.GetById(model.Id); @@ -2960,6 +2969,7 @@ public ActionResult<IEnumerable<NotifySetting>> GetNotificationOptions(int conte return notifications; } + [HttpPost] public IActionResult PostNotificationOptions( int contentId, [FromQuery(Name = "notifyOptions[]")] string[] notifyOptions)
src/Umbraco.Web.BackOffice/Controllers/MediaController.cs+5 −0 modified@@ -385,6 +385,7 @@ public IActionResult DeleteById(int id) /// </summary> /// <param name="move"></param> /// <returns></returns> + [HttpPost] public async Task<IActionResult> PostMove(MoveOrCopy move) { // Authorize... @@ -432,6 +433,7 @@ public async Task<IActionResult> PostMove(MoveOrCopy move) [FileUploadCleanupFilter] [MediaItemSaveValidation] [OutgoingEditorModelEvent] + [HttpPost] public ActionResult<MediaItemDisplay?>? PostSave( [ModelBinder(typeof(MediaItemBinder))] MediaItemSave contentItem) { @@ -547,6 +549,7 @@ public IActionResult EmptyRecycleBin() /// </summary> /// <param name="sorted"></param> /// <returns></returns> + [HttpPost] public async Task<IActionResult> PostSort(ContentSortOrder sorted) { if (sorted == null) @@ -592,6 +595,7 @@ public async Task<IActionResult> PostSort(ContentSortOrder sorted) } } + [HttpPost] public async Task<ActionResult<MediaItemDisplay?>> PostAddFolder(PostedFolder folder) { ActionResult<int?>? parentIdResult = await GetParentIdAsIntAsync(folder.ParentId, true); @@ -625,6 +629,7 @@ public async Task<IActionResult> PostSort(ContentSortOrder sorted) /// <remarks> /// We cannot validate this request with attributes (nicely) due to the nature of the multi-part for data. /// </remarks> + [HttpPost] public async Task<IActionResult> PostAddFile([FromForm] string path, [FromForm] string currentFolder, [FromForm] string contentTypeAlias, List<IFormFile> file) {
tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Authorization/ContentPermissionsQueryStringHandlerTests.cs+32 −14 modified@@ -2,9 +2,7 @@ // See LICENSE for more details. using System; -using System.Collections.Generic; using System.Security.Claims; -using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; @@ -35,7 +33,7 @@ public class ContentPermissionsQueryStringHandlerTests public async Task Node_Id_From_Requirement_With_Permission_Is_Authorized() { var authHandlerContext = CreateAuthorizationHandlerContext(NodeId); - var mockHttpContextAccessor = CreateMockHttpContextAccessor(); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(); var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "A" }); await sut.HandleAsync(authHandlerContext); @@ -47,7 +45,7 @@ public async Task Node_Id_From_Requirement_With_Permission_Is_Authorized() public async Task Node_Id_From_Requirement_Without_Permission_Is_Not_Authorized() { var authHandlerContext = CreateAuthorizationHandlerContext(NodeId); - var mockHttpContextAccessor = CreateMockHttpContextAccessor(); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(); var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "B" }); await sut.HandleAsync(authHandlerContext); @@ -60,7 +58,7 @@ public async Task Node_Id_From_Requirement_Without_Permission_Is_Not_Authorized( public async Task Node_Id_Missing_From_Requirement_And_QueryString_Is_Authorized() { var authHandlerContext = CreateAuthorizationHandlerContext(); - var mockHttpContextAccessor = CreateMockHttpContextAccessor("xxx"); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue("xxx"); var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "A" }); await sut.HandleAsync(authHandlerContext); @@ -72,7 +70,7 @@ public async Task Node_Id_Missing_From_Requirement_And_QueryString_Is_Authorized public async Task Node_Integer_Id_From_QueryString_With_Permission_Is_Authorized() { var authHandlerContext = CreateAuthorizationHandlerContext(); - var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: NodeId.ToString()); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: NodeId.ToString()); var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "A" }); await sut.HandleAsync(authHandlerContext); @@ -85,7 +83,21 @@ public async Task Node_Integer_Id_From_QueryString_With_Permission_Is_Authorized public async Task Node_Integer_Id_From_QueryString_Without_Permission_Is_Not_Authorized() { var authHandlerContext = CreateAuthorizationHandlerContext(); - var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: NodeId.ToString()); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: NodeId.ToString()); + var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "B" }); + + await sut.HandleAsync(authHandlerContext); + + Assert.IsFalse(authHandlerContext.HasSucceeded); + AssertContentCached(mockHttpContextAccessor); + } + + [Test] + public async Task Node_Integer_Id_From_QueryString_Without_Permission_Is_Not_Authorized_Even_When_Additional_Parameter_For_Id_With_Permission_Is_Provided() + { + // Provides initially failing test and verifies fix for advisory https://github.com/umbraco/Umbraco-CMS/security/advisories/GHSA-wx5h-wqfq-v698 + var authHandlerContext = CreateAuthorizationHandlerContext(); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValues(queryStringValues: new[] { NodeId.ToString(), 1001.ToString() }); var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "B" }); await sut.HandleAsync(authHandlerContext); @@ -98,7 +110,7 @@ public async Task Node_Integer_Id_From_QueryString_Without_Permission_Is_Not_Aut public async Task Node_Udi_Id_From_QueryString_With_Permission_Is_Authorized() { var authHandlerContext = CreateAuthorizationHandlerContext(); - var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: s_nodeUdi.ToString()); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: s_nodeUdi.ToString()); var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "A" }); await sut.HandleAsync(authHandlerContext); @@ -111,7 +123,7 @@ public async Task Node_Udi_Id_From_QueryString_With_Permission_Is_Authorized() public async Task Node_Udi_Id_From_QueryString_Without_Permission_Is_Not_Authorized() { var authHandlerContext = CreateAuthorizationHandlerContext(); - var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: s_nodeUdi.ToString()); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: s_nodeUdi.ToString()); var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "B" }); await sut.HandleAsync(authHandlerContext); @@ -124,7 +136,7 @@ public async Task Node_Udi_Id_From_QueryString_Without_Permission_Is_Not_Authori public async Task Node_Guid_Id_From_QueryString_With_Permission_Is_Authorized() { var authHandlerContext = CreateAuthorizationHandlerContext(); - var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: s_nodeGuid.ToString()); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: s_nodeGuid.ToString()); var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "A" }); await sut.HandleAsync(authHandlerContext); @@ -137,7 +149,7 @@ public async Task Node_Guid_Id_From_QueryString_With_Permission_Is_Authorized() public async Task Node_Guid_Id_From_QueryString_Without_Permission_Is_Not_Authorized() { var authHandlerContext = CreateAuthorizationHandlerContext(); - var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: s_nodeGuid.ToString()); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: s_nodeGuid.ToString()); var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "B" }); await sut.HandleAsync(authHandlerContext); @@ -150,7 +162,7 @@ public async Task Node_Guid_Id_From_QueryString_Without_Permission_Is_Not_Author public async Task Node_Invalid_Id_From_QueryString_Is_Authorized() { var authHandlerContext = CreateAuthorizationHandlerContext(); - var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: "invalid"); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: "invalid"); var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, new[] { "A" }); await sut.HandleAsync(authHandlerContext); @@ -169,14 +181,20 @@ private static AuthorizationHandlerContext CreateAuthorizationHandlerContext(int return new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement }, user, resource); } - private static Mock<IHttpContextAccessor> CreateMockHttpContextAccessor( + private static Mock<IHttpContextAccessor> CreateMockHttpContextAccessorWithQueryStringValue( string queryStringName = QueryStringName, string queryStringValue = "") + => CreateMockHttpContextAccessorWithQueryStringValues(queryStringName, new[] { queryStringValue }); + + private static Mock<IHttpContextAccessor> CreateMockHttpContextAccessorWithQueryStringValues( + string queryStringName = QueryStringName, + string[]? queryStringValues = null) { + queryStringValues ??= Array.Empty<string>(); var mockHttpContextAccessor = new Mock<IHttpContextAccessor>(); var mockHttpContext = new Mock<HttpContext>(); var mockHttpRequest = new Mock<HttpRequest>(); - var queryParams = new Dictionary<string, StringValues> { { queryStringName, queryStringValue } }; + var queryParams = new Dictionary<string, StringValues> { { queryStringName, new StringValues(queryStringValues) } }; mockHttpRequest.SetupGet(x => x.Query).Returns(new QueryCollection(queryParams)); mockHttpContext.SetupGet(x => x.Request).Returns(mockHttpRequest.Object); mockHttpContext.SetupGet(x => x.Items).Returns(new Dictionary<object, object>());
tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Authorization/MediaPermissionsQueryStringHandlerTests.cs+39 −13 modified@@ -2,9 +2,7 @@ // See LICENSE for more details. using System; -using System.Collections.Generic; using System.Security.Claims; -using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; @@ -34,7 +32,7 @@ public class MediaPermissionsQueryStringHandlerTests public async Task Node_Id_Missing_From_QueryString_Is_Authorized() { var authHandlerContext = CreateAuthorizationHandlerContext(); - var mockHttpContextAccessor = CreateMockHttpContextAccessor("xxx"); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue("xxx"); var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId); await sut.HandleAsync(authHandlerContext); @@ -46,7 +44,7 @@ public async Task Node_Id_Missing_From_QueryString_Is_Authorized() public async Task Node_Integer_Id_From_QueryString_With_Permission_Is_Authorized() { var authHandlerContext = CreateAuthorizationHandlerContext(); - var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: NodeId.ToString()); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: NodeId.ToString()); var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId); await sut.HandleAsync(authHandlerContext); @@ -59,7 +57,21 @@ public async Task Node_Integer_Id_From_QueryString_With_Permission_Is_Authorized public async Task Node_Integer_Id_From_QueryString_Without_Permission_Is_Not_Authorized() { var authHandlerContext = CreateAuthorizationHandlerContext(); - var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: NodeId.ToString()); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: NodeId.ToString()); + var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, 1001); + + await sut.HandleAsync(authHandlerContext); + + Assert.IsFalse(authHandlerContext.HasSucceeded); + AssertMediaCached(mockHttpContextAccessor); + } + + [Test] + public async Task Node_Integer_Id_From_QueryString_Without_Permission_Is_Not_Authorized_Even_When_Additional_Parameter_For_Id_With_Permission_Is_Provided() + { + // Provides initially failing test and verifies fix for advisory https://github.com/umbraco/Umbraco-CMS/security/advisories/GHSA-wx5h-wqfq-v698 + var authHandlerContext = CreateAuthorizationHandlerContext(); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValues(queryStringValues: new[] { NodeId.ToString(), 1001.ToString() }); var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, 1001); await sut.HandleAsync(authHandlerContext); @@ -72,7 +84,7 @@ public async Task Node_Integer_Id_From_QueryString_Without_Permission_Is_Not_Aut public async Task Node_Udi_Id_From_QueryString_With_Permission_Is_Authorized() { var authHandlerContext = CreateAuthorizationHandlerContext(); - var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: s_nodeUdi.ToString()); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: s_nodeUdi.ToString()); var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId); await sut.HandleAsync(authHandlerContext); @@ -85,7 +97,7 @@ public async Task Node_Udi_Id_From_QueryString_With_Permission_Is_Authorized() public async Task Node_Udi_Id_From_QueryString_Without_Permission_Is_Not_Authorized() { var authHandlerContext = CreateAuthorizationHandlerContext(); - var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: s_nodeUdi.ToString()); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: s_nodeUdi.ToString()); var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, 1001); await sut.HandleAsync(authHandlerContext); @@ -98,7 +110,7 @@ public async Task Node_Udi_Id_From_QueryString_Without_Permission_Is_Not_Authori public async Task Node_Guid_Id_From_QueryString_With_Permission_Is_Authorized() { var authHandlerContext = CreateAuthorizationHandlerContext(); - var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: s_nodeGuid.ToString()); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: s_nodeGuid.ToString()); var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId); await sut.HandleAsync(authHandlerContext); @@ -111,7 +123,7 @@ public async Task Node_Guid_Id_From_QueryString_With_Permission_Is_Authorized() public async Task Node_Guid_Id_From_QueryString_Without_Permission_Is_Not_Authorized() { var authHandlerContext = CreateAuthorizationHandlerContext(); - var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: s_nodeGuid.ToString()); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: s_nodeGuid.ToString()); var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId, 1001); await sut.HandleAsync(authHandlerContext); @@ -124,7 +136,7 @@ public async Task Node_Guid_Id_From_QueryString_Without_Permission_Is_Not_Author public async Task Node_Invalid_Id_From_QueryString_Is_Authorized() { var authHandlerContext = CreateAuthorizationHandlerContext(); - var mockHttpContextAccessor = CreateMockHttpContextAccessor(queryStringValue: "invalid"); + var mockHttpContextAccessor = CreateMockHttpContextAccessorWithQueryStringValue(queryStringValue: "invalid"); var sut = CreateHandler(mockHttpContextAccessor.Object, NodeId); await sut.HandleAsync(authHandlerContext); @@ -140,21 +152,35 @@ private static AuthorizationHandlerContext CreateAuthorizationHandlerContext() return new AuthorizationHandlerContext(new List<IAuthorizationRequirement> { requirement }, user, resource); } - private static Mock<IHttpContextAccessor> CreateMockHttpContextAccessor( + private static Mock<IHttpContextAccessor> CreateMockHttpContextAccessorWithQueryStringValue( string queryStringName = QueryStringName, string queryStringValue = "") + => CreateMockHttpContextAccessorWithQueryStringValues(queryStringName, new[] { queryStringValue }); + + private static Mock<IHttpContextAccessor> CreateMockHttpContextAccessorWithQueryStringValues( + string queryStringName = QueryStringName, + string[]? queryStringValues = null) { + queryStringValues ??= Array.Empty<string>(); + var mockHttpContextAccessor = new Mock<IHttpContextAccessor>(); var mockHttpContext = new Mock<HttpContext>(); var mockHttpRequest = new Mock<HttpRequest>(); - var queryParams = new Dictionary<string, StringValues> { { queryStringName, queryStringValue } }; + var queryParams = new Dictionary<string, StringValues> { { queryStringName, new StringValues(queryStringValues) } }; mockHttpRequest.SetupGet(x => x.Query).Returns(new QueryCollection(queryParams)); mockHttpContext.SetupGet(x => x.Request).Returns(mockHttpRequest.Object); mockHttpContext.SetupGet(x => x.Items).Returns(new Dictionary<object, object>()); mockHttpContextAccessor.SetupGet(x => x.HttpContext).Returns(mockHttpContext.Object); return mockHttpContextAccessor; } + /// <summary> + /// + /// </summary> + /// <param name="httpContextAccessor"></param> + /// <param name="nodeId"></param> + /// <param name="startMediaId">the startMediaId of the user being setup</param> + /// <returns></returns> private MediaPermissionsQueryStringHandler CreateHandler( IHttpContextAccessor httpContextAccessor, int nodeId, @@ -179,7 +205,7 @@ private static Mock<IEntityService> CreateMockEntityService() mockEntityService .Setup(x => x.GetId( It.Is<Guid>(y => y == s_nodeGuid), - It.Is<UmbracoObjectTypes>(y => y == UmbracoObjectTypes.Document))) + It.Is<UmbracoObjectTypes>(y => y == UmbracoObjectTypes.Media))) .Returns(Attempt<int>.Succeed(NodeId)); return mockEntityService; }
tests/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Authorization/MediaPermissionsResourceHandlerTests.cs+1 −1 modified@@ -44,7 +44,7 @@ public async Task Resource_With_Media_With_Permission_Is_Authorized() } [Test] - public async Task Resource_With_Node_Id_Withou_Permission_Is_Not_Authorized() + public async Task Resource_With_Node_Id_Without_Permission_Is_Not_Authorized() { var authHandlerContext = CreateAuthorizationHandlerContext(NodeId, true); var sut = CreateHandler(NodeId, 1001);
version.json+1 −1 modified@@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "10.8.8", + "version": "10.8.9", "assemblyVersion": { "precision": "build" },
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-wx5h-wqfq-v698ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-27602ghsaADVISORY
- github.com/umbraco/Umbraco-CMS/commit/5b54bed406682ceff57903bf7d3c57814eef31a7ghsax_refsource_MISCWEB
- github.com/umbraco/Umbraco-CMS/commit/7888b9a4ce5ae7f9bda7ff3bb705b8fcd2f1675dghsax_refsource_MISCWEB
- github.com/umbraco/Umbraco-CMS/security/advisories/GHSA-wx5h-wqfq-v698ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.