CVE-2022-0274: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: Cross-site Scripting (XSS) - Stored in NuGet OrchardCore.Application.Cms.Targets prior to 1.2.2.
CWE description: The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
CVSS v3 base score: 5.4
Published: 2022-01-19T18:15Z
Repository: https://github.com/orchardcms/orchardcore
Language: C#
Files changed: src/OrchardCore.Modules/OrchardCore.ContentFields/Drivers/LinkFieldDisplayDriver.cs, src/OrchardCore.Modules/OrchardCore.Contents/AuditTrail/Controllers/AuditTrailContentController.cs, src/OrchardCore.Modules/OrchardCore.Menu/Drivers/HtmlMenuItemPartDisplayDriver.cs, src/OrchardCore.Modules/OrchardCore.Menu/Drivers/LinkMenuItemPartDisplayDriver.cs, src/OrchardCore.Modules/OrchardCore.Users/Workflows/Activities/RegisterUserTask.cs, src/OrchardCore.Modules/OrchardCore.Workflows/Activities/NotifyTask.cs, src/OrchardCore.Modules/OrchardCore.Workflows/Views/Items/UserTaskEvent.Fields.Design.cshtml, src/OrchardCore.Modules/OrchardCore.Workflows/Views/Workflow/Details.cshtml, src/OrchardCore.Modules/OrchardCore.Workflows/Views/Workflow/Index.cshtml, src/OrchardCore/OrchardCore.Workflows.Abstractions/Helpers/ActivityExtensions.cs
Commit date: 2022-01-18 11:03:05 -0800
Commit message: Fix localization and sanitization usages (#11034)


Diff: diff --git a/src/OrchardCore.Modules/OrchardCore.ContentFields/Drivers/LinkFieldDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.ContentFields/Drivers/LinkFieldDisplayDriver.cs
index e31df434b..cf8e9d2cb 100644
--- a/src/OrchardCore.Modules/OrchardCore.ContentFields/Drivers/LinkFieldDisplayDriver.cs
+++ b/src/OrchardCore.Modules/OrchardCore.ContentFields/Drivers/LinkFieldDisplayDriver.cs
@@ -1,6 +1,7 @@
 using System;
+using System.Text.Encodings.Web;
 using System.Threading.Tasks;
 using Microsoft.AspNetCore.Mvc.Infrastructure;
 using Microsoft.AspNetCore.Mvc.Routing;
 using Microsoft.Extensions.Localization;
 using OrchardCore.ContentFields.Fields;
@@ -9,28 +10,35 @@ using OrchardCore.ContentFields.ViewModels;
 using OrchardCore.ContentManagement.Display.ContentDisplay;
 using OrchardCore.ContentManagement.Display.Models;
 using OrchardCore.ContentManagement.Metadata.Models;
 using OrchardCore.DisplayManagement.ModelBinding;
 using OrchardCore.DisplayManagement.Views;
+using OrchardCore.Infrastructure.Html;
 using OrchardCore.Mvc.ModelBinding;
 
 namespace OrchardCore.ContentFields.Drivers
 {
     public class LinkFieldDisplayDriver : ContentFieldDisplayDriver<LinkField>
     {
         private readonly IUrlHelperFactory _urlHelperFactory;
         private readonly IActionContextAccessor _actionContextAccessor;
         private readonly IStringLocalizer S;
+        private readonly IHtmlSanitizerService _htmlSanitizerService;
+        private readonly HtmlEncoder _htmlencoder;
 
         public LinkFieldDisplayDriver(
             IUrlHelperFactory urlHelperFactory,
             IActionContextAccessor actionContextAccessor,
-            IStringLocalizer<LinkFieldDisplayDriver> localizer)
+            IStringLocalizer<LinkFieldDisplayDriver> localizer,
+            IHtmlSanitizerService htmlSanitizerService,
+            HtmlEncoder htmlencoder)
         {
             _urlHelperFactory = urlHelperFactory;
             _actionContextAccessor = actionContextAccessor;
             S = localizer;
+            _htmlSanitizerService = htmlSanitizerService;
+            _htmlencoder = htmlencoder;
         }
 
         public override IDisplayResult Display(LinkField field, BuildFieldDisplayContext context)
         {
             return Initialize<DisplayLinkFieldViewModel>(GetDisplayShapeType(context), model =>
@@ -89,23 +97,29 @@ namespace OrchardCore.ContentFields.Drivers
                 }
                 else if (!String.IsNullOrWhiteSpace(field.Url) && !Uri.IsWellFormedUriString(urlToValidate, UriKind.RelativeOrAbsolute))
                 {
                     updater.ModelState.AddModelError(Prefix, nameof(field.Url), S["{0} is an invalid url.", field.Url]);
                 }
+                else
+                {
+                    var link = $"<a href=\"{_htmlencoder.Encode(urlToValidate)}\"></a>";
+
+                    if (!String.Equals(link, _htmlSanitizerService.Sanitize(link), StringComparison.OrdinalIgnoreCase))
+                    {
+                        updater.ModelState.AddModelError(Prefix, nameof(field.Url), S["{0} is an invalid url.", field.Url]);
+                    }
+                }
 
                 // Validate Text
                 if (settings.LinkTextMode == LinkTextMode.Required && String.IsNullOrWhiteSpace(field.Text))
                 {
                     updater.ModelState.AddModelError(Prefix, nameof(field.Text), S["The link text is required for {0}.", context.PartFieldDefinition.DisplayName()]);
                 }
                 else if (settings.LinkTextMode == LinkTextMode.Static && String.IsNullOrWhiteSpace(settings.DefaultText))
                 {
                     updater.ModelState.AddModelError(Prefix, nameof(field.Text), S["The text default value is required for {0}.", context.PartFieldDefinition.DisplayName()]);
                 }
-
-                // Run this through a sanitizer in case someone puts html in it.
-                // No settings.
             }
 
             return Edit(field, context);
         }
     }
