Headline
CVE-2021-25976: Merge pull request #1742 from PiranhaCMS/features/manager-security-up… · PiranhaCMS/piranha.core@e42abac
In PiranhaCMS, versions 4.0.0-alpha1 to 9.2.0 are vulnerable to cross-site request forgery (CSRF) when performing various actions supported by the management system, such as deleting a user, deleting a role, editing a post, deleting a media folder etc., when an ID is known.
Permalink
Browse files
Merge pull request #1742 from PiranhaCMS/features/manager-security-up…
…date
Updated manager security. Fixes #1741
- Loading branch information
Showing with 1,051 additions and 630 deletions.
- +2 −3 core/Piranha.Manager.LocalAuth/Areas/Manager/Pages/Login.cshtml.cs
- +1 −1 core/Piranha.Manager/Areas/Manager/Pages/Index.cshtml.cs
- +0 −1 core/Piranha.Manager/Areas/Manager/Shared/Partial/_ContentPickerModal.cshtml
- +0 −1 core/Piranha.Manager/Areas/Manager/Shared/Partial/_MediaPickerModal.cshtml
- +0 −1 core/Piranha.Manager/Areas/Manager/Shared/Partial/_PagePickerModal.cshtml
- +0 −1 core/Piranha.Manager/Areas/Manager/Shared/Partial/_PostPickerModal.cshtml
- +0 −1 core/Piranha.Manager/Areas/Manager/Shared/Partial/_PreviewModal.cshtml
- +6 −1 core/Piranha.Manager/Areas/Manager/Shared/_Layout.cshtml
- +1 −0 core/Piranha.Manager/Controllers/AliasApiController.cs
- +55 −0 core/Piranha.Manager/Controllers/AuthController.cs
- +21 −14 core/Piranha.Manager/Controllers/CommentApiController.cs
- +1 −0 core/Piranha.Manager/Controllers/ConfigApiController.cs
- +4 −3 core/Piranha.Manager/Controllers/ContentApiController.cs
- +51 −5 core/Piranha.Manager/Controllers/LanguageApiController.cs
- +5 −4 core/Piranha.Manager/Controllers/MediaApiController.cs
- +1 −0 core/Piranha.Manager/Controllers/ModuleApiController.cs
- +10 −9 core/Piranha.Manager/Controllers/PageApiController.cs
- +1 −0 core/Piranha.Manager/Controllers/PermissionApiController.cs
- +7 −6 core/Piranha.Manager/Controllers/PostApiController.cs
- +1 −0 core/Piranha.Manager/Controllers/SiteApiController.cs
- +166 −166 core/Piranha.Manager/{ → Extensions}/ManagerExtensions.cs
- +23 −6 core/Piranha.Manager/{ → Extensions}/ManagerStartupExtensions.cs
- +36 −0 core/Piranha.Manager/ManagerOptions.cs
- +5 −0 core/Piranha.Manager/Models/LanguageEditModel.cs
- +5 −5 core/Piranha.Manager/assets/dist/css/full.min.css
- +5 −5 core/Piranha.Manager/assets/dist/css/slim.min.css
- +43 −36 core/Piranha.Manager/assets/dist/js/piranha.alias.js
- +1 −1 core/Piranha.Manager/assets/dist/js/piranha.alias.min.js
- +53 −29 core/Piranha.Manager/assets/dist/js/piranha.comment.js
- +1 −1 core/Piranha.Manager/assets/dist/js/piranha.comment.min.js
- +5 −1 core/Piranha.Manager/assets/dist/js/piranha.components.js
- +1 −1 core/Piranha.Manager/assets/dist/js/piranha.components.min.js
- +8 −5 core/Piranha.Manager/assets/dist/js/piranha.config.js
- +1 −1 core/Piranha.Manager/assets/dist/js/piranha.config.min.js
- +6 −4 core/Piranha.Manager/assets/dist/js/piranha.contentedit.js
- +1 −1 core/Piranha.Manager/assets/dist/js/piranha.contentedit.min.js
- +5 −1 core/Piranha.Manager/assets/dist/js/piranha.contentlist.js
- +1 −1 core/Piranha.Manager/assets/dist/js/piranha.contentlist.min.js
- +61 −23 core/Piranha.Manager/assets/dist/js/piranha.js
- +52 −31 core/Piranha.Manager/assets/dist/js/piranha.media.js
- +1 −1 core/Piranha.Manager/assets/dist/js/piranha.media.min.js
- +1 −1 core/Piranha.Manager/assets/dist/js/piranha.min.js
- +30 −19 core/Piranha.Manager/assets/dist/js/piranha.pageedit.js
- +1 −1 core/Piranha.Manager/assets/dist/js/piranha.pageedit.min.js
- +6 −4 core/Piranha.Manager/assets/dist/js/piranha.pagelist.js
- +1 −1 core/Piranha.Manager/assets/dist/js/piranha.pagelist.min.js
- +29 −22 core/Piranha.Manager/assets/dist/js/piranha.postedit.js
- +1 −1 core/Piranha.Manager/assets/dist/js/piranha.postedit.min.js
- +10 −10 core/Piranha.Manager/assets/dist/js/piranha.siteedit.js
- +1 −1 core/Piranha.Manager/assets/dist/js/piranha.siteedit.min.js
- +5 −1 core/Piranha.Manager/assets/src/js/components/post-archive.vue
- +43 −36 core/Piranha.Manager/assets/src/js/piranha.alias.js
- +53 −29 core/Piranha.Manager/assets/src/js/piranha.comment.js
- +8 −5 core/Piranha.Manager/assets/src/js/piranha.config.js
- +6 −4 core/Piranha.Manager/assets/src/js/piranha.contentedit.js
- +5 −1 core/Piranha.Manager/assets/src/js/piranha.contentlist.js
- +1 −0 core/Piranha.Manager/assets/src/js/piranha.dropzone.js
- +23 −17 core/Piranha.Manager/assets/src/js/piranha.languageedit.js
- +52 −31 core/Piranha.Manager/assets/src/js/piranha.media.js
- +8 −5 core/Piranha.Manager/assets/src/js/piranha.mediapicker.js
- +7 −0 core/Piranha.Manager/assets/src/js/piranha.notifications.js
- +30 −19 core/Piranha.Manager/assets/src/js/piranha.pageedit.js
- +6 −4 core/Piranha.Manager/assets/src/js/piranha.pagelist.js
- +29 −22 core/Piranha.Manager/assets/src/js/piranha.postedit.js
- +10 −10 core/Piranha.Manager/assets/src/js/piranha.siteedit.js
- +22 −1 core/Piranha.Manager/assets/src/js/piranha.utils.js
- +3 −1 core/Piranha.Manager/assets/src/scss/inc/_actions.scss
- +1 −1 core/Piranha.Manager/assets/src/scss/inc/_notifications.scss
- +6 −3 identity/Piranha.AspNetCore.Identity/Areas/Manager/Views/Role/List.cshtml
- +2 −1 identity/Piranha.AspNetCore.Identity/Controllers/RoleController.cs
- +1 −0 identity/Piranha.AspNetCore.Identity/Controllers/UserController.cs
- +2 −6 identity/Piranha.AspNetCore.Identity/assets/piranha.useredit.js
- +1 −3 identity/Piranha.AspNetCore.Identity/assets/piranha.userlist.js
@@ -104,10 +104,9 @@ public async Task<IActionResult> OnPostAsync(string returnUrl = null)
if (!string.IsNullOrEmpty(returnUrl))
{
return LocalRedirect(returnUrl);
return LocalRedirect($"~/manager/login/auth?returnUrl={ returnUrl }");
}
return new RedirectToPageResult(“Index”);
return LocalRedirect(“~/manager/login/auth”);
}
}
}
@@ -24,7 +24,7 @@ public IndexModel(IAuthorizationService service)
{
_service = service;
}
public async Task<IActionResult> OnGet()
public async Task<IActionResult> OnGet(string returnUrl = null)
{
var items = await Menu.Items.GetForUser(HttpContext.User, _service);
@@ -1,6 +1,5 @@
@inject ManagerLocalizer Localizer
<!-- The Modal -->
<div class="modal modal-panel fade" id="contentpicker">
<div class="modal-dialog modal-lg">
<div class="modal-content">
@@ -1,7 +1,6 @@
@inject IAuthorizationService Auth
@inject ManagerLocalizer Localizer
<!-- The Modal -->
<div class="modal modal-panel fade" id="mediapicker">
<div class="modal-dialog modal-lg">
<div class="modal-content">
@@ -1,6 +1,5 @@
@inject ManagerLocalizer Localizer
<!-- The Modal -->
<div class="modal modal-panel fade" id="pagepicker">
<div class="modal-dialog modal-lg">
<div class="modal-content">
@@ -1,6 +1,5 @@
@inject ManagerLocalizer Localizer
<!-- The Modal -->
<div class="modal modal-panel fade" id="postpicker">
<div class="modal-dialog modal-lg">
<div class="modal-content">
@@ -1,7 +1,6 @@
@inject IAuthorizationService Auth
@inject ManagerLocalizer Localizer
<!-- The Modal -->
<div class="modal fade" id="previewModal">
<div class="modal-dialog modal-lg">
<div class="modal-content">
@@ -1,4 +1,5 @@
@{
@inject Microsoft.Extensions.Options.IOptions<ManagerOptions> Options
@{
var module = Piranha.App.Modules.Get<Piranha.Manager.Module>();
var prerelease = Piranha.Utils.IsPreRelease(typeof(Piranha.Manager.Module).Assembly) ? “pre-release” : "";
var isRightToLeft = @System.Globalization.CultureInfo.CurrentCulture.TextInfo.IsRightToLeft;
@@ -53,6 +54,10 @@
var piranha = {};
window.piranha = piranha;
piranha.baseUrl = "@Url.Content(“~/”)";
piranha.antiForgery = {
cookieName: "@Options.Value.XsrfCookieName",
headerName: “@Options.Value.XsrfHeaderName”
};
</script>
<partial name="~/Areas/Manager/Shared/Partial/_EditorConfig.cshtml" />
@@ -25,6 +25,7 @@ namespace Piranha.Manager.Controllers
[Route(“manager/api/alias”)]
[Authorize(Policy = Permission.Admin)]
[ApiController]
[AutoValidateAntiforgeryToken]
public class AliasApiController : Controller
{
private readonly IApi _api;
@@ -0,0 +1,55 @@
/*
* Copyright © .NET Foundation and Contributors
*
* This software may be modified and distributed under the terms
* of the MIT license. See the LICENSE file for details.
*
* https://github.com/piranhacms/piranha.core
*
*/
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
namespace Piranha.Manager.Controllers
{
[Area(“Manager”)]
[Route(“manager/login/auth”)]
[Authorize(Policy = Permission.Admin)]
[ApiController]
public sealed class AuthController : Controller
{
private readonly IAntiforgery _antiForgery;
private readonly ManagerOptions _options;
/// <summary>
/// Default constructor.
/// </summary>
/// <param name="antiforgery">The antiforgery service</param>
/// <param name="options">The manager options</param>
public AuthController(IAntiforgery antiforgery, IOptions<ManagerOptions> options)
{
_antiForgery = antiforgery;
_options = options.Value;
}
[Route(“{returnUrl?}”)]
[HttpGet]
public IActionResult SetAuthCookie(string returnUrl = null)
{
var tokens = _antiForgery.GetAndStoreTokens(HttpContext);
Response.Cookies.Append(_options.XsrfCookieName, tokens.RequestToken, new CookieOptions
{
HttpOnly = false
});
if (!string.IsNullOrEmpty(returnUrl))
{
return LocalRedirect(returnUrl);
}
return LocalRedirect(“~/manager”);
}
}
}
@@ -24,8 +24,15 @@ namespace Piranha.Manager.Controllers
[Route(“manager/api/comment”)]
[Authorize(Policy = Permission.Admin)]
[ApiController]
[AutoValidateAntiforgeryToken]
public class CommentApiController : Controller
{
public class ApprovalModel
{
public Guid Id { get; set; }
public Guid? ParentId { get; set; }
}
private readonly CommentService _service;
private readonly ManagerLocalizer _localizer;
@@ -50,14 +57,14 @@ public Task<CommentListModel> List(Guid? id = null)
return _service.Get(id);
}
[Route(“approve/{id}/{parentId?}”)]
[HttpGet]
[Route(“approve”)]
[HttpPost]
[Authorize(Policy = Permission.CommentsApprove)]
public async Task<CommentListModel> Approve(Guid id, Guid? parentId = null)
public async Task<CommentListModel> Approve(ApprovalModel model)
{
await _service.ApproveAsync(id);
await _service.ApproveAsync(model.Id).ConfigureAwait(false);
var result = await List(parentId);
var result = await List(model.ParentId).ConfigureAwait(false);
result.Status = new StatusMessage
{
@@ -67,14 +74,14 @@ public async Task<CommentListModel> Approve(Guid id, Guid? parentId = null)
return result;
}
[Route(“unapprove/{id}/{parentId?}”)]
[HttpGet]
[Route(“unapprove”)]
[HttpPost]
[Authorize(Policy = Permission.CommentsApprove)]
public async Task<CommentListModel> UnApprove(Guid id, Guid? parentId = null)
public async Task<CommentListModel> UnApprove(ApprovalModel model)
{
await _service.UnApproveAsync(id);
await _service.UnApproveAsync(model.Id).ConfigureAwait(false);
var result = await List(parentId);
var result = await List(model.ParentId).ConfigureAwait(false);
result.Status = new StatusMessage
{
@@ -84,12 +91,12 @@ public async Task<CommentListModel> UnApprove(Guid id, Guid? parentId = null)
return result;
}
[Route(“delete/{id}”)]
[HttpGet]
[Route(“delete”)]
[HttpDelete]
[Authorize(Policy = Permission.CommentsDelete)]
public async Task<StatusMessage> Delete(Guid id)
public async Task<StatusMessage> Delete([FromBody]Guid id)
{
await _service.DeleteAsync(id);
await _service.DeleteAsync(id).ConfigureAwait(false);
var result = new StatusMessage
{
@@ -22,6 +22,7 @@ namespace Piranha.Manager.Controllers
[Route(“manager/api/config”)]
[Authorize(Policy = Permission.Admin)]
[ApiController]
[AutoValidateAntiforgeryToken]
public class ConfigApiController : Controller
{
private readonly ConfigService _service;
@@ -25,6 +25,7 @@ namespace Piranha.Manager.Controllers
[Route(“manager/api/content”)]
[Authorize(Policy = Permission.Admin)]
[ApiController]
[AutoValidateAntiforgeryToken]
public class ContentApiController : Controller
{
private readonly IApi _api;
@@ -228,10 +229,10 @@ public async Task<ContentEditModel> Save(ContentEditModel model)
/// </summary>
/// <param name="id">The unique id</param>
/// <returns>The result of the operation</returns>
[Route(“delete/{id}”)]
[HttpGet]
[Route(“delete”)]
[HttpDelete]
[Authorize(Policy = Permission.ContentDelete)]
public async Task<StatusMessage> Delete(Guid id)
public async Task<StatusMessage> Delete([FromBody]Guid id)
{
try
{
@@ -14,7 +14,6 @@
using Microsoft.AspNetCore.Mvc;
using Piranha.Manager.Models;
using Piranha.Manager.Services;
using Piranha.Models;
namespace Piranha.Manager.Controllers
{
@@ -25,6 +24,7 @@ namespace Piranha.Manager.Controllers
[Route(“manager/api/language”)]
[Authorize(Policy = Permission.Admin)]
[ApiController]
[AutoValidateAntiforgeryToken]
public class LanguageApiController : Controller
{
private readonly LanguageService _service;
@@ -56,16 +56,62 @@ public async Task<LanguageEditModel> Get()
/// <param name="model">The model</param>
[Route(“”)]
[HttpPost]
public async Task<LanguageEditModel> Save(LanguageEditModel model)
public async Task<IActionResult> Save(LanguageEditModel model)
{
return await _service.Save(model);
try
{
var result = await _service.Save(model);
result.Status = new StatusMessage
{
Type = StatusMessage.Success,
Body = _localizer.Language[“The language was successfully saved”]
};
return Ok(result);
}
catch (Exception e)
{
var result = new LanguageEditModel();
result.Status = new StatusMessage
{
Type = StatusMessage.Error,
Body = e.Message
};
return BadRequest(result);
}
}
/// <summary>
/// Deletes the language with the given id.
/// </summary>
/// <param name="id">The unique id</param>
[Route(“{id}”)]
[HttpDelete]
public async Task<LanguageEditModel> Delete(Guid id)
public async Task<IActionResult> Delete(Guid id)
{
return await _service.Delete(id);
try
{
var result = await _service.Delete(id);
result.Status = new StatusMessage
{
Type = StatusMessage.Success,
Body = _localizer.Language[“The language was successfully deleted”]
};
return Ok(result);
}
catch (Exception e)
{
var result = new LanguageEditModel();
result.Status = new StatusMessage
{
Type = StatusMessage.Error,
Body = e.Message
};
return BadRequest(result);
}
}
}
}
@@ -28,6 +28,7 @@ namespace Piranha.Manager.Controllers
[Route(“manager/api/media”)]
[Authorize(Policy = Permission.Admin)]
[ApiController]
[AutoValidateAntiforgeryToken]
public class MediaApiController : Controller
{
private readonly MediaService _service;
@@ -153,10 +154,10 @@ public async Task<IActionResult> SaveFolder(MediaFolderModel model, MediaType? f
}
}
[Route(“folder/delete/{id:Guid}”)]
[HttpGet]
[Route(“folder/delete”)]
[HttpDelete]
[Authorize(Policy = Permission.MediaDeleteFolder)]
public async Task<IActionResult> DeleteFolder(Guid id)
public async Task<IActionResult> DeleteFolder([FromBody]Guid id)
{
try
{
@@ -299,7 +300,7 @@ public async Task<IActionResult> Move([FromBody] IEnumerable<Guid> items, Guid?
}
[Route(“delete”)]
[HttpPost]
[HttpDelete]
[Consumes(“application/json”)]
[Authorize(Policy = Permission.MediaDelete)]
public async Task<IActionResult> Delete([FromBody] IEnumerable<Guid> items)
@@ -23,6 +23,7 @@ namespace Piranha.Manager.Controllers
[Route(“manager/api/module”)]
[Authorize(Policy = Permission.Admin)]
[ApiController]
[AutoValidateAntiforgeryToken]
public class ModuleApiController : Controller
{
private readonly ModuleService _service;