From e618ec1b4c8236b89b86159418c9c7b0f17b51e3 Mon Sep 17 00:00:00 2001 From: Yunus Emre Kalkan Date: Wed, 1 Dec 2021 11:18:19 +0300 Subject: [PATCH] Cms-Kit: PreventXSS option for markdown renderer --- .../Commenting/CommentingViewComponent.cs | 4 +- .../Renderers/IMarkdownToHtmlRenderer.cs | 2 +- .../Renderers/MarkdownToHtmlRenderer.cs | 116 +++++++++++++++++- .../Volo.CmsKit.Public.Web.csproj | 1 + 4 files changed, 116 insertions(+), 7 deletions(-) diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/CommentingViewComponent.cs b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/CommentingViewComponent.cs index 22797cc002..0a608740e9 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/CommentingViewComponent.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/CommentingViewComponent.cs @@ -65,12 +65,12 @@ public class CommentingViewComponent : AbpViewComponent foreach (var comment in viewModel.Comments) { viewModel.RawCommentTexts.Add(comment.Id, comment.Text); - comment.Text = await MarkdownToHtmlRenderer.RenderAsync(comment.Text); + comment.Text = await MarkdownToHtmlRenderer.RenderAsync(comment.Text, true); foreach (var reply in comment.Replies) { viewModel.RawCommentTexts.Add(reply.Id, reply.Text); - reply.Text = await MarkdownToHtmlRenderer.RenderAsync(reply.Text); + reply.Text = await MarkdownToHtmlRenderer.RenderAsync(reply.Text, true); } } } diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Renderers/IMarkdownToHtmlRenderer.cs b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Renderers/IMarkdownToHtmlRenderer.cs index 9f19d9f3da..d11e76f6dd 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Renderers/IMarkdownToHtmlRenderer.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Renderers/IMarkdownToHtmlRenderer.cs @@ -4,5 +4,5 @@ namespace Volo.CmsKit.Public.Web.Renderers; public interface IMarkdownToHtmlRenderer { - Task RenderAsync(string rawMarkdown); + Task RenderAsync(string rawMarkdown, bool preventXSS = true); } diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Renderers/MarkdownToHtmlRenderer.cs b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Renderers/MarkdownToHtmlRenderer.cs index 6998a3d828..b6fab3c008 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Renderers/MarkdownToHtmlRenderer.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Renderers/MarkdownToHtmlRenderer.cs @@ -1,21 +1,129 @@ -using Markdig; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using Markdig; using System.Threading.Tasks; +using System.Web; using Volo.Abp.DependencyInjection; +using Ganss.XSS; namespace Volo.CmsKit.Public.Web.Renderers; public class MarkdownToHtmlRenderer : IMarkdownToHtmlRenderer, ITransientDependency { + private readonly HtmlSanitizer _htmlSanitizer; protected MarkdownPipeline MarkdownPipeline { get; } public MarkdownToHtmlRenderer(MarkdownPipeline markdownPipeline) { MarkdownPipeline = markdownPipeline; + _htmlSanitizer = new HtmlSanitizer(); } - public Task RenderAsync(string rawMarkdown) + public async Task RenderAsync(string rawMarkdown, bool preventXSS = false) { - return Task.FromResult( - Markdown.ToHtml(rawMarkdown, MarkdownPipeline)); + if (preventXSS) + { + rawMarkdown = EncodeHtmlTags(rawMarkdown, true); + } + + var html = Markdown.ToHtml(rawMarkdown, MarkdownPipeline); + + if (preventXSS) + { + html = _htmlSanitizer.Sanitize(html); + } + + return html; + } + + + private static List GetCodeBlockIndices(string markdownText) + { + var regexObj = new Regex(@"```(\w)*|`(\w)*", RegexOptions.IgnoreCase | + RegexOptions.IgnorePatternWhitespace | + RegexOptions.Singleline | + RegexOptions.Multiline | + RegexOptions.ExplicitCapture); + + var matches = regexObj.Matches(markdownText); + var indices = new List(); + + for (var i = 0; i < matches.Count; i++) + { + if (!indices.Any() || indices.Last().EndIndex.HasValue) + { + indices.Add(new CodeBlockIndexPair(matches[i].Index)); + } + else + { + indices.Last().EndIndex = matches[i].Index; + } + } + + return indices; + } + + /// + /// Encodes html tags. + /// + private static string EncodeHtmlTags(string text, bool dontEncodeCodeBlocks = true) + { + List codeBlockIndices = null; + if (dontEncodeCodeBlocks) + { + codeBlockIndices = GetCodeBlockIndices(text); + } + + return Regex.Replace(text, @"<[^>]*>", match => + { + if (dontEncodeCodeBlocks && codeBlockIndices != null) + { + var isInCodeBlock = false; + foreach (var codeBlock in codeBlockIndices) + { + if (IsInCodeBlock(match.Index, codeBlock.StartIndex, codeBlock.EndIndex)) + { + isInCodeBlock = true; + break; + } + } + + if (isInCodeBlock) + { + return match.ToString(); + } + else + { + return HttpUtility.HtmlEncode(match.ToString()); + } + } + else + { + return HttpUtility.HtmlEncode(match.ToString()); + } + }); + } + + private static bool IsInCodeBlock(int currentIndex, int codeBlockStartIndex, int? codeBlockEndIndex) + { + if (codeBlockEndIndex.HasValue) + { + return (currentIndex >= codeBlockStartIndex && currentIndex <= codeBlockEndIndex); + } + + return currentIndex >= codeBlockStartIndex; + } + + private class CodeBlockIndexPair + { + public int StartIndex { get; private set; } + public int? EndIndex { get; set; } + + public CodeBlockIndexPair(int startIndex, int? endIndex = null) + { + StartIndex = startIndex; + EndIndex = endIndex; + } } } diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Volo.CmsKit.Public.Web.csproj b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Volo.CmsKit.Public.Web.csproj index 5ab0e7bf44..2dc1fd425c 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Volo.CmsKit.Public.Web.csproj +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Volo.CmsKit.Public.Web.csproj @@ -17,6 +17,7 @@ +