diff --git a/app/Activity/Models/Comment.php b/app/Activity/Models/Comment.php index 6efa3df6f..038788afb 100644 --- a/app/Activity/Models/Comment.php +++ b/app/Activity/Models/Comment.php @@ -4,6 +4,7 @@ namespace BookStack\Activity\Models; use BookStack\App\Model; use BookStack\Users\Models\HasCreatorAndUpdater; +use BookStack\Util\HtmlContentFilter; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\MorphTo; @@ -73,4 +74,9 @@ class Comment extends Model implements Loggable { return "Comment #{$this->local_id} (ID: {$this->id}) for {$this->entity_type} (ID: {$this->entity_id})"; } + + public function safeHtml(): string + { + return HtmlContentFilter::removeScriptsFromHtmlString($this->html ?? ''); + } } diff --git a/resources/js/wysiwyg/config.js b/resources/js/wysiwyg/config.js index 36d78b325..fa2df9c11 100644 --- a/resources/js/wysiwyg/config.js +++ b/resources/js/wysiwyg/config.js @@ -339,6 +339,7 @@ export function buildForInput(options) { toolbar: 'bold italic link bullist numlist', content_style: getContentStyle(options), file_picker_types: 'file', + valid_elements: 'p,a[href|title],ol,ul,li,strong,em,br', file_picker_callback: filePickerCallback, init_instance_callback(editor) { addCustomHeadContent(editor.getDoc()); diff --git a/resources/views/comments/comment.blade.php b/resources/views/comments/comment.blade.php index e00307f0f..b507a810b 100644 --- a/resources/views/comments/comment.blade.php +++ b/resources/views/comments/comment.blade.php @@ -1,3 +1,6 @@ +@php + $commentHtml = $comment->safeHtml(); +@endphp <div component="{{ $readOnly ? '' : 'page-comment' }}" option:page-comment:comment-id="{{ $comment->id }}" option:page-comment:comment-local-id="{{ $comment->local_id }}" @@ -71,13 +74,13 @@ <a class="text-muted text-small" href="#comment{{ $comment->parent_id }}">@icon('reply'){{ trans('entities.comment_in_reply_to', ['commentId' => '#' . $comment->parent_id]) }}</a> </p> @endif - {!! $comment->html !!} + {!! $commentHtml !!} </div> @if(!$readOnly && userCan('comment-update', $comment)) <form novalidate refs="page-comment@form" hidden class="content pt-s px-s block"> <div class="form-group description-input"> - <textarea refs="page-comment@input" name="html" rows="3" placeholder="{{ trans('entities.comment_placeholder') }}">{{ $comment->html }}</textarea> + <textarea refs="page-comment@input" name="html" rows="3" placeholder="{{ trans('entities.comment_placeholder') }}">{{ $commentHtml }}</textarea> </div> <div class="form-group text-right"> <button type="button" class="button outline" refs="page-comment@form-cancel">{{ trans('common.cancel') }}</button> diff --git a/tests/Entity/CommentTest.php b/tests/Entity/CommentTest.php index c080359bc..76e014e80 100644 --- a/tests/Entity/CommentTest.php +++ b/tests/Entity/CommentTest.php @@ -82,11 +82,10 @@ class CommentTest extends TestCase public function test_scripts_cannot_be_injected_via_comment_html() { - $this->asAdmin(); $page = $this->entities->page(); $script = '<script>const a = "script";</script><p onclick="1">My lovely comment</p>'; - $this->postJson("/comment/$page->id", [ + $this->asAdmin()->postJson("/comment/$page->id", [ 'html' => $script, ]); @@ -104,6 +103,20 @@ class CommentTest extends TestCase $pageView->assertSee('<p>My lovely comment</p><p>updated</p>'); } + public function test_scripts_are_removed_even_if_already_in_db() + { + $page = $this->entities->page(); + Comment::factory()->create([ + 'html' => '<script>superbadscript</script><p onclick="superbadonclick">scriptincommentest</p>', + 'entity_type' => 'page', 'entity_id' => $page + ]); + + $resp = $this->asAdmin()->get($page->getUrl()); + $resp->assertSee('scriptincommentest', false); + $resp->assertDontSee('superbadscript', false); + $resp->assertDontSee('superbadonclick', false); + } + public function test_reply_comments_are_nested() { $this->asAdmin();