diff --git a/src/OrchardCore.Modules/OrchardCore.Contents/AuditTrail/Controllers/AuditTrailContentController.cs b/src/OrchardCore.Modules/OrchardCore.Contents/AuditTrail/Controllers/AuditTrailContentController.cs
index 582f47b8e..88aff43c6 100644
--- a/src/OrchardCore.Modules/OrchardCore.Contents/AuditTrail/Controllers/AuditTrailContentController.cs
+++ b/src/OrchardCore.Modules/OrchardCore.Contents/AuditTrail/Controllers/AuditTrailContentController.cs
@@ -109,16 +109,19 @@ namespace OrchardCore.Contents.AuditTrail.Controllers
             {
                 return Forbid();
             }
 
             var result = await _contentManager.RestoreAsync(contentItem);
+
             if (!result.Succeeded)
             {
                 await _notifier.WarningAsync(H["'{0}' was not restored, the version is not valid.", contentItem.DisplayText]);
+
                 foreach (var error in result.Errors)
                 {
-                    await _notifier.WarningAsync(new LocalizedHtmlString(error.ErrorMessage, error.ErrorMessage));
+                    // Pass ErrorMessage as an argument to ensure it is encoded
+                    await _notifier.WarningAsync(new LocalizedHtmlString(nameof(AuditTrailContentController.Restore), "{0}", false, error.ErrorMessage));
                 }
 
                 return RedirectToAction("Index", "Admin", new { area = "OrchardCore.AuditTrail" });
             }
 
diff --git a/src/OrchardCore.Modules/OrchardCore.Menu/Drivers/HtmlMenuItemPartDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Menu/Drivers/HtmlMenuItemPartDisplayDriver.cs
index eb331b27f..f483404ac 100644
--- a/src/OrchardCore.Modules/OrchardCore.Menu/Drivers/HtmlMenuItemPartDisplayDriver.cs
+++ b/src/OrchardCore.Modules/OrchardCore.Menu/Drivers/HtmlMenuItemPartDisplayDriver.cs
@@ -1,6 +1,11 @@
+using System;
+using System.Text.Encodings.Web;
 using System.Threading.Tasks;
