From cd15410b1466e60591ed86ece0a50314dec2dfc8 Mon Sep 17 00:00:00 2001 From: Kosta Date: Thu, 8 Feb 2024 12:05:45 +0100 Subject: [PATCH] Ensure block comments are always sanitized on save and render (#35246) --- .../changelog/fix-sanitize-block-comments | 4 + .../assets/class-verbum-block-utils.php | 112 ++++++++++++++++++ .../assets/class-verbum-gutenberg-editor.php | 87 +------------- .../verbum-comments/class-verbum-comments.php | 9 +- .../class-verbum-block-utils-test.php | 84 +++++++++++++ 5 files changed, 210 insertions(+), 86 deletions(-) create mode 100644 projects/packages/jetpack-mu-wpcom/changelog/fix-sanitize-block-comments create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/assets/class-verbum-block-utils.php create mode 100644 projects/packages/jetpack-mu-wpcom/tests/php/features/verbum-comments/class-verbum-block-utils-test.php diff --git a/projects/packages/jetpack-mu-wpcom/changelog/fix-sanitize-block-comments b/projects/packages/jetpack-mu-wpcom/changelog/fix-sanitize-block-comments new file mode 100644 index 0000000000000..052754757fb02 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/changelog/fix-sanitize-block-comments @@ -0,0 +1,4 @@ +Significance: patch +Type: security + + diff --git a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/assets/class-verbum-block-utils.php b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/assets/class-verbum-block-utils.php new file mode 100644 index 0000000000000..fb280ed501ff7 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/assets/class-verbum-block-utils.php @@ -0,0 +1,112 @@ +register( $allowed_block ); + } + + $filtered_blocks = array(); + $blocks = parse_blocks( $content ); + + foreach ( $blocks as $block ) { + $filtered_blocks[] = new WP_Block( $block, array(), $registry ); + } + + return $filtered_blocks; + } + + /** + * Render blocks in the comment content + * Filters blocks that aren't allowed + * + * @param string $comment_content - Text of the comment. + * @return string + */ + public static function render_verbum_blocks( $comment_content ) { + if ( ! has_blocks( $comment_content ) ) { + return $comment_content; + } + + $blocks = self::filter_blocks( $comment_content ); + $comment_content = ''; + + foreach ( $blocks as $block ) { + $comment_content .= $block->render(); + } + + return $comment_content; + } + + /** + * Get a list of allowed blocks by looking at the allowed comment tags + * + * @return string[] + */ + public static function get_allowed_blocks() { + global $allowedtags; + + $allowed_blocks = array( 'core/paragraph', 'core/list', 'core/code', 'core/list-item', 'core/quote', 'core/image', 'core/embed' ); + $convert = array( + 'blockquote' => 'core/quote', + 'h1' => 'core/heading', + 'h2' => 'core/heading', + 'h3' => 'core/heading', + 'img' => 'core/image', + 'ul' => 'core/list', + 'ol' => 'core/list', + 'pre' => 'core/code', + ); + + foreach ( array_keys( $allowedtags ) as $tag ) { + if ( isset( $convert[ $tag ] ) ) { + $allowed_blocks[] = $convert[ $tag ]; + } + } + + return $allowed_blocks; + } +} diff --git a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/assets/class-verbum-gutenberg-editor.php b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/assets/class-verbum-gutenberg-editor.php index 164c5633ae57f..6a9ac17801cc2 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/assets/class-verbum-gutenberg-editor.php +++ b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/assets/class-verbum-gutenberg-editor.php @@ -7,6 +7,8 @@ declare( strict_types = 1 ); +require_once __DIR__ . '/class-verbum-block-utils.php'; + /** * Verbum_Gutenberg_Editor is responsible for loading the Gutenberg editor for comments. * @@ -29,10 +31,11 @@ function () { }, 9999 ); + add_filter( 'init', array( $this, 'remove_strict_kses_filters' ) ); - add_filter( 'comment_text', array( $this, 'render_verbum_blocks' ) ); - add_filter( 'pre_comment_content', array( $this, 'remove_blocks' ) ); add_filter( 'wp_enqueue_scripts', array( $this, 'enqueue_assets' ) ); + add_filter( 'comment_text', array( \Verbum_Block_Utils::class, 'render_verbum_blocks' ) ); + add_filter( 'pre_comment_content', array( \Verbum_Block_Utils::class, 'remove_blocks' ) ); } /** @@ -59,84 +62,4 @@ public function enqueue_assets() { $vbe_cache_buster ); } - - /** - * Render blocks in the comment content - * Filters blocks that aren't allowed - * - * @param string $comment_content - Text of the comment. - * @return string - */ - public function render_verbum_blocks( $comment_content ) { - if ( ! has_blocks( $comment_content ) ) { - return $comment_content; - } - - $blocks = parse_blocks( $comment_content ); - $comment_content = ''; - - $allowed_blocks = self::get_allowed_blocks(); - foreach ( $blocks as $block ) { - if ( in_array( $block['blockName'], $allowed_blocks, true ) ) { - $comment_content .= render_block( $block ); - } - } - - return $comment_content; - } - - /** - * Remove blocks that aren't allowed - * - * @param string $content - Text of the comment. - * @return string - */ - public function remove_blocks( $content ) { - if ( ! has_blocks( $content ) ) { - return $content; - } - - $allowed_blocks = self::get_allowed_blocks(); - // The block attributes come slashed and `parse_blocks` won't be able to parse them. - $content = wp_unslash( $content ); - $blocks = parse_blocks( $content ); - $output = ''; - - foreach ( $blocks as $block ) { - if ( in_array( $block['blockName'], $allowed_blocks, true ) ) { - $output .= serialize_block( $block ); - } - } - - return ltrim( $output ); - } - - /** - * Get a list of allowed blocks by looking at the allowed comment tags - * - * @return string[] - */ - public static function get_allowed_blocks() { - global $allowedtags; - - $allowed_blocks = array( 'core/paragraph', 'core/list', 'core/code', 'core/list-item', 'core/quote', 'core/image', 'core/embed' ); - $convert = array( - 'blockquote' => 'core/quote', - 'h1' => 'core/heading', - 'h2' => 'core/heading', - 'h3' => 'core/heading', - 'img' => 'core/image', - 'ul' => 'core/list', - 'ol' => 'core/list', - 'pre' => 'core/code', - ); - - foreach ( array_keys( $allowedtags ) as $tag ) { - if ( isset( $convert[ $tag ] ) ) { - $allowed_blocks[] = $convert[ $tag ]; - } - } - - return $allowed_blocks; - } } diff --git a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/class-verbum-comments.php b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/class-verbum-comments.php index c27e4aef70059..33d5cc0ed055c 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/class-verbum-comments.php +++ b/projects/packages/jetpack-mu-wpcom/src/features/verbum-comments/class-verbum-comments.php @@ -13,6 +13,7 @@ require_once __DIR__ . '/assets/class-wpcom-rest-api-v2-verbum-auth.php'; require_once __DIR__ . '/assets/class-wpcom-rest-api-v2-verbum-oembed.php'; require_once __DIR__ . '/assets/class-verbum-gutenberg-editor.php'; +require_once __DIR__ . '/assets/class-verbum-block-utils.php'; /** * Verbum Comments Experience @@ -247,7 +248,7 @@ public function enqueue_assets() { 'enableSubscriptionModal' => boolval( $this->should_show_subscription_modal() ), 'currentLocale' => $locale, 'isJetpackComments' => is_jetpack_comments(), - 'allowedBlocks' => \Verbum_Gutenberg_Editor::get_allowed_blocks(), + 'allowedBlocks' => \Verbum_Block_Utils::get_allowed_blocks(), 'embedNonce' => wp_create_nonce( 'embed_nonce' ), 'verbumBundleUrl' => plugins_url( 'dist/index.js', __FILE__ ), 'isRTL' => is_rtl( $locale ), @@ -560,9 +561,9 @@ public function should_load_gutenberg_comments() { return false; } - $blog_id = verbum_get_blog_id(); - $e2e_tests = has_blog_sticker( 'a8c-e2e-test-blog', $blog_id ); - $has_blocks_flag = has_blog_sticker( 'verbum-block-comments', $blog_id ); + $blog_id = $this->blog_id; + $e2e_tests = function_exists( 'has_blog_sticker' ) && has_blog_sticker( 'a8c-e2e-test-blog', $blog_id ); + $has_blocks_flag = function_exists( 'has_blog_sticker' ) && has_blog_sticker( 'verbum-block-comments', $blog_id ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended $gutenberg_query_param = isset( $_GET['verbum_gutenberg'] ) ? intval( $_GET['verbum_gutenberg'] ) : null; // This will release to 50% of sites. diff --git a/projects/packages/jetpack-mu-wpcom/tests/php/features/verbum-comments/class-verbum-block-utils-test.php b/projects/packages/jetpack-mu-wpcom/tests/php/features/verbum-comments/class-verbum-block-utils-test.php new file mode 100644 index 0000000000000..0383c1f069d64 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/tests/php/features/verbum-comments/class-verbum-block-utils-test.php @@ -0,0 +1,84 @@ +assertEquals( $comment_content, $filtered_content ); + } + + /** + * Ensure blocks are filtered when 'render_verbum_blocks' is applied + * + * @covers Verbum_Block_Utils::render_verbum_blocks + */ + public function test_comment_text_block_sanitization() { + $comment_content = 'Testing'; + $filtered_content = Verbum_Block_Utils::render_verbum_blocks( $comment_content ); + $this->assertEquals( 'Testing', $filtered_content ); + } + + /** + * Ensure blocks are rendered properly + * + * @covers Verbum_Block_Utils::render_verbum_blocks + */ + public function test_comment_text_block_sanitization_sanity_check() { + $comment_content = '

test

something

someone
'; + $filtered_content = preg_replace( '/\R+/', '', Verbum_Block_Utils::render_verbum_blocks( $comment_content ) ); + + $expected_content = '

test

something

someone
'; + $this->assertEquals( $expected_content, $filtered_content ); + } + + /** + * Ensure innerBlocks are filtered when 'render_verbum_blocks' is applied + * + * @covers Verbum_Block_Utils::render_verbum_blocks + */ + public function test_comment_text_block_sanitization_inner_blocks() { + $comment_content = ''; + $filtered_content = Verbum_Block_Utils::render_verbum_blocks( $comment_content ); + $this->assertSame( '', $filtered_content ); + } + + /** + * Ensure string comments are not modified when 'pre_comment_content' is applied + * + * @covers Verbum_Block_Utils::remove_blocks + */ + public function test_pre_comment_content_string_comment() { + $comment_content = 'This is a test comment'; + $filtered_content = Verbum_Block_Utils::remove_blocks( $comment_content ); + $this->assertEquals( $comment_content, $filtered_content ); + } + + /** + * Ensure blocks are filtered when 'pre_comment_content' is applied + * + * @covers Verbum_Block_Utils::remove_blocks + */ + public function test_pre_comment_content__block_sanitization() { + $comment_content = 'Testing'; + $filtered_content = Verbum_Block_Utils::remove_blocks( $comment_content ); + $this->assertEquals( 'Testing', $filtered_content ); + } +}