Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.ViewModels.Document;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Actions;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Models.ContentPublishing;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Security.Authorization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Web.Common.Authorization;
using Umbraco.Extensions;

namespace Umbraco.Cms.Api.Management.Controllers.Document;

[ApiVersion("1.0")]
public class CreateAndPublishDocumentController : DocumentControllerBase
{
private readonly IAuthorizationService _authorizationService;
private readonly IDocumentEditingPresentationFactory _documentEditingPresentationFactory;
private readonly IContentEditingService _contentEditingService;
private readonly IContentPublishingService _contentPublishingService;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;

public CreateAndPublishDocumentController(
IAuthorizationService authorizationService,
IDocumentEditingPresentationFactory documentEditingPresentationFactory,
IContentEditingService contentEditingService,
IContentPublishingService contentPublishingService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
{
_authorizationService = authorizationService;
_documentEditingPresentationFactory = documentEditingPresentationFactory;
_contentEditingService = contentEditingService;
_contentPublishingService = contentPublishingService;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
}

[HttpPost("create-and-publish")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> CreateAndPublish(

Check failure

Code scanning / CodeQL

Missing cross-site request forgery token validation High

Method 'CreateAndPublish' handles a POST request without performing CSRF token validation.

Copilot Autofix

AI 6 months ago

In general, the fix is to ensure that any state-changing POST action that can be called from a browser with cookie-based authentication validates an anti-forgery token. In ASP.NET Core MVC/Web API controllers using [ApiController]-style patterns, the common approach is to add [ValidateAntiForgeryToken] (or the ASP.NET Core equivalent [AutoValidateAntiforgeryToken] at a broader scope) on POST actions, and ensure clients send the token with their requests.

For this specific controller, the minimal, non-breaking change is to decorate the CreateAndPublish POST action with the anti-forgery validation attribute while leaving the rest of the logic untouched. The project already uses Microsoft.AspNetCore.Mvc, which defines ValidateAntiForgeryTokenAttribute, so no new imports are needed. Concretely, in src/Umbraco.Cms.Api.Management/Controllers/Document/CreateAndPublishDocumentController.cs, just above the CreateAndPublish method (around line 48), add [ValidateAntiForgeryToken] alongside the existing attributes ([HttpPost("create-and-publish")], [MapToApiVersion("1.0")], etc.). This will cause ASP.NET Core’s anti-forgery system to validate the token whenever this method is invoked via an HTTP POST, reusing the existing infrastructure without changing the method body or signatures.

Suggested changeset 1
src/Umbraco.Cms.Api.Management/Controllers/Document/CreateAndPublishDocumentController.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/CreateAndPublishDocumentController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/CreateAndPublishDocumentController.cs
--- a/src/Umbraco.Cms.Api.Management/Controllers/Document/CreateAndPublishDocumentController.cs
+++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/CreateAndPublishDocumentController.cs
@@ -41,6 +41,7 @@
     }
 
     [HttpPost("create-and-publish")]
+    [ValidateAntiForgeryToken]
     [MapToApiVersion("1.0")]
     [ProducesResponseType(StatusCodes.Status201Created)]
     [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
EOF
@@ -41,6 +41,7 @@
}

[HttpPost("create-and-publish")]
[ValidateAntiForgeryToken]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
Copilot is powered by AI and may make mistakes. Always verify output.
CancellationToken cancellationToken,
CreateAndPublishDocumentRequestModel requestModel)
{
// Authorize both create and publish permissions upfront.
AuthorizationResult createAuthorizationResult = await _authorizationService.AuthorizeResourceAsync(
User,
ContentPermissionResource.WithKeys(ActionNew.ActionLetter, requestModel.Parent?.Id),
AuthorizationPolicies.ContentPermissionByResource);

AuthorizationResult publishAuthorizationResult = await _authorizationService.AuthorizeResourceAsync(
User,
ContentPermissionResource.WithKeys(ActionPublish.ActionLetter, requestModel.Parent?.Id, requestModel.Cultures.OfType<string>()),
AuthorizationPolicies.ContentPermissionByResource);

if (createAuthorizationResult.Succeeded is false || publishAuthorizationResult.Succeeded is false)
{
return Forbidden();
}

// Create the document.
ContentCreateModel createModel = _documentEditingPresentationFactory.MapCreateModel(requestModel);
Attempt<ContentCreateResult, ContentEditingOperationStatus> createResult =
await _contentEditingService.CreateAsync(createModel, CurrentUserKey(_backOfficeSecurityAccessor));

if (createResult.Success is false)
{
return ContentEditingOperationStatusResult(createResult.Status);
}

// If create had validation errors, don't attempt to publish - it will fail.
if (createResult.Status == ContentEditingOperationStatus.PropertyValidationError)
{
return DocumentPublishingOperationStatusResult(
ContentPublishingOperationStatus.ContentInvalid,
invalidPropertyAliases: createResult.Result.ValidationResult.ValidationErrors.Select(e => e.Alias));
}

// Build immediate publish model (no schedule).
IList<CulturePublishScheduleModel> culturePublishSchedules = GetImmediateCulturePublishSchedule(requestModel.Cultures);

// Publish the document immediately using the already-loaded content.
// Skip validation since create succeeded with no validation errors.
Attempt<ContentPublishingResult, ContentPublishingOperationStatus> publishResult =
await _contentPublishingService.PublishAsync(
createResult.Result.Content!,
culturePublishSchedules,
CurrentUserKey(_backOfficeSecurityAccessor),
skipValidation: true);

if (publishResult.Success is false)
{
return DocumentPublishingOperationStatusResult(publishResult.Status, invalidPropertyAliases: publishResult.Result.InvalidPropertyAliases);
}

return CreatedAtId<ByKeyDocumentController>(controller => nameof(controller.ByKey), createResult.Result.Content!.Key);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -194,4 +194,9 @@ protected IActionResult ContentQueryOperationStatusResult(ContentQueryOperationS
.WithTitle("Unknown content query status.")
.Build()),
});

protected static IList<CulturePublishScheduleModel> GetImmediateCulturePublishSchedule(IEnumerable<string?> cultures) =>
cultures
.Select(culture => new CulturePublishScheduleModel { Culture = culture })
.ToList();
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.Security.Authorization.Content;
using Umbraco.Cms.Api.Management.ViewModels.Document;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Actions;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.ViewModels.Document;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Actions;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Models.ContentPublishing;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Security.Authorization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Services.OperationStatus;
using Umbraco.Cms.Web.Common.Authorization;
using Umbraco.Extensions;

namespace Umbraco.Cms.Api.Management.Controllers.Document;

[ApiVersion("1.0")]
public class UpdateAndPublishDocumentController : DocumentControllerBase
{
private readonly IAuthorizationService _authorizationService;
private readonly IDocumentEditingPresentationFactory _documentEditingPresentationFactory;
private readonly IContentEditingService _contentEditingService;
private readonly IContentPublishingService _contentPublishingService;
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;

public UpdateAndPublishDocumentController(
IAuthorizationService authorizationService,
IDocumentEditingPresentationFactory documentEditingPresentationFactory,
IContentEditingService contentEditingService,
IContentPublishingService contentPublishingService,
IBackOfficeSecurityAccessor backOfficeSecurityAccessor)
{
_authorizationService = authorizationService;
_documentEditingPresentationFactory = documentEditingPresentationFactory;
_contentEditingService = contentEditingService;
_contentPublishingService = contentPublishingService;
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
}

[HttpPut("{id:guid}/update-and-publish")]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<IActionResult> UpdateAndPublish(
CancellationToken cancellationToken,
Guid id,
UpdateAndPublishDocumentRequestModel requestModel)
{
// Authorize both update and publish permissions upfront.
AuthorizationResult updateAuthorizationResult = await _authorizationService.AuthorizeResourceAsync(
User,
ContentPermissionResource.WithKeys(ActionUpdate.ActionLetter, id),
AuthorizationPolicies.ContentPermissionByResource);

AuthorizationResult publishAuthorizationResult = await _authorizationService.AuthorizeResourceAsync(
User,
ContentPermissionResource.WithKeys(ActionPublish.ActionLetter, id, requestModel.Cultures.OfType<string>()),
AuthorizationPolicies.ContentPermissionByResource);

if (updateAuthorizationResult.Succeeded is false || publishAuthorizationResult.Succeeded is false)
{
return Forbidden();
}

// Update the document.
ContentUpdateModel updateModel = _documentEditingPresentationFactory.MapUpdateModel(requestModel);
Attempt<ContentUpdateResult, ContentEditingOperationStatus> updateResult =
await _contentEditingService.UpdateAsync(id, updateModel, CurrentUserKey(_backOfficeSecurityAccessor));

if (updateResult.Success is false)
{
return ContentEditingOperationStatusResult(updateResult.Status);
}

// If update had validation errors, don't attempt to publish - it will fail.
if (updateResult.Status == ContentEditingOperationStatus.PropertyValidationError)
{
return DocumentPublishingOperationStatusResult(
ContentPublishingOperationStatus.ContentInvalid,
invalidPropertyAliases: updateResult.Result.ValidationResult.ValidationErrors.Select(e => e.Alias));
}

// Build immediate publish model (no schedule).
IList<CulturePublishScheduleModel> culturePublishSchedules = GetImmediateCulturePublishSchedule(requestModel.Cultures);

// Publish the document immediately using the already-loaded content.
// Skip validation since update succeeded with no validation errors.
Attempt<ContentPublishingResult, ContentPublishingOperationStatus> publishResult =
await _contentPublishingService.PublishAsync(
updateResult.Result.Content!,
culturePublishSchedules,
CurrentUserKey(_backOfficeSecurityAccessor),
skipValidation: true);

if (publishResult.Success is false)
{
return DocumentPublishingOperationStatusResult(publishResult.Status, invalidPropertyAliases: publishResult.Result.InvalidPropertyAliases);
}

return Ok();
}
}
Loading
Loading