+using Microsoft.AspNetCore.Mvc.Infrastructure;
+using Microsoft.AspNetCore.Mvc.Routing;
+using Microsoft.Extensions.Localization;
 using OrchardCore.ContentManagement.Display.ContentDisplay;
 using OrchardCore.ContentManagement.Display.Models;
 using OrchardCore.DisplayManagement.ModelBinding;
 using OrchardCore.DisplayManagement.Views;
 using OrchardCore.Infrastructure.Html;
@@ -10,15 +15,29 @@ using OrchardCore.Menu.ViewModels;
 
 namespace OrchardCore.Menu.Drivers
 {
     public class HtmlMenuItemPartDisplayDriver : ContentPartDisplayDriver<HtmlMenuItemPart>
     {
+        private readonly IUrlHelperFactory _urlHelperFactory;
+        private readonly IActionContextAccessor _actionContextAccessor;
         private readonly IHtmlSanitizerService _htmlSanitizerService;
+        private readonly HtmlEncoder _htmlencoder;
+        private readonly IStringLocalizer S;
 
-        public HtmlMenuItemPartDisplayDriver(IHtmlSanitizerService htmlSanitizerService)
+        public HtmlMenuItemPartDisplayDriver(
+            IUrlHelperFactory urlHelperFactory,
+            IActionContextAccessor actionContextAccessor,
+            IStringLocalizer<HtmlMenuItemPartDisplayDriver> localizer,
+            IHtmlSanitizerService htmlSanitizerService,
+            HtmlEncoder htmlencoder
+            )
         {
+            _urlHelperFactory = urlHelperFactory;
+            _actionContextAccessor = actionContextAccessor;
             _htmlSanitizerService = htmlSanitizerService;
+            _htmlencoder = htmlencoder;
+            S = localizer;
         }
 
         public override IDisplayResult Display(HtmlMenuItemPart part, BuildPartDisplayContext context)
         {
             var settings = context.TypePartDefinition.GetSettings<HtmlMenuItemPartSettings>();
@@ -60,10 +79,39 @@ namespace OrchardCore.Menu.Drivers
             if (await updater.TryUpdateModelAsync(model, Prefix))
             {
                 part.ContentItem.DisplayText = model.Name;
                 part.Html = settings.SanitizeHtml ? _htmlSanitizerService.Sanitize(model.Html) : model.Html;
                 part.Url = model.Url;
+
+                var urlToValidate = part.Url;
+
+                if (!String.IsNullOrEmpty(urlToValidate))
+                {
+                    urlToValidate = urlToValidate.Split('#', 2)[0];
+
+                    if (urlToValidate.StartsWith("~/", StringComparison.Ordinal))
+                    {
+                        var urlHelper = _urlHelperFactory.GetUrlHelper(_actionContextAccessor.ActionContext);
+                        urlToValidate = urlHelper.Content(urlToValidate);
+                    }
+
+                    urlToValidate = urlToValidate.ToUriComponents();
+
+                    if (!Uri.IsWellFormedUriString(urlToValidate, UriKind.RelativeOrAbsolute))
+                    {
+                        updater.ModelState.AddModelError(nameof(part.Url), S["{0} is an invalid url.", part.Url]);
+                    }
+                    else
+                    {
+                        var link = $"<a href=\"{_htmlencoder.Encode(urlToValidate)}\"></a>";
+
+                        if (!String.Equals(link, _htmlSanitizerService.Sanitize(link), StringComparison.OrdinalIgnoreCase))
+                        {
+                            updater.ModelState.AddModelError(nameof(part.Url), S["{0} is an invalid url.", part.Url]);
+                        }
+                    }
+                }
             }
 
             return Edit(part, context);
         }
     }
diff --git a/src/OrchardCore.Modules/OrchardCore.Menu/Drivers/LinkMenuItemPartDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Menu/Drivers/LinkMenuItemPartDisplayDriver.cs
index 1fd1dfd47..f6c078cdb 100644
--- a/src/OrchardCore.Modules/OrchardCore.Menu/Drivers/LinkMenuItemPartDisplayDriver.cs
+++ b/src/OrchardCore.Modules/OrchardCore.Menu/Drivers/LinkMenuItemPartDisplayDriver.cs
@@ -1,17 +1,43 @@
+using System;
+using System.Text.Encodings.Web;
 using System.Threading.Tasks;
+using Microsoft.AspNetCore.Mvc.Infrastructure;
+using Microsoft.AspNetCore.Mvc.Routing;
+using Microsoft.Extensions.Localization;
 using OrchardCore.ContentManagement.Display.ContentDisplay;
 using OrchardCore.ContentManagement.Display.Models;
 using OrchardCore.DisplayManagement.ModelBinding;
 using OrchardCore.DisplayManagement.Views;
+using OrchardCore.Infrastructure.Html;
 using OrchardCore.Menu.Models;
 using OrchardCore.Menu.ViewModels;
 
 namespace OrchardCore.Menu.Drivers
 {
     public class LinkMenuItemPartDisplayDriver : ContentPartDisplayDriver<LinkMenuItemPart>
     {
+        private readonly IUrlHelperFactory _urlHelperFactory;
+        private readonly IActionContextAccessor _actionContextAccessor;
+        private readonly IHtmlSanitizerService _htmlSanitizerService;
+        private readonly HtmlEncoder _htmlencoder;
+        private readonly IStringLocalizer S;
+
+        public LinkMenuItemPartDisplayDriver(
+            IUrlHelperFactory urlHelperFactory,
+            IActionContextAccessor actionContextAccessor,
+            IStringLocalizer<LinkMenuItemPartDisplayDriver> localizer,
+            IHtmlSanitizerService htmlSanitizerService,
+            HtmlEncoder htmlencoder
+            )
+        {
+            _urlHelperFactory = urlHelperFactory;
+            _actionContextAccessor = actionContextAccessor; 
+            _htmlSanitizerService = htmlSanitizerService;
+            _htmlencoder = htmlencoder;
+            S = localizer;
+        }
 
         public override IDisplayResult Display(LinkMenuItemPart part, BuildPartDisplayContext context)
         {
             return Combine(
                 Dynamic("LinkMenuItemPart_Admin", shape =>
@@ -43,14 +69,45 @@ namespace OrchardCore.Menu.Drivers
 
             if (await updater.TryUpdateModelAsync(model, Prefix))
             {
                 part.Url = model.Url;
                 part.ContentItem.DisplayText = model.Name;
+
                 // This code can be removed in a later release.
 #pragma warning disable 0618
                 part.Name = model.Name;
 #pragma warning restore 0618
+
+                var urlToValidate = part.Url;
+
+                if (!String.IsNullOrEmpty(urlToValidate))
+                {
+                    urlToValidate = urlToValidate.Split('#', 2)[0];
+
+                    if (urlToValidate.StartsWith("~/", StringComparison.Ordinal))
+                    {
+                        var urlHelper = _urlHelperFactory.GetUrlHelper(_actionContextAccessor.ActionContext);
+                        urlToValidate = urlHelper.Content(urlToValidate);
+                    }
+
+                    urlToValidate = urlToValidate.ToUriComponents();
+
+                    if (!Uri.IsWellFormedUriString(urlToValidate, UriKind.RelativeOrAbsolute))
+                    {
+                        updater.ModelState.AddModelError(nameof(part.Url), S["{0} is an invalid url.", part.Url]);
+                    }
+                    else
+                    {
+                        var link = $"<a href=\"{_htmlencoder.Encode(urlToValidate)}\"></a>";
+
+                        if (!String.Equals(link, _htmlSanitizerService.Sanitize(link), StringComparison.OrdinalIgnoreCase))
+                        {
+                            updater.ModelState.AddModelError(nameof(part.Url), S["{0} is an invalid url.", part.Url]);
+                        }
+                    }
+                }
             }
+
             return Edit(part);
         }
     }
 }
diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Workflows/Activities/RegisterUserTask.cs b/src/OrchardCore.Modules/OrchardCore.Users/Workflows/Activities/RegisterUserTask.cs
index 0b1ff3222..39d0b7384 100644
--- a/src/OrchardCore.Modules/OrchardCore.Users/Workflows/Activities/RegisterUserTask.cs
+++ b/src/OrchardCore.Modules/OrchardCore.Users/Workflows/Activities/RegisterUserTask.cs
@@ -132,19 +132,18 @@ namespace OrchardCore.Users.Workflows.Activities
                         "Registration", new { area = "OrchardCore.Users", userId = user.UserId, code });
 
                     workflowContext.Properties["EmailConfirmationUrl"] = uri;
 
                     var subject = await _expressionEvaluator.EvaluateAsync(ConfirmationEmailSubject, workflowContext, null);
-                    var localizedSubject = new LocalizedString(nameof(RegisterUserTask), subject);
 
                     var body = await _expressionEvaluator.EvaluateAsync(ConfirmationEmailTemplate, workflowContext, _htmlEncoder);
-                    var localizedBody = new LocalizedHtmlString(nameof(RegisterUserTask), body);
+
                     var message = new MailMessage()
                     {
                         To = email,
-                        Subject = localizedSubject.ResourceNotFound ? subject : localizedSubject.Value,
-                        Body = localizedBody.IsResourceNotFound ? body : localizedBody.Value,
+                        Subject = subject,
+                        Body = body,
                         IsBodyHtml = true
                     };
                     var smtpService = _httpContextAccessor.HttpContext.RequestServices.GetService<ISmtpService>();
 
                     if (smtpService == null)
diff --git a/src/OrchardCore.Modules/OrchardCore.Workflows/Activities/NotifyTask.cs b/src/OrchardCore.Modules/OrchardCore.Workflows/Activities/NotifyTask.cs
index 412fe8b03..20e936fcf 100644
--- a/src/OrchardCore.Modules/OrchardCore.Workflows/Activities/NotifyTask.cs
+++ b/src/OrchardCore.Modules/OrchardCore.Workflows/Activities/NotifyTask.cs
@@ -53,10 +53,12 @@ namespace OrchardCore.Workflows.Activities
         }
 
         public override async Task<ActivityExecutionResult> ExecuteAsync(WorkflowExecutionContext workflowContext, ActivityContext activityContext)
         {
             var message = await _expressionEvaluator.EvaluateAsync(Message, workflowContext, _htmlEncoder);
+
+            // The notification message can contain HTML by design
             await _notifier.AddAsync(NotificationType, new LocalizedHtmlString(nameof(NotifyTask), message));
 
             return Outcomes("Done");
         }
     }
