Fix stored XSS in TextHTML plugin via HTML sanitization#2406
Fix stored XSS in TextHTML plugin via HTML sanitization#2406
Conversation
Agent-Logs-Url: https://github.com/fluentcms/FluentCMS/sessions/3dec0a96-d3c2-4c65-b9ba-866ae059056a Co-authored-by: pournasserian <24959477+pournasserian@users.noreply.github.com>
Agent-Logs-Url: https://github.com/fluentcms/FluentCMS/sessions/3dec0a96-d3c2-4c65-b9ba-866ae059056a Co-authored-by: pournasserian <24959477+pournasserian@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR addresses a stored XSS vulnerability in the TextHTMLViewPlugin by sanitizing stored HTML before rendering it as a MarkupString in the Blazor UI.
Changes:
- Add the
HtmlSanitizer(Ganss.Xss) NuGet dependency to the TextHTML plugin project. - Sanitize and cache HTML content in
TextHTMLViewPluginon initialization and on updates. - Render sanitized HTML (
_sanitizedContent) instead of rawItem.Content.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
src/Frontend/Plugins/FluentCMS.Web.Plugins.TextHTML/FluentCMS.Web.Plugins.TextHTML.csproj |
Adds the HtmlSanitizer package dependency. |
src/Frontend/Plugins/FluentCMS.Web.Plugins.TextHTML/TextHTMLViewPlugin.razor.cs |
Introduces sanitizer usage and caches sanitized output. |
src/Frontend/Plugins/FluentCMS.Web.Plugins.TextHTML/TextHTMLViewPlugin.razor |
Switches rendering to use the cached sanitized HTML. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| Item.Content = content; | ||
| _sanitizedContent = Sanitizer.Sanitize(Item.Content); |
There was a problem hiding this comment.
UpdateContent sanitizes into _sanitizedContent, but it still persists the unsanitized content via Item.Content/Item.ToDictionary(). That means the stored value remains dangerous for any other rendering paths (including admin preview/InlineEditor) and future code that might render Item.Content directly. Consider sanitizing before assigning to Item.Content/before calling UpdateAsync (or store both raw+sanitized explicitly if you need raw for editing).
| Item.Content = content; | |
| _sanitizedContent = Sanitizer.Sanitize(Item.Content); | |
| var sanitized = Sanitizer.Sanitize(content); | |
| Item.Content = sanitized; | |
| _sanitizedContent = sanitized; |
| <div class="f-html-content"> | ||
| @((MarkupString)Item.Content) | ||
| @((MarkupString)_sanitizedContent) |
There was a problem hiding this comment.
This change sanitizes the non-admin render path, but the admin preview path still passes Item.Content into InlineEditor. InlineEditor initializes by assigning innerHTML = content (see InlineEditor.razor.js), so stored XSS will still execute for admins when previewing/editing. Consider feeding _sanitizedContent to InlineEditor as well (or sanitizing inside InlineEditor before setting innerHTML).
| if (response?.Data != null && response.Data.ToContentList<TextHTMLContent>().Count != 0) | ||
| { | ||
| Item = response.Data.ToContentList<TextHTMLContent>().FirstOrDefault() ?? default!; | ||
| _sanitizedContent = Item is not null ? Sanitizer.Sanitize(Item.Content) : string.Empty; |
There was a problem hiding this comment.
response.Data.ToContentList<TextHTMLContent>() is computed multiple times in this block. Consider materializing it once into a local variable (e.g., var items = ...) and then using items.Count/items.FirstOrDefault() to reduce repeated work and simplify the conditionals.
| if (response?.Data != null && response.Data.ToContentList<TextHTMLContent>().Count != 0) | |
| { | |
| Item = response.Data.ToContentList<TextHTMLContent>().FirstOrDefault() ?? default!; | |
| _sanitizedContent = Item is not null ? Sanitizer.Sanitize(Item.Content) : string.Empty; | |
| if (response?.Data != null) | |
| { | |
| var items = response.Data.ToContentList<TextHTMLContent>(); | |
| if (items.Count != 0) | |
| { | |
| Item = items.FirstOrDefault() ?? default!; | |
| _sanitizedContent = Item is not null ? Sanitizer.Sanitize(Item.Content) : string.Empty; | |
| } |
TextHTMLViewPluginrendered stored content directly asMarkupStringwith no sanitization, allowing arbitrary script injection (e.g.,<img src=1 onerror=alert('XSS')>) that executed on page load.Changes
FluentCMS.Web.Plugins.TextHTML.csproj— AddedHtmlSanitizer(v9.0.892, Ganss.Xss) package dependency.TextHTMLViewPlugin.razor.cs— Added a staticHtmlSanitizerinstance; compute and cache sanitized output into_sanitizedContenton initial load and on content update — not on every render cycle.TextHTMLViewPlugin.razor— Render_sanitizedContentinstead of rawItem.Content.HtmlSanitizerstrips<script>tags and event handler attributes (onerror,onclick, etc.) while preserving valid HTML markup.