Skip to content

Commit e8a6e1f

Browse files
committed
Customize: Allow arbitrary CSS in global styles custom CSS.
Relax Global Styles custom CSS filters to allow arbitrary CSS. Escape HTML characters `<>&` in Global Styles data to prevent it from being mangled by post content filters. The data is JSON encoded and stored in `post_content`. Filters operating on `post_content` expect it to contain HTML. Some KSES filters would otherwise remove essential CSS features like the `<custom-ident>` CSS data type because they appear to be HTML tags. [61418] changed STYLE tag generation to use the HTML API for improved safety. Developed in WordPress/wordpress-develop#10641. Props jonsurrell, dmsnell, westonruter, ramonopoly, oandregal, jorgefilipecosta, sabernhardt, soyebsalar01. See #64418. Built from https://develop.svn.wordpress.org/trunk@61486 git-svn-id: http://core.svn.wordpress.org/trunk@60798 1a063a9b-81f0-0310-95a4-ce76da25c4cd
1 parent 0f1223c commit e8a6e1f

File tree

5 files changed

+126
-48
lines changed

5 files changed

+126
-48
lines changed

wp-includes/css/dist/index.php

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@
77
*/
88

99
return array(
10+
array(
11+
'handle' => 'wp-preferences',
12+
'path' => 'preferences/style',
13+
'dependencies' => array('wp-components'),
14+
),
1015
array(
1116
'handle' => 'wp-nux',
1217
'path' => 'nux/style',
@@ -17,11 +22,6 @@
1722
'path' => 'list-reusable-blocks/style',
1823
'dependencies' => array('wp-components'),
1924
),
20-
array(
21-
'handle' => 'wp-preferences',
22-
'path' => 'preferences/style',
23-
'dependencies' => array('wp-components'),
24-
),
2525
array(
2626
'handle' => 'wp-reusable-blocks',
2727
'path' => 'reusable-blocks/style',
@@ -57,36 +57,36 @@
5757
'path' => 'block-directory/style',
5858
'dependencies' => array('wp-block-editor', 'wp-components', 'wp-editor'),
5959
),
60-
array(
61-
'handle' => 'wp-media-utils',
62-
'path' => 'media-utils/style',
63-
'dependencies' => array('wp-components'),
64-
),
6560
array(
6661
'handle' => 'wp-customize-widgets',
6762
'path' => 'customize-widgets/style',
6863
'dependencies' => array('wp-block-editor', 'wp-block-library', 'wp-components', 'wp-media-utils', 'wp-preferences', 'wp-widgets'),
6964
),
70-
array(
71-
'handle' => 'wp-edit-widgets',
72-
'path' => 'edit-widgets/style',
73-
'dependencies' => array('wp-block-editor', 'wp-block-library', 'wp-components', 'wp-media-utils', 'wp-patterns', 'wp-preferences', 'wp-widgets'),
74-
),
7565
array(
7666
'handle' => 'wp-edit-post',
7767
'path' => 'edit-post/style',
7868
'dependencies' => array('wp-block-editor', 'wp-block-library', 'wp-commands', 'wp-components', 'wp-editor', 'wp-preferences', 'wp-widgets'),
7969
),
8070
array(
81-
'handle' => 'wp-block-library',
82-
'path' => 'block-library/style',
83-
'dependencies' => array('wp-block-editor', 'wp-components', 'wp-patterns'),
71+
'handle' => 'wp-edit-widgets',
72+
'path' => 'edit-widgets/style',
73+
'dependencies' => array('wp-block-editor', 'wp-block-library', 'wp-components', 'wp-media-utils', 'wp-patterns', 'wp-preferences', 'wp-widgets'),
74+
),
75+
array(
76+
'handle' => 'wp-media-utils',
77+
'path' => 'media-utils/style',
78+
'dependencies' => array('wp-components'),
8479
),
8580
array(
8681
'handle' => 'wp-editor',
8782
'path' => 'editor/style',
8883
'dependencies' => array('wp-block-editor', 'wp-commands', 'wp-components', 'wp-media-utils', 'wp-patterns', 'wp-preferences'),
8984
),
85+
array(
86+
'handle' => 'wp-block-library',
87+
'path' => 'block-library/style',
88+
'dependencies' => array('wp-block-editor', 'wp-components', 'wp-patterns'),
89+
),
9090
array(
9191
'handle' => 'wp-block-editor',
9292
'path' => 'block-editor/style',

wp-includes/js/dist/script-modules/index.php

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,21 @@
77
*/
88

99
return array(
10+
array(
11+
'id' => '@wordpress/interactivity',
12+
'path' => 'interactivity/index',
13+
'asset' => 'interactivity/index.min.asset.php',
14+
),
15+
array(
16+
'id' => '@wordpress/latex-to-mathml',
17+
'path' => 'latex-to-mathml/index',
18+
'asset' => 'latex-to-mathml/index.min.asset.php',
19+
),
20+
array(
21+
'id' => '@wordpress/latex-to-mathml/loader',
22+
'path' => 'latex-to-mathml/loader',
23+
'asset' => 'latex-to-mathml/loader.min.asset.php',
24+
),
1025
array(
1126
'id' => '@wordpress/interactivity-router',
1227
'path' => 'interactivity-router/index',
@@ -18,40 +33,25 @@
1833
'asset' => 'interactivity-router/full-page.min.asset.php',
1934
),
2035
array(
21-
'id' => '@wordpress/core-abilities',
22-
'path' => 'core-abilities/index',
23-
'asset' => 'core-abilities/index.min.asset.php',
36+
'id' => '@wordpress/abilities',
37+
'path' => 'abilities/index',
38+
'asset' => 'abilities/index.min.asset.php',
2439
),
2540
array(
2641
'id' => '@wordpress/a11y',
2742
'path' => 'a11y/index',
2843
'asset' => 'a11y/index.min.asset.php',
2944
),
3045
array(
31-
'id' => '@wordpress/interactivity',
32-
'path' => 'interactivity/index',
33-
'asset' => 'interactivity/index.min.asset.php',
34-
),
35-
array(
36-
'id' => '@wordpress/abilities',
37-
'path' => 'abilities/index',
38-
'asset' => 'abilities/index.min.asset.php',
46+
'id' => '@wordpress/core-abilities',
47+
'path' => 'core-abilities/index',
48+
'asset' => 'core-abilities/index.min.asset.php',
3949
),
4050
array(
4151
'id' => '@wordpress/route',
4252
'path' => 'route/index',
4353
'asset' => 'route/index.min.asset.php',
4454
),
45-
array(
46-
'id' => '@wordpress/latex-to-mathml',
47-
'path' => 'latex-to-mathml/index',
48-
'asset' => 'latex-to-mathml/index.min.asset.php',
49-
),
50-
array(
51-
'id' => '@wordpress/latex-to-mathml/loader',
52-
'path' => 'latex-to-mathml/loader',
53-
'asset' => 'latex-to-mathml/loader.min.asset.php',
54-
),
5555
array(
5656
'id' => '@wordpress/edit-site-init',
5757
'path' => 'edit-site-init/index',

wp-includes/kses.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2386,7 +2386,13 @@ function wp_filter_global_styles_post( $data ) {
23862386
$data_to_encode = WP_Theme_JSON::remove_insecure_properties( $decoded_data, 'custom' );
23872387

23882388
$data_to_encode['isGlobalStylesUserThemeJSON'] = true;
2389-
return wp_slash( wp_json_encode( $data_to_encode ) );
2389+
/**
2390+
* JSON encode the data stored in post content.
2391+
* Escape characters that are likely to be mangled by HTML filters: "<>&".
2392+
*
2393+
* This matches the escaping in {@see WP_REST_Global_Styles_Controller::prepare_item_for_database()}.
2394+
*/
2395+
return wp_slash( wp_json_encode( $data_to_encode, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ) );
23902396
}
23912397
return $data;
23922398
}

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

Lines changed: 79 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,14 @@ protected function prepare_item_for_database( $request ) {
275275
}
276276
$config['isGlobalStylesUserThemeJSON'] = true;
277277
$config['version'] = WP_Theme_JSON::LATEST_SCHEMA;
278-
$changes->post_content = wp_json_encode( $config );
278+
/**
279+
* JSON encode the data stored in post content.
280+
* Escape characters that are likely to be mangled by HTML filters: "<>&".
281+
*
282+
* This data is later re-encoded by {@see wp_filter_global_styles_post()}.
283+
* The escaping is also applied here as a precaution.
284+
*/
285+
$changes->post_content = wp_json_encode( $config, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP );
279286
}
280287

281288
// Post title.
@@ -659,22 +666,87 @@ public function get_theme_items( $request ) {
659666
/**
660667
* Validate style.css as valid CSS.
661668
*
662-
* Currently just checks for invalid markup.
669+
* Currently just checks that CSS will not break an HTML STYLE tag.
663670
*
664671
* @since 6.2.0
665672
* @since 6.4.0 Changed method visibility to protected.
673+
* @since 7.0.0 Only restricts contents which risk prematurely closing the STYLE element,
674+
* either through a STYLE end tag or a prefix of one which might become a
675+
* full end tag when combined with the contents of other styles.
666676
*
667677
* @param string $css CSS to validate.
668678
* @return true|WP_Error True if the input was validated, otherwise WP_Error.
669679
*/
670680
protected function validate_custom_css( $css ) {
671-
if ( preg_match( '#</?\w+#', $css ) ) {
672-
return new WP_Error(
673-
'rest_custom_css_illegal_markup',
674-
__( 'Markup is not allowed in CSS.' ),
675-
array( 'status' => 400 )
681+
$length = strlen( $css );
682+
for (
683+
$at = strcspn( $css, '<' );
684+
$at < $length;
685+
$at += strcspn( $css, '<', ++$at )
686+
) {
687+
$remaining_strlen = $length - $at;
688+
/**
689+
* Custom CSS text is expected to render inside an HTML STYLE element.
690+
* A STYLE closing tag must not appear within the CSS text because it
691+
* would close the element prematurely.
692+
*
693+
* The text must also *not* end with a partial closing tag (e.g., `<`,
694+
* `</`, … `</style`) because subsequent styles which are concatenated
695+
* could complete it, forming a valid `</style>` tag.
696+
*
697+
* Example:
698+
*
699+
* $style_a = 'p { font-weight: bold; </sty';
700+
* $style_b = 'le> gotcha!';
701+
* $combined = "{$style_a}{$style_b}";
702+
*
703+
* $style_a = 'p { font-weight: bold; </style';
704+
* $style_b = 'p > b { color: red; }';
705+
* $combined = "{$style_a}\n{$style_b}";
706+
*
707+
* Note how in the second example, both of the style contents are benign
708+
* when analyzed on their own. The first style was likely the result of
709+
* improper truncation, while the second is perfectly sound. It was only
710+
* through concatenation that these two scripts combined to form content
711+
* that would have broken out of the containing STYLE element, thus
712+
* corrupting the page and potentially introducing security issues.
713+
*
714+
* @see https://html.spec.whatwg.org/multipage/parsing.html#rawtext-end-tag-name-state
715+
*/
716+
$possible_style_close_tag = 0 === substr_compare(
717+
$css,
718+
'</style',
719+
$at,
720+
min( 7, $remaining_strlen ),
721+
true
676722
);
723+
if ( $possible_style_close_tag ) {
724+
if ( $remaining_strlen < 8 ) {
725+
return new WP_Error(
726+
'rest_custom_css_illegal_markup',
727+
sprintf(
728+
/* translators: %s is the CSS that was provided. */
729+
__( 'The CSS must not end in "%s".' ),
730+
esc_html( substr( $css, $at ) )
731+
),
732+
array( 'status' => 400 )
733+
);
734+
}
735+
736+
if ( 1 === strspn( $css, " \t\f\r\n/>", $at + 7, 1 ) ) {
737+
return new WP_Error(
738+
'rest_custom_css_illegal_markup',
739+
sprintf(
740+
/* translators: %s is the CSS that was provided. */
741+
__( 'The CSS must not contain "%s".' ),
742+
esc_html( substr( $css, $at, 8 ) )
743+
),
744+
array( 'status' => 400 )
745+
);
746+
}
747+
}
677748
}
749+
678750
return true;
679751
}
680752
}

wp-includes/version.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
*
1717
* @global string $wp_version
1818
*/
19-
$wp_version = '7.0-alpha-61485';
19+
$wp_version = '7.0-alpha-61486';
2020

2121
/**
2222
* Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.

0 commit comments

Comments
 (0)