Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/wp-includes/block-supports/custom-css.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ function wp_render_custom_css_support_styles( $parsed_block ) {
return $parsed_block;
}

// Validate CSS doesn't contain HTML markup (same validation as global styles REST API).
if ( preg_match( '#</?\w+#', $custom_css ) ) {
// Validate CSS is safe for a STYLE element (same rules as global styles and Customizer).
if ( is_wp_error( wp_validate_css_for_style_element( $custom_css ) ) ) {
return $parsed_block;
}

Expand Down
13 changes: 13 additions & 0 deletions src/wp-includes/blocks.php
Original file line number Diff line number Diff line change
Expand Up @@ -2075,8 +2075,20 @@ function _filter_block_content_callback( $matches ) {
* @return array The filtered and sanitized block object result.
*/
function filter_block_kses( $block, $allowed_html, $allowed_protocols = array() ) {
// Per-block custom CSS is not HTML; skip KSES and sanitize with strip_tags + validation instead.
$block_custom_css = null;
if ( isset( $block['attrs']['style']['css'] ) && is_string( $block['attrs']['style']['css'] ) ) {
$block_custom_css = $block['attrs']['style']['css'];
$block['attrs']['style']['css'] = '';
}

$block['attrs'] = filter_block_kses_value( $block['attrs'], $allowed_html, $allowed_protocols, $block );

if ( null !== $block_custom_css ) {
$css = wp_strip_all_tags( $block_custom_css );
$block['attrs']['style']['css'] = is_wp_error( wp_validate_css_for_style_element( $css ) ) ? '' : $css;
}

if ( is_array( $block['innerBlocks'] ) ) {
foreach ( $block['innerBlocks'] as $i => $inner_block ) {
$block['innerBlocks'][ $i ] = filter_block_kses( $inner_block, $allowed_html, $allowed_protocols );
Expand All @@ -2092,6 +2104,7 @@ function filter_block_kses( $block, $allowed_html, $allowed_protocols = array()
*
* @since 5.3.1
* @since 6.5.5 Added the `$block_context` parameter.
* @since 7.0.0 Per-block custom CSS (attrs.style.css) is no longer passed here; see filter_block_kses().
*
* @param string[]|string $value The attribute value to filter.
* @param array[]|string $allowed_html An array of allowed HTML elements and attributes,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,10 +153,9 @@ public function value() {
* @since 4.7.0
* @since 4.9.0 Checking for balanced characters has been moved client-side via linting in code editor.
* @since 5.9.0 Renamed `$css` to `$value` for PHP 8 named parameter support.
* @since 7.0.0 Only restricts contents which risk prematurely closing the STYLE element,
* either through a STYLE end tag or a prefix of one which might become a
* full end tag when combined with the contents of other styles.
* @since 7.0.0 Delegates to wp_validate_css_for_style_element().
*
* @see wp_validate_css_for_style_element()
* @see WP_REST_Global_Styles_Controller::validate_custom_css()
*
* @param string $value CSS to validate.
Expand All @@ -166,81 +165,14 @@ public function validate( $value ) {
// Restores the more descriptive, specific name for use within this method.
$css = $value;

$validity = new WP_Error();

$length = strlen( $css );
for (
$at = strcspn( $css, '<' );
$at < $length;
$at += strcspn( $css, '<', ++$at )
) {
$remaining_strlen = $length - $at;
/**
* Custom CSS text is expected to render inside an HTML STYLE element.
* A STYLE closing tag must not appear within the CSS text because it
* would close the element prematurely.
*
* The text must also *not* end with a partial closing tag (e.g., `<`,
* `</`, … `</style`) because subsequent styles which are concatenated
* could complete it, forming a valid `</style>` tag.
*
* Example:
*
* $style_a = 'p { font-weight: bold; </sty';
* $style_b = 'le> gotcha!';
* $combined = "{$style_a}{$style_b}";
*
* $style_a = 'p { font-weight: bold; </style';
* $style_b = 'p > b { color: red; }';
* $combined = "{$style_a}\n{$style_b}";
*
* Note how in the second example, both of the style contents are benign
* when analyzed on their own. The first style was likely the result of
* improper truncation, while the second is perfectly sound. It was only
* through concatenation that these two styles combined to form content
* that would have broken out of the containing STYLE element, thus
* corrupting the page and potentially introducing security issues.
*
* @see https://html.spec.whatwg.org/multipage/parsing.html#rawtext-end-tag-name-state
*/
$possible_style_close_tag = 0 === substr_compare(
$css,
'</style',
$at,
min( 7, $remaining_strlen ),
true
);
if ( $possible_style_close_tag ) {
if ( $remaining_strlen < 8 ) {
$validity->add(
'illegal_markup',
sprintf(
/* translators: %s is the CSS that was provided. */
__( 'The CSS must not end in "%s".' ),
esc_html( substr( $css, $at ) )
)
);
break;
}

if ( 1 === strspn( $css, " \t\f\r\n/>", $at + 7, 1 ) ) {
$validity->add(
'illegal_markup',
sprintf(
/* translators: %s is the CSS that was provided. */
__( 'The CSS must not contain "%s".' ),
esc_html( substr( $css, $at, 8 ) )
)
);
break;
}
}
$result = wp_validate_css_for_style_element( $css );
if ( is_wp_error( $result ) ) {
$validity = new WP_Error();
$validity->add( 'illegal_markup', $result->get_error_message() );
return $validity;
}

if ( ! $validity->has_errors() ) {
$validity = parent::validate( $css );
}
return $validity;
return parent::validate( $css );
}

/**
Expand Down
91 changes: 91 additions & 0 deletions src/wp-includes/formatting.php
Original file line number Diff line number Diff line change
Expand Up @@ -5557,6 +5557,97 @@ function wp_strip_all_tags( $text, $remove_breaks = false ) {
return trim( $text );
}

/**
* Validates that CSS is safe to output inside an HTML STYLE element.
*
* Rejects CSS that contains `</style>` or a partial closing tag (e.g. `</sty`,
* `</style`) that could become a full closing tag when concatenated with other
* styles, which would break out of the STYLE element and risk XSS.
*
* Used by the Global Styles REST API, the Customizer custom CSS setting, and
* per-block custom CSS so all CSS-in-style-element flows share the same rules.
*
* @since 7.0.0
*
* @see WP_REST_Global_Styles_Controller::validate_custom_css()
* @see WP_Customize_Custom_CSS_Setting::validate()
*
* @param string $css CSS to validate.
* @return true|WP_Error True if the CSS is safe for a STYLE element, otherwise WP_Error.
*/
function wp_validate_css_for_style_element( $css ) {
$length = strlen( $css );
for (
$at = strcspn( $css, '<' );
$at < $length;
$at += 1 + strcspn( $css, '<', $at + 1 )
) {
$remaining_strlen = $length - $at;
/**
* Custom CSS text is expected to render inside an HTML STYLE element.
* A STYLE closing tag must not appear within the CSS text because it
* would close the element prematurely.
*
* The text must also *not* end with a partial closing tag (e.g., `<`,
* `</`, … `</style`) because subsequent styles which are concatenated
* could complete it, forming a valid `</style>` tag.
*
* Example:
*
* $style_a = 'p { font-weight: bold; </sty';
* $style_b = 'le> gotcha!';
* $combined = "{$style_a}{$style_b}";
*
* $style_a = 'p { font-weight: bold; </style';
* $style_b = 'p > b { color: red; }';
* $combined = "{$style_a}\n{$style_b}";
*
* Note how in the second example, both of the style contents are benign
* when analyzed on their own. The first style was likely the result of
* improper truncation, while the second is perfectly sound. It was only
* through concatenation that these two styles combined to form content
* that would have broken out of the containing STYLE element, thus
* corrupting the page and potentially introducing security issues.
*
* @link https://html.spec.whatwg.org/multipage/parsing.html#rawtext-end-tag-name-state
*/
$possible_style_close_tag = 0 === substr_compare(
$css,
'</style',
$at,
min( 7, $remaining_strlen ),
true
);
if ( $possible_style_close_tag ) {
if ( $remaining_strlen < 8 ) {
return new WP_Error(
'rest_custom_css_illegal_markup',
sprintf(
/* translators: %s is the CSS that was provided. */
__( 'The CSS must not end in "%s".' ),
esc_html( substr( $css, $at ) )
),
array( 'status' => 400 )
);
}

if ( 1 === strspn( $css, " \t\f\r\n/>", $at + 7, 1 ) ) {
return new WP_Error(
'rest_custom_css_illegal_markup',
sprintf(
/* translators: %s is the CSS that was provided. */
__( 'The CSS must not contain "%s".' ),
esc_html( substr( $css, $at, 8 ) )
),
array( 'status' => 400 )
);
}
}
}

return true;
}

/**
* Sanitizes a string from user input or from the database.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -666,89 +666,20 @@ public function get_theme_items( $request ) {
/**
* Validate style.css as valid CSS.
*
* Currently just checks that CSS will not break an HTML STYLE tag.
* Delegates to wp_validate_css_for_style_element() so global styles, Customizer,
* and per-block custom CSS share the same validation rules.
*
* @since 6.2.0
* @since 6.4.0 Changed method visibility to protected.
* @since 7.0.0 Only restricts contents which risk prematurely closing the STYLE element,
* either through a STYLE end tag or a prefix of one which might become a
* full end tag when combined with the contents of other styles.
* @since 7.0.0 Delegates to wp_validate_css_for_style_element().
*
* @see wp_validate_css_for_style_element()
* @see WP_Customize_Custom_CSS_Setting::validate()
*
* @param string $css CSS to validate.
* @return true|WP_Error True if the input was validated, otherwise WP_Error.
*/
protected function validate_custom_css( $css ) {
$length = strlen( $css );
for (
$at = strcspn( $css, '<' );
$at < $length;
$at += strcspn( $css, '<', ++$at )
) {
$remaining_strlen = $length - $at;
/**
* Custom CSS text is expected to render inside an HTML STYLE element.
* A STYLE closing tag must not appear within the CSS text because it
* would close the element prematurely.
*
* The text must also *not* end with a partial closing tag (e.g., `<`,
* `</`, … `</style`) because subsequent styles which are concatenated
* could complete it, forming a valid `</style>` tag.
*
* Example:
*
* $style_a = 'p { font-weight: bold; </sty';
* $style_b = 'le> gotcha!';
* $combined = "{$style_a}{$style_b}";
*
* $style_a = 'p { font-weight: bold; </style';
* $style_b = 'p > b { color: red; }';
* $combined = "{$style_a}\n{$style_b}";
*
* Note how in the second example, both of the style contents are benign
* when analyzed on their own. The first style was likely the result of
* improper truncation, while the second is perfectly sound. It was only
* through concatenation that these two styles combined to form content
* that would have broken out of the containing STYLE element, thus
* corrupting the page and potentially introducing security issues.
*
* @link https://html.spec.whatwg.org/multipage/parsing.html#rawtext-end-tag-name-state
*/
$possible_style_close_tag = 0 === substr_compare(
$css,
'</style',
$at,
min( 7, $remaining_strlen ),
true
);
if ( $possible_style_close_tag ) {
if ( $remaining_strlen < 8 ) {
return new WP_Error(
'rest_custom_css_illegal_markup',
sprintf(
/* translators: %s is the CSS that was provided. */
__( 'The CSS must not end in "%s".' ),
esc_html( substr( $css, $at ) )
),
array( 'status' => 400 )
);
}

if ( 1 === strspn( $css, " \t\f\r\n/>", $at + 7, 1 ) ) {
return new WP_Error(
'rest_custom_css_illegal_markup',
sprintf(
/* translators: %s is the CSS that was provided. */
__( 'The CSS must not contain "%s".' ),
esc_html( substr( $css, $at, 8 ) )
),
array( 'status' => 400 )
);
}
}
}

return true;
return wp_validate_css_for_style_element( $css );
}
}
Loading