From 7bfad0f4c7936bf012dbea41bdc7bea6f800de66 Mon Sep 17 00:00:00 2001 From: Engincan VESKE <43685404+EngincanV@users.noreply.github.com> Date: Fri, 28 Apr 2023 12:46:39 +0300 Subject: [PATCH 1/2] CMS Kit: Improve Comments for disallowing external URLs --- .../Comments/CommentPublicAppService.cs | 79 +++++++++---------- .../Comments/CommentPublicAppService_Tests.cs | 25 +++++- 2 files changed, 58 insertions(+), 46 deletions(-) diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo/CmsKit/Public/Comments/CommentPublicAppService.cs b/modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo/CmsKit/Public/Comments/CommentPublicAppService.cs index 4a7bd8be8c..8d378ad925 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo/CmsKit/Public/Comments/CommentPublicAppService.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo/CmsKit/Public/Comments/CommentPublicAppService.cs @@ -25,7 +25,8 @@ namespace Volo.CmsKit.Public.Comments; [RequiresGlobalFeature(typeof(CommentsFeature))] public class CommentPublicAppService : CmsKitPublicAppServiceBase, ICommentPublicAppService { - protected string RegexMarkdownUrlPattern = @"\[[^\]]*\]\((?.*?)\)(?![^\x60]*\x60)"; + protected string RegexUrlPattern = + @"https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()!@:%_\+.~#?&\/\/=]*)"; protected ICommentRepository CommentRepository { get; } protected ICmsUserLookupService CmsUserLookupService { get; } @@ -124,7 +125,41 @@ public class CommentPublicAppService : CmsKitPublicAppServiceBase, ICommentPubli throw new AbpAuthorizationException(); } } + + protected virtual void CheckExternalUrls(string entityType, string text) + { + if (!CmsCommentOptions.AllowedExternalUrls.TryGetValue(entityType, out var allowedExternalUrls)) + { + return; + } + + text = text.Replace("www.", "https://www.").Replace("://https", ""); + + var matches = Regex.Matches(text, RegexUrlPattern, + RegexOptions.Compiled | RegexOptions.IgnoreCase); + + foreach (Match match in matches) + { + if (!match.Success || match.Groups.Count <= 0) + { + continue; + } + var normalizedFullUrl = NormalizeUrl(match.Groups[0].Value); + + if (!allowedExternalUrls.Any(allowedExternalUrl => + normalizedFullUrl.Contains(NormalizeUrl(allowedExternalUrl), StringComparison.OrdinalIgnoreCase))) + { + throw new UserFriendlyException(L["UnAllowedExternalUrlMessage"]); + } + } + } + + private static string NormalizeUrl(string url) + { + return url.Replace("www.", "").RemovePostFix("/"); + } + private List ConvertCommentsToNestedStructure(List comments) { //TODO: I think this method can be optimized if you use dictionaries instead of straight search @@ -156,46 +191,4 @@ public class CommentPublicAppService : CmsKitPublicAppServiceBase, ICommentPubli { return ObjectMapper.Map(comments.Single(c => c.Comment.Id == commentId).Author); } - - private void CheckExternalUrls(string entityType, string text) - { - if (!CmsCommentOptions.AllowedExternalUrls.TryGetValue(entityType, out var allowedExternalUrls)) - { - return; - } - - var matches = Regex.Matches(text, RegexMarkdownUrlPattern, - RegexOptions.Compiled | RegexOptions.IgnoreCase); - - foreach (Match match in matches) - { - if (!match.Success || match.Groups.Count < 2) - { - continue; - } - - var url = NormalizeUrl(match.Groups[1].Value); - if (!IsExternalUrl(url)) - { - continue; - } - - if (!allowedExternalUrls.Any(allowedExternalUrl => - url.Contains(NormalizeUrl(allowedExternalUrl), StringComparison.OrdinalIgnoreCase))) - { - throw new UserFriendlyException(L["UnAllowedExternalUrlMessage"]); - } - } - } - - private static bool IsExternalUrl(string url) - { - return url.StartsWith("https", StringComparison.InvariantCultureIgnoreCase) || - url.StartsWith("http", StringComparison.InvariantCultureIgnoreCase); - } - - private static string NormalizeUrl(string url) - { - return url.Replace("www.", "").RemovePostFix("/"); - } } diff --git a/modules/cms-kit/test/Volo.CmsKit.Application.Tests/Comments/CommentPublicAppService_Tests.cs b/modules/cms-kit/test/Volo.CmsKit.Application.Tests/Comments/CommentPublicAppService_Tests.cs index ddd913a93f..cbb165c1cd 100644 --- a/modules/cms-kit/test/Volo.CmsKit.Application.Tests/Comments/CommentPublicAppService_Tests.cs +++ b/modules/cms-kit/test/Volo.CmsKit.Application.Tests/Comments/CommentPublicAppService_Tests.cs @@ -62,9 +62,28 @@ public class CommentPublicAppService_Tests : CmsKitApplicationTestBase .ShouldBeTrue(); }); } + + [Theory] + [InlineData("https://abp.io/features")] + public async Task CreateAsync_ShouldCreateComment_If_Url_Allowed(string text) + { + _currentUser.Id.Returns(_cmsKitTestData.User2Id); - [Fact] - public async Task CreateAsync_ShouldThrowUserFriendlyException_If_Url_UnAllowed() + await _commentAppService.CreateAsync( + _cmsKitTestData.EntityType1, + _cmsKitTestData.EntityId1, + new CreateCommentInput + { + RepliedCommentId = null, + Text = text + } + ); + } + + [Theory] + [InlineData("[ABP Community](https://community.abp.io/)")] + [InlineData("docs.abp.io")] + public async Task CreateAsync_ShouldThrowUserFriendlyException_If_Url_UnAllowed(string text) { _currentUser.Id.Returns(_cmsKitTestData.User2Id); @@ -75,7 +94,7 @@ public class CommentPublicAppService_Tests : CmsKitApplicationTestBase new CreateCommentInput { RepliedCommentId = null, - Text = "[ABP Community](https://community.abp.io/)", //not allowed URL + Text = text, //not allowed URL } )); } From a92fb193931950f5b25fbb72c0a8465025b08cbd Mon Sep 17 00:00:00 2001 From: Engincan VESKE Date: Mon, 1 May 2023 09:25:39 +0300 Subject: [PATCH 2/2] Update Getting-Started-Single-Layered.md --- docs/en/Getting-Started-Single-Layered.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/en/Getting-Started-Single-Layered.md b/docs/en/Getting-Started-Single-Layered.md index b2a8bc8948..5ccad79649 100644 --- a/docs/en/Getting-Started-Single-Layered.md +++ b/docs/en/Getting-Started-Single-Layered.md @@ -12,6 +12,6 @@ This tutorial explains how to **create and run** a new Single-Layered web application using the ABP Framework. Follow the steps below: -1. [Setup your development environment](Getting-Started-Setup-Environment-Single.md) -2. [Creating a new solution](Getting-Started-Create-Solution-Single.md) -3. [Running the solution](Getting-Started-Running-Solution-Single.md) \ No newline at end of file +1. [Setup your development environment](Getting-Started-Setup-Environment-Single-Layer.md) +2. [Creating a new solution](Getting-Started-Create-Solution-Single-Layer.md) +3. [Running the solution](Getting-Started-Running-Solution-Single-Layer.md)