diff --git a/src/OrchardCore.Modules/OrchardCore.Workflows/Views/Items/UserTaskEvent.Fields.Design.cshtml b/src/OrchardCore.Modules/OrchardCore.Workflows/Views/Items/UserTaskEvent.Fields.Design.cshtml
index e34b15913..41e929c00 100644
--- a/src/OrchardCore.Modules/OrchardCore.Workflows/Views/Items/UserTaskEvent.Fields.Design.cshtml
+++ b/src/OrchardCore.Modules/OrchardCore.Workflows/Views/Items/UserTaskEvent.Fields.Design.cshtml
@@ -6,11 +6,11 @@
 </header>
 
 @if (Model.Activity.Actions.Any())
 {
     <span>@T["Request any user action of <em>{0}</em>", string.Join(", ", Model.Activity.Actions)]</span><br />
-    <span>@T["Required roles: <em>{0}</em>", Model.Activity.Roles.Any() ? new LocalizedHtmlString("RequiredRoles", string.Join(", ", Model.Activity.Roles)) : T["Any"]]</span>
+    <span>@T["Required roles: <em>{0}</em>", Model.Activity.Roles.Any() ? Html.Raw(Html.Encode(string.Join(", ", Model.Activity.Roles))) : T["Any"]]</span>
 }
 else
 {
     <span>@T["No user actions specified"]</span>
 }
