From 57f0f39f4dc834c274aaa417e74ce0f25dd9742b Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Wed, 25 Feb 2026 21:14:21 +0100 Subject: [PATCH] Enhanced Markdown security by using SecurePipeline --- .../Chat/ContentBlockComponent.razor | 6 +- .../Chat/ContentBlockComponent.razor.cs | 109 +++++++++++++++++- .../Components/Changelog.razor | 2 +- .../Components/ChatComponent.razor | 3 +- .../Components/ConfidenceInfo.razor | 4 +- .../Settings/SettingsPanelProviders.razor | 2 +- .../Dialogs/DocumentCheckDialog.razor | 4 +- .../Dialogs/PandocDialog.razor | 4 +- .../Dialogs/UpdateDialog.razor | 4 +- app/MindWork AI Studio/Pages/Home.razor | 6 +- .../Pages/Information.razor | 2 +- app/MindWork AI Studio/Tools/Markdown.cs | 11 +- .../wwwroot/changelog/v26.3.1.md | 3 +- 13 files changed, 135 insertions(+), 25 deletions(-) diff --git a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor index 7c09ae78..f3f2f528 100644 --- a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor +++ b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor @@ -96,10 +96,10 @@ } else { - + @if (textContent.Sources.Count > 0) { - + } } } @@ -135,4 +135,4 @@ } } - \ No newline at end of file + diff --git a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs index e29a016d..17894232 100644 --- a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs +++ b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs @@ -10,6 +10,18 @@ namespace AIStudio.Chat; /// public partial class ContentBlockComponent : MSGComponentBase { + private static readonly string[] HTML_TAG_MARKERS = + [ + " /// The role of the chat content block. /// @@ -68,18 +80,36 @@ public partial class ContentBlockComponent : MSGComponentBase private RustService RustService { get; init; } = null!; private bool HideContent { get; set; } + private bool hasRenderHash; + private int lastRenderHash; #region Overrides of ComponentBase protected override async Task OnInitializedAsync() { - // Register the streaming events: - this.Content.StreamingDone = this.AfterStreaming; - this.Content.StreamingEvent = () => this.InvokeAsync(this.StateHasChanged); - + this.RegisterStreamingEvents(); await base.OnInitializedAsync(); } + protected override Task OnParametersSetAsync() + { + this.RegisterStreamingEvents(); + return base.OnParametersSetAsync(); + } + + protected override bool ShouldRender() + { + var currentRenderHash = this.CreateRenderHash(); + if (!this.hasRenderHash || currentRenderHash != this.lastRenderHash) + { + this.lastRenderHash = currentRenderHash; + this.hasRenderHash = true; + return true; + } + + return false; + } + /// /// Gets called when the content stream ended. /// @@ -111,6 +141,47 @@ public partial class ContentBlockComponent : MSGComponentBase }); } + private void RegisterStreamingEvents() + { + this.Content.StreamingDone = this.AfterStreaming; + this.Content.StreamingEvent = () => this.InvokeAsync(this.StateHasChanged); + } + + private int CreateRenderHash() + { + var hash = new HashCode(); + hash.Add(this.Role); + hash.Add(this.Type); + hash.Add(this.Time); + hash.Add(this.Class); + hash.Add(this.IsLastContentBlock); + hash.Add(this.IsSecondToLastBlock); + hash.Add(this.HideContent); + hash.Add(this.SettingsManager.IsDarkMode); + hash.Add(this.RegenerateEnabled()); + hash.Add(this.Content.InitialRemoteWait); + hash.Add(this.Content.IsStreaming); + hash.Add(this.Content.FileAttachments.Count); + hash.Add(this.Content.Sources.Count); + + switch (this.Content) + { + case ContentText text: + var textValue = text.Text ?? string.Empty; + hash.Add(textValue.Length); + hash.Add(textValue.GetHashCode(StringComparison.Ordinal)); + hash.Add(text.Sources.Count); + break; + + case ContentImage image: + hash.Add(image.SourceType); + hash.Add(image.Source); + break; + } + + return hash.ToHashCode(); + } + #endregion private string CardClasses => $"my-2 rounded-lg {this.Class}"; @@ -121,6 +192,34 @@ public partial class ContentBlockComponent : MSGComponentBase { CodeBlock = { Theme = this.CodeColorPalette }, }; + + private string NormalizeMarkdownForRendering(string text) + { + var cleaned = text.RemoveThinkTags().Trim(); + if (string.IsNullOrWhiteSpace(cleaned)) + return string.Empty; + + if (cleaned.Contains("```", StringComparison.Ordinal)) + return cleaned; + + if (LooksLikeRawHtml(cleaned)) + return $"```html{Environment.NewLine}{cleaned}{Environment.NewLine}```"; + + return cleaned; + } + + private static bool LooksLikeRawHtml(string text) + { + var content = text.TrimStart(); + if (!content.StartsWith("<", StringComparison.Ordinal)) + return false; + + foreach (var marker in HTML_TAG_MARKERS) + if (content.Contains(marker, StringComparison.OrdinalIgnoreCase)) + return true; + + return content.Contains("", StringComparison.Ordinal); + } private async Task RemoveBlock() { @@ -194,4 +293,4 @@ public partial class ContentBlockComponent : MSGComponentBase var result = await ReviewAttachmentsDialog.OpenDialogAsync(this.DialogService, this.Content.FileAttachments.ToHashSet()); this.Content.FileAttachments = result.ToList(); } -} \ No newline at end of file +} diff --git a/app/MindWork AI Studio/Components/Changelog.razor b/app/MindWork AI Studio/Components/Changelog.razor index 1afebfc3..2d573afb 100644 --- a/app/MindWork AI Studio/Components/Changelog.razor +++ b/app/MindWork AI Studio/Components/Changelog.razor @@ -6,4 +6,4 @@ } - \ No newline at end of file + diff --git a/app/MindWork AI Studio/Components/ChatComponent.razor b/app/MindWork AI Studio/Components/ChatComponent.razor index 52b82b9b..1467c01e 100644 --- a/app/MindWork AI Studio/Components/ChatComponent.razor +++ b/app/MindWork AI Studio/Components/ChatComponent.razor @@ -16,6 +16,7 @@ @if (!block.HideFromUser) { - \ No newline at end of file + diff --git a/app/MindWork AI Studio/Components/ConfidenceInfo.razor b/app/MindWork AI Studio/Components/ConfidenceInfo.razor index f27fe58e..ed7c45ef 100644 --- a/app/MindWork AI Studio/Components/ConfidenceInfo.razor +++ b/app/MindWork AI Studio/Components/ConfidenceInfo.razor @@ -28,7 +28,7 @@ @T("Description") - + @if (this.currentConfidence.Sources.Count > 0) { @@ -67,4 +67,4 @@ - \ No newline at end of file + diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor index f6704dc5..f89d7b52 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor @@ -104,7 +104,7 @@ @context.ToName() - + diff --git a/app/MindWork AI Studio/Dialogs/DocumentCheckDialog.razor b/app/MindWork AI Studio/Dialogs/DocumentCheckDialog.razor index f3b75837..dc2ee563 100644 --- a/app/MindWork AI Studio/Dialogs/DocumentCheckDialog.razor +++ b/app/MindWork AI Studio/Dialogs/DocumentCheckDialog.razor @@ -54,7 +54,7 @@ Class="ma-2 pe-4" HelperText="@T("This is the content we loaded from your file — including headings, lists, and formatting. Use this to verify your file loads as expected.")">
- +
@@ -83,4 +83,4 @@ @T("Close") - \ No newline at end of file + diff --git a/app/MindWork AI Studio/Dialogs/PandocDialog.razor b/app/MindWork AI Studio/Dialogs/PandocDialog.razor index 2914b38e..f9638ff9 100644 --- a/app/MindWork AI Studio/Dialogs/PandocDialog.razor +++ b/app/MindWork AI Studio/Dialogs/PandocDialog.razor @@ -30,7 +30,7 @@ } else if (!string.IsNullOrWhiteSpace(this.licenseText)) { - + } @@ -226,4 +226,4 @@ } } - \ No newline at end of file + diff --git a/app/MindWork AI Studio/Dialogs/UpdateDialog.razor b/app/MindWork AI Studio/Dialogs/UpdateDialog.razor index 62f3dd7a..3eaa6db9 100644 --- a/app/MindWork AI Studio/Dialogs/UpdateDialog.razor +++ b/app/MindWork AI Studio/Dialogs/UpdateDialog.razor @@ -5,7 +5,7 @@ @this.HeaderText - + @@ -15,4 +15,4 @@ @T("Install now") - \ No newline at end of file + diff --git a/app/MindWork AI Studio/Pages/Home.razor b/app/MindWork AI Studio/Pages/Home.razor index 53d48e6e..43118b24 100644 --- a/app/MindWork AI Studio/Pages/Home.razor +++ b/app/MindWork AI Studio/Pages/Home.razor @@ -27,7 +27,7 @@ - + @@ -35,9 +35,9 @@ - + - \ No newline at end of file + diff --git a/app/MindWork AI Studio/Pages/Information.razor b/app/MindWork AI Studio/Pages/Information.razor index 435a6a56..e9389237 100644 --- a/app/MindWork AI Studio/Pages/Information.razor +++ b/app/MindWork AI Studio/Pages/Information.razor @@ -297,7 +297,7 @@ - + diff --git a/app/MindWork AI Studio/Tools/Markdown.cs b/app/MindWork AI Studio/Tools/Markdown.cs index 0ecf3774..24946386 100644 --- a/app/MindWork AI Studio/Tools/Markdown.cs +++ b/app/MindWork AI Studio/Tools/Markdown.cs @@ -1,7 +1,16 @@ +using Markdig; + namespace AIStudio.Tools; public static class Markdown { + private static readonly MarkdownPipeline SAFE_MARKDOWN_PIPELINE = new MarkdownPipelineBuilder() + .UseAdvancedExtensions() + .DisableHtml() + .Build(); + + public static MarkdownPipeline SecurePipeline => SAFE_MARKDOWN_PIPELINE; + public static MudMarkdownProps DefaultConfig => new() { Heading = @@ -19,4 +28,4 @@ public static class Markdown }, } }; -} \ No newline at end of file +} diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md b/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md index 840e2947..7f4e3b46 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.3.1.md @@ -2,4 +2,5 @@ - Improved the performance by caching the OS language detection and requesting the user language only once per app start. - Improved the user-language logging by limiting language detection logs to a single entry per app start. - Improved the logbook readability by removing non-readable special characters from log entries. -- Improved the logbook reliability by significantly reducing duplicate log entries. \ No newline at end of file +- Improved the logbook reliability by significantly reducing duplicate log entries. +- Fixed an issue where the app could turn white or appear invisible in certain chats after HTML-like content was shown. \ No newline at end of file