From 353fa40c01eead7df3f4ed20cc9e073f4027d7ff Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 23 Jul 2025 20:30:56 +0200 Subject: [PATCH 01/18] Add test for unclosed script tag --- tests/phpunit/tests/blocks/editor.php | 62 +++++++++++++++++++++++---- 1 file changed, 54 insertions(+), 8 deletions(-) diff --git a/tests/phpunit/tests/blocks/editor.php b/tests/phpunit/tests/blocks/editor.php index 838137fa76f0d..7952fefb7ca32 100644 --- a/tests/phpunit/tests/blocks/editor.php +++ b/tests/phpunit/tests/blocks/editor.php @@ -9,7 +9,6 @@ * @group blocks */ class Tests_Blocks_Editor extends WP_UnitTestCase { - /** * Sets up each test method. */ @@ -631,8 +630,8 @@ function filter_add_preload_paths( $preload_paths, WP_Block_Editor_Context $cont $after = implode( '', wp_scripts()->registered['wp-api-fetch']->extra['after'] ); $this->assertStringContainsString( 'wp.apiFetch.createPreloadingMiddleware', $after ); - $this->assertStringContainsString( '"\/wp\/v2\/blocks"', $after ); - $this->assertStringContainsString( '"\/wp\/v2\/types"', $after ); + $this->assertStringContainsString( '"/wp/v2/blocks"', $after ); + $this->assertStringContainsString( '"/wp/v2/types"', $after ); } /** @@ -697,11 +696,11 @@ public function data_block_editor_rest_api_preload_adds_missing_leading_slash() return array( 'a string without a slash' => array( 'preload_paths' => array( 'wp/v2/blocks' ), - 'expected' => '\/wp\/v2\/blocks', + 'expected' => '/wp/v2/blocks', ), 'a string with a slash' => array( 'preload_paths' => array( '/wp/v2/blocks' ), - 'expected' => '\/wp\/v2\/blocks', + 'expected' => '/wp/v2/blocks', ), 'a string starting with a question mark' => array( 'preload_paths' => array( '?context=edit' ), @@ -709,16 +708,63 @@ public function data_block_editor_rest_api_preload_adds_missing_leading_slash() ), 'an array with a string without a slash' => array( 'preload_paths' => array( array( 'wp/v2/blocks', 'OPTIONS' ) ), - 'expected' => '\/wp\/v2\/blocks', + 'expected' => '/wp/v2/blocks', ), 'an array with a string with a slash' => array( 'preload_paths' => array( array( '/wp/v2/blocks', 'OPTIONS' ) ), - 'expected' => '\/wp\/v2\/blocks', + 'expected' => '/wp/v2/blocks', ), 'an array with a string starting with a question mark' => array( 'preload_paths' => array( array( '?context=edit', 'OPTIONS' ) ), - 'expected' => '\/?context=edit', + 'expected' => '/?context=edit', ), ); } + + /** + * @ticket 62797 + * + * @covers ::block_editor_rest_api_preload + * + * Some valid JSON-encoded data is dangerous to embed in HTML without appropriate + * escaping. This test includes prints an example of such data that would prevent + * the enclosing `' ) ) { - _doing_it_wrong( - __FUNCTION__, - sprintf( - /* translators: 1: #is', '$1', $data ) ); + if ( false !== stripos( $data, '', wp_sanitize_script_attributes( $attributes ) ); + $processor = new WP_HTML_Tag_Processor( $script_tag ); + if ( $processor->next_tag( 'SCRIPT' ) && $processor->set_modifiable_text( $data ) ) { + return $processor->get_updated_html() . "\n"; + } + return sprintf( "%s\n", wp_sanitize_script_attributes( $attributes ), $data ); } From 4b194ef24e93c14d949b2654c4230fccc00096a2 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 23 Jul 2025 20:56:39 +0200 Subject: [PATCH 06/18] Improve closing script tag test --- tests/phpunit/tests/blocks/editor.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/phpunit/tests/blocks/editor.php b/tests/phpunit/tests/blocks/editor.php index 7952fefb7ca32..71d28e62a4e6f 100644 --- a/tests/phpunit/tests/blocks/editor.php +++ b/tests/phpunit/tests/blocks/editor.php @@ -738,7 +738,7 @@ public function test_ensure_preload_data_script_tag_closes() { array( 'methods' => 'GET', 'callback' => function () { - return '' => array( '', 'Comments end in -->' ), 'Comment with --!>' => array( '', 'Invalid but legitimate comments end in --!>' ), - 'SCRIPT with ' => array( '', 'Just a ' ), - 'SCRIPT with ' => array( '', 'beforeafter' ), + 'SCRIPT with ' => array( '', 'Just a ' ), + 'SCRIPT with ' => array( '', 'beforeafter' ), ); } } From b26f45d7bc0ae0454dfa2c8864ae86ba81966253 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 23 Jul 2025 22:35:46 +0200 Subject: [PATCH 08/18] Add dangerous script escaping tests --- .../wpHtmlTagProcessorModifiableText.php | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/phpunit/tests/html-api/wpHtmlTagProcessorModifiableText.php b/tests/phpunit/tests/html-api/wpHtmlTagProcessorModifiableText.php index 75cd9c1bf84af..7ab76ad110f8c 100644 --- a/tests/phpunit/tests/html-api/wpHtmlTagProcessorModifiableText.php +++ b/tests/phpunit/tests/html-api/wpHtmlTagProcessorModifiableText.php @@ -492,4 +492,39 @@ public static function data_unallowed_modifiable_text_updates() { 'SCRIPT with ' => array( '', 'beforeafter' ), ); } + + /** + * Ensures that script tag contents are safely updated. + * + * @ticket 62797 + * + * @dataProvider data_script_tag_text_updates + * + * @param string $html HTML containing a SCRIPT tag to be modified. + * @param string $update Update containing possibly-compromising text. + * @param string $expected Expected result. + */ + public function test_safely_updates_dangerous_javascript_script_tag_contents( string $html, string $update, string $expected ) { + $processor = new WP_HTML_Tag_Processor( $html ); + $this->assertTrue( $processor->next_tag( 'SCRIPT' ) ); + $this->assertTrue( $processor->set_modifiable_text( $update ) ); + $this->assertSame( $expected, $processor->get_updated_html() ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_script_tag_text_updates(): array { + return array( + 'Simple update' => array( '', '{}', '' ), + 'var script;1', 'var script;1' ), + '1/' => array( '', '1/', '' ), + 'var SCRIPT;1', 'var SCRIPT;1' ), + '1/' => array( '', '1/', '' ), + '""' => array( '', '""', '' ), + '""' => array( '', '""', '' ), + ); + } } From 4b9aa42a1681c1518b6a526246b8071016db04dd Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 23 Jul 2025 22:36:37 +0200 Subject: [PATCH 09/18] Apply script tag escaping in case of ` could close the SCRIPT element prematurely. + * + * The text `/' => array( '', '1/', '' ), 'var SCRIPT;1', 'var SCRIPT;1' ), '1/' => array( '', '1/', '' ), '""' => array( '', '""', '' ), '""' => array( '', '""', '' ), + 'Module tag' => array( '', '"' ), + 'Tag with type' => array( '', '"' ), + 'Tag with language' => array( '', '"' ), ); } } From 092eecfae239c606dce49282150e214e8a1a237b Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 30 Jul 2025 19:08:52 +0200 Subject: [PATCH 14/18] Fix boolean => bool return type Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/wp-includes/html-api/class-wp-html-tag-processor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/html-api/class-wp-html-tag-processor.php b/src/wp-includes/html-api/class-wp-html-tag-processor.php index 8a84ab92c9f0b..9ebb1da31ec28 100644 --- a/src/wp-includes/html-api/class-wp-html-tag-processor.php +++ b/src/wp-includes/html-api/class-wp-html-tag-processor.php @@ -3837,7 +3837,7 @@ static function ( $tag_match ) { * * @since {WP_VERSION} * - * @return boolean True if the script tag will be evaluated as JavaScript. + * @return bool True if the script tag will be evaluated as JavaScript. */ public function is_javascript_script_tag(): bool { if ( 'SCRIPT' !== $this->get_tag() || $this->get_namespace() !== 'html' ) { From ed224884e794b346124cd28a97903c2a7c66f1aa Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 30 Jul 2025 19:11:09 +0200 Subject: [PATCH 15/18] Fix and improve tag processor comments --- .../html-api/class-wp-html-tag-processor.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/wp-includes/html-api/class-wp-html-tag-processor.php b/src/wp-includes/html-api/class-wp-html-tag-processor.php index 9ebb1da31ec28..cbbbbdd3d3aaa 100644 --- a/src/wp-includes/html-api/class-wp-html-tag-processor.php +++ b/src/wp-includes/html-api/class-wp-html-tag-processor.php @@ -3882,7 +3882,7 @@ public function is_javascript_script_tag(): bool { * * > A string is a JavaScript MIME type essence match if it is an ASCII case-insensitive * > match for one of the JavaScript MIME type essence strings. - + * * > A JavaScript MIME type is any MIME type whose essence is one of the following: * > * > - application/ecmascript @@ -3902,8 +3902,8 @@ public function is_javascript_script_tag(): bool { * > - text/x-ecmascript * > - text/x-javascript * - * @see https://mimesniff.spec.whatwg.org/#javascript-mime-type * @see https://mimesniff.spec.whatwg.org/#javascript-mime-type-essence-match + * @see https://mimesniff.spec.whatwg.org/#javascript-mime-type */ switch ( strtolower( $type_string ) ) { case 'application/ecmascript': @@ -3925,11 +3925,11 @@ public function is_javascript_script_tag(): bool { return true; /* - * > Otherwise, if the script block's type string is an ASCII case-insensitive match for - * > the string "module", then set el's type to "module". - * - * A module is evaluated as JavaScript - */ + * > Otherwise, if the script block's type string is an ASCII case-insensitive match for + * > the string "module", then set el's type to "module". + * + * A module is evaluated as JavaScript. + */ case 'module': return true; } From 529afadbe562c50e2cc8d866c3642111d0cf3032 Mon Sep 17 00:00:00 2001 From: Jon Surrell Date: Wed, 30 Jul 2025 21:32:14 +0200 Subject: [PATCH 16/18] Improve and add tests --- .../wpHtmlTagProcessorModifiableText.php | 41 +++++++++++-------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/tests/phpunit/tests/html-api/wpHtmlTagProcessorModifiableText.php b/tests/phpunit/tests/html-api/wpHtmlTagProcessorModifiableText.php index 854fbeff1cbd2..6a53d3e1bd479 100644 --- a/tests/phpunit/tests/html-api/wpHtmlTagProcessorModifiableText.php +++ b/tests/phpunit/tests/html-api/wpHtmlTagProcessorModifiableText.php @@ -448,13 +448,14 @@ public static function data_tokens_with_basic_modifiable_text_updates() { * the structure of the containing element, such as in a script or comment. * * @ticket 61617 + * @ticket 62797 * * @dataProvider data_unallowed_modifiable_text_updates * * @param string $html_with_nonempty_modifiable_text Will be used to find the test element. * @param string $invalid_update Update containing possibly-compromising text. */ - public function test_rejects_updates_with_unallowed_substrings( string $html_with_nonempty_modifiable_text, string $invalid_update ) { + public function test_rejects_dangerous_updates( string $html_with_nonempty_modifiable_text, string $invalid_update ) { $processor = new WP_HTML_Tag_Processor( $html_with_nonempty_modifiable_text ); while ( '' === $processor->get_modifiable_text() && $processor->next_token() ) { @@ -486,15 +487,18 @@ public function test_rejects_updates_with_unallowed_substrings( string $html_wit */ public static function data_unallowed_modifiable_text_updates() { return array( - 'Comment with -->' => array( '', 'Comments end in -->' ), - 'Comment with --!>' => array( '', 'Invalid but legitimate comments end in --!>' ), - 'SCRIPT with ' => array( '', 'Just a ' ), - 'SCRIPT with ' => array( '', 'beforeafter' ), + 'Comment with -->' => array( '', 'Comments end in -->' ), + 'Comment with --!>' => array( '', 'Invalid but legitimate comments end in --!>' ), + 'Non-JS SCRIPT with ', '