diff --git a/src/OrchardCore.Modules/OrchardCore.Workflows/Views/Workflow/Details.cshtml b/src/OrchardCore.Modules/OrchardCore.Workflows/Views/Workflow/Details.cshtml
index 32988dfe6..517b588ad 100644
--- a/src/OrchardCore.Modules/OrchardCore.Workflows/Views/Workflow/Details.cshtml
+++ b/src/OrchardCore.Modules/OrchardCore.Workflows/Views/Workflow/Details.cshtml
@@ -42,11 +42,11 @@
                         <label>@T["Created"]</label>
                         <span>@await LocalClock.ConvertToLocalAsync(Model.Workflow.CreatedUtc)</span>
                     </div>
                     <div class="col">
                         <label>@T["Status"]</label>
-                        <span>@Model.Workflow.Status.GetLocalizedStatus(T)</span>
+                        <span>@T.GetLocalizedStatus(Model.Workflow.Status)</span>
                     </div>
                     @if (Model.Workflow.Status == WorkflowStatus.Faulted)
                     {
                         <div class="col">
                             <label>@T["Fault Message"]</label>
diff --git a/src/OrchardCore.Modules/OrchardCore.Workflows/Views/Workflow/Index.cshtml b/src/OrchardCore.Modules/OrchardCore.Workflows/Views/Workflow/Index.cshtml
index 3d3ea9e15..7b5dd404c 100644
--- a/src/OrchardCore.Modules/OrchardCore.Workflows/Views/Workflow/Index.cshtml
+++ b/src/OrchardCore.Modules/OrchardCore.Workflows/Views/Workflow/Index.cshtml
@@ -102,11 +102,11 @@
                             <a asp-action="Details" asp-route-id="@entry.Workflow.Id">@entry.Workflow.WorkflowId</a>
                             <div>
                                 <span class="hint">@T["Created {0}", (object)(await DisplayAsync(await New.TimeSpan(Utc: entry.Workflow.CreatedUtc)))]</span>
                                 <div class="info">
                                     <span class="badge badge-@statusCss">
-                                        @entry.Workflow.Status.GetLocalizedStatus(T)
+                                        @T.GetLocalizedStatus(entry.Workflow.Status)
                                     </span>
                                 </div>
                             </div>
                         </div>
                     </li>
diff --git a/src/OrchardCore/OrchardCore.Workflows.Abstractions/Helpers/ActivityExtensions.cs b/src/OrchardCore/OrchardCore.Workflows.Abstractions/Helpers/ActivityExtensions.cs
index f471aeb36..7cfe4fa20 100644
--- a/src/OrchardCore/OrchardCore.Workflows.Abstractions/Helpers/ActivityExtensions.cs
+++ b/src/OrchardCore/OrchardCore.Workflows.Abstractions/Helpers/ActivityExtensions.cs
@@ -6,38 +6,38 @@ using OrchardCore.Workflows.Models;
 
 namespace OrchardCore.Workflows.Helpers
 {
     public static class ActivityExtensions
     {
-        private static IHtmlLocalizer H;
-
         public static bool IsEvent(this IActivity activity)
         {
             return activity is IEvent;
         }
 
         public static LocalizedHtmlString GetTitleOrDefault(this IActivity activity, Func<LocalizedHtmlString> defaultTitle)
         {
             var title = activity.As<ActivityMetadata>().Title;
-            return !string.IsNullOrEmpty(title) ? new LocalizedHtmlString(title, title) : defaultTitle();
+
+            // A string used in LocalizedHtmlString won't be encoded so it needs to be pre-encoded.
+            // Passing the title as an argument so it uses the HtmlEncoder when rendered
+            // Another options would be to use new LocalizedHtmlString(Html.Encode(title)) but it's not available in the current context
+
+            return !string.IsNullOrEmpty(title) ? new LocalizedHtmlString(nameof(ActivityExtensions.GetTitleOrDefault), "{0}", false, title) : defaultTitle();
         }
 
-        public static LocalizedHtmlString GetLocalizedStatus(this WorkflowStatus status, IHtmlLocalizer localizer)
+        public static LocalizedHtmlString GetLocalizedStatus(this IHtmlLocalizer H, WorkflowStatus status)
         {
-            // Field for PoExtractor compatibility
-            H = localizer;
-
             return status switch
             {
                 WorkflowStatus.Aborted => H["Aborted"],
                 WorkflowStatus.Executing => H["Executing"],
                 WorkflowStatus.Faulted => H["Faulted"],
                 WorkflowStatus.Finished => H["Finished"],
                 WorkflowStatus.Halted => H["Halted"],
                 WorkflowStatus.Idle => H["Idle"],
                 WorkflowStatus.Resuming => H["Resuming"],
                 WorkflowStatus.Starting => H["Starting"],
-                _ => new LocalizedHtmlString(status.ToString(), status.ToString()),
+                _ => throw new NotSupportedException(),
             };
         }
     }
 }

Vulnerable code:             IStringLocalizer<LinkFieldDisplayDriver> localizer)

                // Run this through a sanitizer in case someone puts html in it.
                // No settings.
                    await _notifier.WarningAsync(new LocalizedHtmlString(error.ErrorMessage, error.ErrorMessage));
        public HtmlMenuItemPartDisplayDriver(IHtmlSanitizerService htmlSanitizerService)
                    var localizedSubject = new LocalizedString(nameof(RegisterUserTask), subject);
                    var localizedBody = new LocalizedHtmlString(nameof(RegisterUserTask), body);
                        Subject = localizedSubject.ResourceNotFound ? subject : localizedSubject.Value,
                        Body = localizedBody.IsResourceNotFound ? body : localizedBody.Value,
    <span>@T["Required roles: <em>{0}</em>", Model.Activity.Roles.Any() ? new LocalizedHtmlString("RequiredRoles", string.Join(", ", Model.Activity.Roles)) : T["Any"]]</span>
                        <span>@Model.Workflow.Status.GetLocalizedStatus(T)</span>
                                        @entry.Workflow.Status.GetLocalizedStatus(T)
        private static IHtmlLocalizer H;

            return !string.IsNullOrEmpty(title) ? new LocalizedHtmlString(title, title) : defaultTitle();
        public static LocalizedHtmlString GetLocalizedStatus(this WorkflowStatus status, IHtmlLocalizer localizer)
            // Field for PoExtractor compatibility
            H = localizer;

                _ => new LocalizedHtmlString(status.ToString(), status.ToString()),
