Skip to content

Commit c0edfab

Browse files
committed
Merge branch 'trunk' of https://github.com/WordPress/wordpress-develop into trac-48456-codemirror-v5-upgrade
2 parents 3048f37 + d188679 commit c0edfab

9 files changed

Lines changed: 220 additions & 67 deletions

File tree

src/wp-admin/includes/class-wp-comments-list-table.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ public function prepare_items() {
155155
'number' => $number,
156156
'post_id' => $post_id,
157157
'type' => $comment_type,
158+
'type__not_in' => array( 'note' ),
158159
'orderby' => $orderby,
159160
'order' => $order,
160161
'post_type' => $post_type,

src/wp-admin/includes/class-wp-list-table.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,14 +80,14 @@ class WP_List_Table {
8080
protected $_column_headers;
8181

8282
/**
83-
* {@internal Missing Summary}
83+
* List of private properties made readable for backward compatibility.
8484
*
8585
* @var array
8686
*/
8787
protected $compat_fields = array( '_args', '_pagination_args', 'screen', '_actions', '_pagination' );
8888

8989
/**
90-
* {@internal Missing Summary}
90+
* List of private/protected methods made readable for backward compatibility.
9191
*
9292
* @var array
9393
*/
@@ -280,7 +280,7 @@ public function __call( $name, $arguments ) {
280280
}
281281

282282
/**
283-
* Checks the current user's permissions
283+
* Checks the current user's permissions.
284284
*
285285
* @since 3.1.0
286286
* @abstract

src/wp-includes/customize/class-wp-customize-custom-css-setting.php

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,11 @@ public function value() {
153153
* @since 4.7.0
154154
* @since 4.9.0 Checking for balanced characters has been moved client-side via linting in code editor.
155155
* @since 5.9.0 Renamed `$css` to `$value` for PHP 8 named parameter support.
156+
* @since 7.0.0 Only restricts contents which risk prematurely closing the STYLE element,
157+
* either through a STYLE end tag or a prefix of one which might become a
158+
* full end tag when combined with the contents of other styles.
159+
*
160+
* @see WP_REST_Global_Styles_Controller::validate_custom_css()
156161
*
157162
* @param string $value CSS to validate.
158163
* @return true|WP_Error True if the input was validated, otherwise WP_Error.
@@ -163,8 +168,73 @@ public function validate( $value ) {
163168

164169
$validity = new WP_Error();
165170

166-
if ( preg_match( '#</?\w+#', $css ) ) {
167-
$validity->add( 'illegal_markup', __( 'Markup is not allowed in CSS.' ) );
171+
$length = strlen( $css );
172+
for (
173+
$at = strcspn( $css, '<' );
174+
$at < $length;
175+
$at += strcspn( $css, '<', ++$at )
176+
) {
177+
$remaining_strlen = $length - $at;
178+
/**
179+
* Custom CSS text is expected to render inside an HTML STYLE element.
180+
* A STYLE closing tag must not appear within the CSS text because it
181+
* would close the element prematurely.
182+
*
183+
* The text must also *not* end with a partial closing tag (e.g., `<`,
184+
* `</`, … `</style`) because subsequent styles which are concatenated
185+
* could complete it, forming a valid `</style>` tag.
186+
*
187+
* Example:
188+
*
189+
* $style_a = 'p { font-weight: bold; </sty';
190+
* $style_b = 'le> gotcha!';
191+
* $combined = "{$style_a}{$style_b}";
192+
*
193+
* $style_a = 'p { font-weight: bold; </style';
194+
* $style_b = 'p > b { color: red; }';
195+
* $combined = "{$style_a}\n{$style_b}";
196+
*
197+
* Note how in the second example, both of the style contents are benign
198+
* when analyzed on their own. The first style was likely the result of
199+
* improper truncation, while the second is perfectly sound. It was only
200+
* through concatenation that these two styles combined to form content
201+
* that would have broken out of the containing STYLE element, thus
202+
* corrupting the page and potentially introducing security issues.
203+
*
204+
* @see https://html.spec.whatwg.org/multipage/parsing.html#rawtext-end-tag-name-state
205+
*/
206+
$possible_style_close_tag = 0 === substr_compare(
207+
$css,
208+
'</style',
209+
$at,
210+
min( 7, $remaining_strlen ),
211+
true
212+
);
213+
if ( $possible_style_close_tag ) {
214+
if ( $remaining_strlen < 8 ) {
215+
$validity->add(
216+
'illegal_markup',
217+
sprintf(
218+
/* translators: %s is the CSS that was provided. */
219+
__( 'The CSS must not end in "%s".' ),
220+
esc_html( substr( $css, $at ) )
221+
)
222+
);
223+
break;
224+
}
225+
226+
if ( 1 === strspn( $css, " \t\f\r\n/>", $at + 7, 1 ) ) {
227+
$validity->add(
228+
'illegal_markup',
229+
sprintf(
230+
/* translators: %s is the CSS that was provided. */
231+
__( 'The CSS must not contain "%s".' ),
232+
esc_html( substr( $css, $at, 8 ) )
233+
)
234+
);
235+
break;
236+
}
237+
}
168238
}
169239

170240
if ( ! $validity->has_errors() ) {

src/wp-includes/rest-api/endpoints/class-wp-rest-global-styles-controller.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -674,6 +674,8 @@ public function get_theme_items( $request ) {
674674
* either through a STYLE end tag or a prefix of one which might become a
675675
* full end tag when combined with the contents of other styles.
676676
*
677+
* @see WP_Customize_Custom_CSS_Setting::validate()
678+
*
677679
* @param string $css CSS to validate.
678680
* @return true|WP_Error True if the input was validated, otherwise WP_Error.
679681
*/
@@ -707,7 +709,7 @@ protected function validate_custom_css( $css ) {
707709
* Note how in the second example, both of the style contents are benign
708710
* when analyzed on their own. The first style was likely the result of
709711
* improper truncation, while the second is perfectly sound. It was only
710-
* through concatenation that these two scripts combined to form content
712+
* through concatenation that these two styles combined to form content
711713
* that would have broken out of the containing STYLE element, thus
712714
* corrupting the page and potentially introducing security issues.
713715
*

tests/phpunit/tests/admin/wpCommentsListTable.php

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -218,30 +218,61 @@ public function test_get_views_should_return_views_by_default() {
218218
* Verify that the comments table never shows the note comment_type.
219219
*
220220
* @ticket 64198
221+
* @ticket 64474
222+
*
223+
* @dataProvider data_comment_type
224+
*
225+
* @param string $comment_type The comment_type parameter value to test.
221226
*/
222-
public function test_comments_list_table_does_not_show_note_comment_type() {
223-
$post_id = self::factory()->post->create();
224-
$note_id = self::factory()->comment->create(
227+
public function test_comments_list_table_does_not_show_note_comment_type( string $comment_type ) {
228+
$post_id = self::factory()->post->create();
229+
self::factory()->comment->create(
225230
array(
226231
'comment_post_ID' => $post_id,
227232
'comment_content' => 'This is a note.',
228233
'comment_type' => 'note',
229234
'comment_approved' => '1',
235+
'comment_date' => '2024-01-01 10:00:00',
236+
'comment_date_gmt' => '2024-01-01 10:00:00',
230237
)
231238
);
232-
$comment_id = self::factory()->comment->create(
239+
$regular_comment_id = self::factory()->comment->create(
233240
array(
234241
'comment_post_ID' => $post_id,
235242
'comment_content' => 'This is a regular comment.',
236243
'comment_type' => '',
237244
'comment_approved' => '1',
245+
'comment_date' => '2024-01-01 11:00:00',
246+
'comment_date_gmt' => '2024-01-01 11:00:00',
247+
)
248+
);
249+
$pingback_comment_id = self::factory()->comment->create(
250+
array(
251+
'comment_post_ID' => $post_id,
252+
'comment_content' => 'This is a pingback comment.',
253+
'comment_type' => '',
254+
'comment_approved' => '1',
255+
'comment_date' => '2024-01-01 12:00:00',
256+
'comment_date_gmt' => '2024-01-01 12:00:00',
238257
)
239258
);
240-
// Request the note comment type.
241-
$_REQUEST['comment_type'] = 'note';
259+
$_REQUEST['comment_type'] = $comment_type;
242260
$this->table->prepare_items();
243261
$items = $this->table->items;
244-
$this->assertCount( 1, $items );
245-
$this->assertEquals( $comment_id, $items[0]->comment_ID );
262+
$this->assertCount( 2, $items );
263+
$this->assertEquals( $pingback_comment_id, $items[0]->comment_ID );
264+
$this->assertEquals( $regular_comment_id, $items[1]->comment_ID );
265+
}
266+
267+
/**
268+
* Data provider for test_comments_list_table_does_not_show_note_comment_type().
269+
*
270+
* @return array<string, string[]>
271+
*/
272+
public function data_comment_type(): array {
273+
return array(
274+
'note type explicitly requested' => array( 'note' ),
275+
'all type requested' => array( 'all' ),
276+
);
246277
}
247278
}

tests/phpunit/tests/customize/custom-css-setting.php

Lines changed: 102 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,31 @@ public function filter_update_custom_css_data( $data, $args ) {
374374
return $data;
375375
}
376376

377+
/**
378+
* Ensure that dangerous STYLE tag contents do not break HTML output.
379+
*
380+
* @ticket 64418
381+
* @covers ::wp_update_custom_css_post
382+
* @covers ::wp_custom_css_cb
383+
*/
384+
public function test_wp_custom_css_cb_escapes_dangerous_html() {
385+
wp_update_custom_css_post(
386+
'*::before { content: "</style><script>alert(1)</script>"; }',
387+
array(
388+
'stylesheet' => $this->setting->stylesheet,
389+
)
390+
);
391+
$output = get_echo( 'wp_custom_css_cb' );
392+
$expected =
393+
<<<'HTML'
394+
<style id="wp-custom-css">
395+
*::before { content: "\3c\2fstyle><script>alert(1)</script>"; }
396+
</style>
397+
398+
HTML;
399+
$this->assertEqualHTML( $expected, $output );
400+
}
401+
377402
/**
378403
* Tests that validation errors are caught appropriately.
379404
*
@@ -382,8 +407,7 @@ public function filter_update_custom_css_data( $data, $args ) {
382407
*
383408
* @covers WP_Customize_Custom_CSS_Setting::validate
384409
*/
385-
public function test_validate() {
386-
410+
public function test_validate_basic_css() {
387411
// Empty CSS throws no errors.
388412
$result = $this->setting->validate( '' );
389413
$this->assertTrue( $result );
@@ -393,9 +417,84 @@ public function test_validate() {
393417
$result = $this->setting->validate( $basic_css );
394418
$this->assertTrue( $result );
395419

396-
// Check for markup.
420+
// Check for illegal closing STYLE tag.
397421
$unclosed_comment = $basic_css . '</style>';
398422
$result = $this->setting->validate( $unclosed_comment );
399423
$this->assertArrayHasKey( 'illegal_markup', $result->errors );
400424
}
425+
426+
/**
427+
* @ticket 64418
428+
* @covers WP_Customize_Custom_CSS_Setting::validate
429+
*/
430+
public function test_validate_accepts_css_property_at_rule() {
431+
$css =
432+
<<<'CSS'
433+
@property --animate {
434+
syntax: "<custom-ident>";
435+
inherits: true;
436+
initial-value: false;
437+
}
438+
CSS;
439+
$this->assertTrue( $this->setting->validate( $css ) );
440+
}
441+
442+
/**
443+
* @ticket 64418
444+
* @covers ::wp_update_custom_css_post
445+
* @covers ::wp_custom_css_cb
446+
*/
447+
public function test_save_and_print_property_at_rule() {
448+
$css =
449+
<<<'CSS'
450+
@property --animate {
451+
syntax: "<custom-ident>";
452+
inherits: true;
453+
initial-value: false;
454+
}
455+
CSS;
456+
wp_update_custom_css_post( $css, array( 'stylesheet' => $this->setting->stylesheet ) );
457+
$output = get_echo( 'wp_custom_css_cb' );
458+
$expected = "<style id='wp-custom-css'>\n{$css}\n</style>\n";
459+
$this->assertEqualHTML( $expected, $output );
460+
}
461+
462+
/**
463+
* @dataProvider data_custom_css_disallowed
464+
*
465+
* @ticket 64418
466+
* @covers WP_Customize_Custom_CSS_Setting::validate
467+
*/
468+
public function test_validate_prevents( $css, $expected_error_message ) {
469+
$result = $this->setting->validate( $css );
470+
$this->assertWPError( $result );
471+
$this->assertSame( $expected_error_message, $result->get_error_message() );
472+
}
473+
474+
/**
475+
* Data provider.
476+
*
477+
* @return array<string, string[]>
478+
*/
479+
public static function data_custom_css_disallowed(): array {
480+
return array(
481+
'style close tag' => array( 'css…</style>…css', 'The CSS must not contain "&lt;/style&gt;".' ),
482+
'style close tag upper case' => array( '</STYLE>', 'The CSS must not contain "&lt;/STYLE&gt;".' ),
483+
'style close tag mixed case' => array( '</sTyLe>', 'The CSS must not contain "&lt;/sTyLe&gt;".' ),
484+
'style close tag in comment' => array( '/*</style>*/', 'The CSS must not contain "&lt;/style&gt;".' ),
485+
'style close tag (/)' => array( '</style/', 'The CSS must not contain "&lt;/style/".' ),
486+
'style close tag (\t)' => array( "</style\t", "The CSS must not contain \"&lt;/style\t\"." ),
487+
'style close tag (\f)' => array( "</style\f", "The CSS must not contain \"&lt;/style\f\"." ),
488+
'style close tag (\r)' => array( "</style\r", "The CSS must not contain \"&lt;/style\r\"." ),
489+
'style close tag (\n)' => array( "</style\n", "The CSS must not contain \"&lt;/style\n\"." ),
490+
'style close tag (" ")' => array( '</style ', 'The CSS must not contain "&lt;/style ".' ),
491+
'truncated "<"' => array( '<', 'The CSS must not end in "&lt;".' ),
492+
'truncated "</"' => array( '</', 'The CSS must not end in "&lt;/".' ),
493+
'truncated "</s"' => array( '</s', 'The CSS must not end in "&lt;/s".' ),
494+
'truncated "</ST"' => array( '</ST', 'The CSS must not end in "&lt;/ST".' ),
495+
'truncated "</sty"' => array( '</sty', 'The CSS must not end in "&lt;/sty".' ),
496+
'truncated "</STYL"' => array( '</STYL', 'The CSS must not end in "&lt;/STYL".' ),
497+
'truncated "</stYle"' => array( '</stYle', 'The CSS must not end in "&lt;/stYle".' ),
498+
);
499+
}
401500
}

tests/phpunit/tests/dependencies/scripts.php

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1878,22 +1878,6 @@ public function test_concatenate_with_blocking_script_before_and_after_script_wi
18781878
$this->assertEqualHTML( $expected, $print_scripts, '<body>', 'Scripts are being incorrectly concatenated when a main script is registered as deferred after other blocking scripts are registered. Deferred scripts should not be part of the script concat loader query string. ' );
18791879
}
18801880

1881-
/**
1882-
* @ticket 42804
1883-
*/
1884-
public function test_wp_enqueue_script_with_html5_support_does_not_contain_type_attribute() {
1885-
global $wp_version;
1886-
1887-
$GLOBALS['wp_scripts'] = new WP_Scripts();
1888-
$GLOBALS['wp_scripts']->default_version = get_bloginfo( 'version' );
1889-
1890-
wp_enqueue_script( 'empty-deps-no-version', 'example.com' );
1891-
1892-
$expected = "<script src='http://example.com?ver={$wp_version}' id='empty-deps-no-version-js'></script>\n";
1893-
1894-
$this->assertEqualHTML( $expected, get_echo( 'wp_print_scripts' ) );
1895-
}
1896-
18971881
/**
18981882
* Test the different protocol references in wp_enqueue_script
18991883
*

tests/phpunit/tests/dependencies/wpLocalizeScript.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,6 @@ public function test_wp_localize_script_works_before_enqueue_script() {
4646
* @covers ::wp_localize_script
4747
*/
4848
public function test_wp_localize_script_outputs_safe_json() {
49-
add_theme_support( 'html5', array( 'script' ) );
50-
5149
$path = '/test.js';
5250
$base_url = site_url( $path );
5351

0 commit comments

Comments
 (0)