From 8a94bd1968c71348ebfaba8f75ab2c7487dbe69e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Mar 2026 13:44:20 +0000 Subject: [PATCH 1/2] Initial plan From c68405cb913d96a85b6e58e87a57b1588b212038 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 30 Mar 2026 13:53:22 +0000 Subject: [PATCH 2/2] Fix stored XSS vulnerability in PageHead component via HtmlSanitizer allowlist Agent-Logs-Url: https://github.com/fluentcms/FluentCMS/sessions/1651a529-c67c-468a-bb04-c0561e9d436c Co-authored-by: pournasserian <24959477+pournasserian@users.noreply.github.com> --- .../Components/PageHead.razor | 2 +- .../Components/PageHead.razor.cs | 44 ++++++++++++++++++- .../FluentCMS.Web.UI/FluentCMS.Web.UI.csproj | 1 + 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/src/Frontend/FluentCMS.Web.UI/Components/PageHead.razor b/src/Frontend/FluentCMS.Web.UI/Components/PageHead.razor index 65e3cea58..9eb6f37f3 100644 --- a/src/Frontend/FluentCMS.Web.UI/Components/PageHead.razor +++ b/src/Frontend/FluentCMS.Web.UI/Components/PageHead.razor @@ -25,7 +25,7 @@ @if (!string.IsNullOrEmpty(GetSetting("Head"))) { - @((MarkupString)GetSetting("Head")) + @((MarkupString)SanitizeHeadContent(GetSetting("Head"))) } @foreach (var stylesheet in Stylesheets) diff --git a/src/Frontend/FluentCMS.Web.UI/Components/PageHead.razor.cs b/src/Frontend/FluentCMS.Web.UI/Components/PageHead.razor.cs index 580066ef4..cdd5b2d1a 100644 --- a/src/Frontend/FluentCMS.Web.UI/Components/PageHead.razor.cs +++ b/src/Frontend/FluentCMS.Web.UI/Components/PageHead.razor.cs @@ -1,3 +1,5 @@ +using Ganss.Xss; + namespace FluentCMS.Web.UI; public partial class PageHead : IAsyncDisposable @@ -7,6 +9,39 @@ public partial class PageHead : IAsyncDisposable private List Stylesheets { get; set; } = []; + private static readonly HtmlSanitizer _headSanitizer = CreateHeadSanitizer(); + + private static HtmlSanitizer CreateHeadSanitizer() + { + var sanitizer = new HtmlSanitizer(); + sanitizer.AllowedTags.Clear(); + sanitizer.AllowedTags.Add("meta"); + sanitizer.AllowedTags.Add("link"); + sanitizer.AllowedTags.Add("style"); + sanitizer.AllowedTags.Add("noscript"); + sanitizer.AllowedAttributes.Clear(); + sanitizer.AllowedAttributes.Add("name"); + sanitizer.AllowedAttributes.Add("content"); + sanitizer.AllowedAttributes.Add("rel"); + sanitizer.AllowedAttributes.Add("href"); + sanitizer.AllowedAttributes.Add("type"); + sanitizer.AllowedAttributes.Add("media"); + sanitizer.AllowedAttributes.Add("charset"); + sanitizer.AllowedAttributes.Add("property"); + sanitizer.AllowedAttributes.Add("http-equiv"); + sanitizer.AllowedAttributes.Add("sizes"); + sanitizer.AllowedAttributes.Add("hreflang"); + sanitizer.AllowedAttributes.Add("crossorigin"); + sanitizer.AllowedAttributes.Add("integrity"); + sanitizer.AllowedCssProperties.Clear(); + return sanitizer; + } + + private static string SanitizeHeadContent(string content) + { + return _headSanitizer.Sanitize(content); + } + private string GetRobots() { ViewState.Page.Settings.TryGetValue("Index", out var index); @@ -36,9 +71,16 @@ private string GetSetting(string key) return pageValue ?? siteValue ?? string.Empty; } + private static readonly System.Text.RegularExpressions.Regex _googleTagsIdRegex = + new(@"^[A-Za-z0-9\-]+$", System.Text.RegularExpressions.RegexOptions.Compiled); + private string GetGoogleTagsScript() { - return $"\n"; + var tagId = GetSetting("GoogleTagsId"); + if (string.IsNullOrEmpty(tagId) || !_googleTagsIdRegex.IsMatch(tagId)) + return string.Empty; + + return $"\n"; } private async void OnStateChanged(object? sender, EventArgs e) diff --git a/src/Frontend/FluentCMS.Web.UI/FluentCMS.Web.UI.csproj b/src/Frontend/FluentCMS.Web.UI/FluentCMS.Web.UI.csproj index 067344a5f..4007a89bb 100644 --- a/src/Frontend/FluentCMS.Web.UI/FluentCMS.Web.UI.csproj +++ b/src/Frontend/FluentCMS.Web.UI/FluentCMS.Web.UI.csproj @@ -22,6 +22,7 @@ +