Fixed code: using System.Text.Encodings.Web;
using OrchardCore.Infrastructure.Html;
        private readonly IHtmlSanitizerService _htmlSanitizerService;
        private readonly HtmlEncoder _htmlencoder;
            IStringLocalizer<LinkFieldDisplayDriver> localizer,
            IHtmlSanitizerService htmlSanitizerService,
            HtmlEncoder htmlencoder)
            _htmlSanitizerService = htmlSanitizerService;
            _htmlencoder = htmlencoder;
                else
                {
                    var link = $"<a href=\"{_htmlencoder.Encode(urlToValidate)}\"></a>";

                    if (!String.Equals(link, _htmlSanitizerService.Sanitize(link), StringComparison.OrdinalIgnoreCase))
                    {
                        updater.ModelState.AddModelError(Prefix, nameof(field.Url), S["{0} is an invalid url.", field.Url]);
                    }
                }


                    // Pass ErrorMessage as an argument to ensure it is encoded
                    await _notifier.WarningAsync(new LocalizedHtmlString(nameof(AuditTrailContentController.Restore), "{0}", false, error.ErrorMessage));
using System;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.Extensions.Localization;
        private readonly IUrlHelperFactory _urlHelperFactory;
        private readonly IActionContextAccessor _actionContextAccessor;
        private readonly HtmlEncoder _htmlencoder;
        private readonly IStringLocalizer S;
        public HtmlMenuItemPartDisplayDriver(
            IUrlHelperFactory urlHelperFactory,
            IActionContextAccessor actionContextAccessor,
            IStringLocalizer<HtmlMenuItemPartDisplayDriver> localizer,
            IHtmlSanitizerService htmlSanitizerService,
            HtmlEncoder htmlencoder
            )
            _urlHelperFactory = urlHelperFactory;
            _actionContextAccessor = actionContextAccessor;
            _htmlencoder = htmlencoder;
            S = localizer;

                var urlToValidate = part.Url;

                if (!String.IsNullOrEmpty(urlToValidate))
                {
                    urlToValidate = urlToValidate.Split('#', 2)[0];

                    if (urlToValidate.StartsWith("~/", StringComparison.Ordinal))
                    {
                        var urlHelper = _urlHelperFactory.GetUrlHelper(_actionContextAccessor.ActionContext);
                        urlToValidate = urlHelper.Content(urlToValidate);
                    }

                    urlToValidate = urlToValidate.ToUriComponents();

                    if (!Uri.IsWellFormedUriString(urlToValidate, UriKind.RelativeOrAbsolute))
                    {
                        updater.ModelState.AddModelError(nameof(part.Url), S["{0} is an invalid url.", part.Url]);
                    }
                    else
                    {
                        var link = $"<a href=\"{_htmlencoder.Encode(urlToValidate)}\"></a>";

                        if (!String.Equals(link, _htmlSanitizerService.Sanitize(link), StringComparison.OrdinalIgnoreCase))
                        {
                            updater.ModelState.AddModelError(nameof(part.Url), S["{0} is an invalid url.", part.Url]);
                        }
                    }
                }
using System;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.Extensions.Localization;
using OrchardCore.Infrastructure.Html;
        private readonly IUrlHelperFactory _urlHelperFactory;
        private readonly IActionContextAccessor _actionContextAccessor;
        private readonly IHtmlSanitizerService _htmlSanitizerService;
        private readonly HtmlEncoder _htmlencoder;
        private readonly IStringLocalizer S;

        public LinkMenuItemPartDisplayDriver(
            IUrlHelperFactory urlHelperFactory,
            IActionContextAccessor actionContextAccessor,
            IStringLocalizer<LinkMenuItemPartDisplayDriver> localizer,
            IHtmlSanitizerService htmlSanitizerService,
            HtmlEncoder htmlencoder
            )
        {
            _urlHelperFactory = urlHelperFactory;
            _actionContextAccessor = actionContextAccessor; 
            _htmlSanitizerService = htmlSanitizerService;
            _htmlencoder = htmlencoder;
            S = localizer;
        }


                var urlToValidate = part.Url;

                if (!String.IsNullOrEmpty(urlToValidate))
                {
                    urlToValidate = urlToValidate.Split('#', 2)[0];

                    if (urlToValidate.StartsWith("~/", StringComparison.Ordinal))
                    {
                        var urlHelper = _urlHelperFactory.GetUrlHelper(_actionContextAccessor.ActionContext);
                        urlToValidate = urlHelper.Content(urlToValidate);
                    }

                    urlToValidate = urlToValidate.ToUriComponents();

                    if (!Uri.IsWellFormedUriString(urlToValidate, UriKind.RelativeOrAbsolute))
                    {
                        updater.ModelState.AddModelError(nameof(part.Url), S["{0} is an invalid url.", part.Url]);
                    }
                    else
                    {
                        var link = $"<a href=\"{_htmlencoder.Encode(urlToValidate)}\"></a>";

                        if (!String.Equals(link, _htmlSanitizerService.Sanitize(link), StringComparison.OrdinalIgnoreCase))
                        {
                            updater.ModelState.AddModelError(nameof(part.Url), S["{0} is an invalid url.", part.Url]);
                        }
                    }
                }


                        Subject = subject,
                        Body = body,

            // The notification message can contain HTML by design
    <span>@T["Required roles: <em>{0}</em>", Model.Activity.Roles.Any() ? Html.Raw(Html.Encode(string.Join(", ", Model.Activity.Roles))) : T["Any"]]</span>
                        <span>@T.GetLocalizedStatus(Model.Workflow.Status)</span>
                                        @T.GetLocalizedStatus(entry.Workflow.Status)

            // A string used in LocalizedHtmlString won't be encoded so it needs to be pre-encoded.
            // Passing the title as an argument so it uses the HtmlEncoder when rendered
            // Another options would be to use new LocalizedHtmlString(Html.Encode(title)) but it's not available in the current context

            return !string.IsNullOrEmpty(title) ? new LocalizedHtmlString(nameof(ActivityExtensions.GetTitleOrDefault), "{0}", false, title) : defaultTitle();
        public static LocalizedHtmlString GetLocalizedStatus(this IHtmlLocalizer H, WorkflowStatus status)
                _ => throw new NotSupportedException(),