From ee92e93f7941ce74f07624e548871293b5907712 Mon Sep 17 00:00:00 2001 From: whyisjake Date: Thu, 12 Dec 2019 18:52:47 +0000 Subject: [PATCH] Ensure that a user can publish_posts before making a post sticky. Props: danielbachhuber, whyisjake, peterwilson, xknown. Prevent stored XSS through wp_targeted_link_rel(). Props: vortfu, whyisjake, peterwilsoncc, xknown, SergeyBiryukov, flaviozavan. Update wp_kses_bad_protocol() to recognize : on uri attributes, wp_kses_bad_protocol() makes sure to validate that uri attributes don't contain invalid/or not allowed protocols. While this works fine in most cases, there's a risk that by using the colon html5 named entity, one is able to bypass this function. Brings r46895 to the 5.3 branch. Props: xknown, nickdaugherty, peterwilsoncc. Prevent stored XSS in the block editor. Brings r46896 to the 5.3 branch. Prevent escaped unicode characters become unescaped in unsafe HTML during JSON decoding. Props: aduth, epiqueras. Built from https://develop.svn.wordpress.org/branches/5.0@46915 git-svn-id: http://core.svn.wordpress.org/branches/5.0@46715 1a063a9b-81f0-0310-95a4-ce76da25c4cd --- wp-includes/blocks.php | 230 +++++++++++++++++- wp-includes/default-filters.php | 49 ++-- wp-includes/formatting.php | 25 ++ wp-includes/kses.php | 6 +- .../class-wp-rest-posts-controller.php | 6 +- 5 files changed, 283 insertions(+), 33 deletions(-) diff --git a/wp-includes/blocks.php b/wp-includes/blocks.php index 6571676090..721c93bd3c 100644 --- a/wp-includes/blocks.php +++ b/wp-includes/blocks.php @@ -74,11 +74,11 @@ function has_blocks( $post = null ) { * @since 5.0.0 * @see parse_blocks() * - * @param string $block_type Full Block type to look for. + * @param string $block_name Full Block type to look for. * @param int|string|WP_Post|null $post Optional. Post content, post ID, or post object. Defaults to global $post. * @return bool Whether the post content contains the specified block. */ -function has_block( $block_type, $post = null ) { +function has_block( $block_name, $post = null ) { if ( ! has_blocks( $post ) ) { return false; } @@ -90,7 +90,30 @@ function has_block( $block_type, $post = null ) { } } - return false !== strpos( $post, '', $serialized_block_name, $serialized_attributes ); + } + + return sprintf( + '%s', + $serialized_block_name, + $serialized_attributes, + $block_content, + $serialized_block_name + ); +} + +/** + * Returns the content of a block, including comment delimiters, serializing all + * attributes from the given parsed block. + * + * This should be used when preparing a block to be saved to post content. + * Prefer `render_block` when preparing a block for display. Unlike + * `render_block`, this does not evaluate a block's `render_callback`, and will + * instead preserve the markup as parsed. + * + * @since 5.3.1 + * + * @param WP_Block_Parser_Block $block A single parsed block object. + * @return string String of rendered HTML. + */ +function serialize_block( $block ) { + $block_content = ''; + + $index = 0; + foreach ( $block['innerContent'] as $chunk ) { + $block_content .= is_string( $chunk ) ? $chunk : serialize_block( $block['innerBlocks'][ $index++ ] ); + } + + if ( ! is_array( $block['attrs'] ) ) { + $block['attrs'] = array(); + } + + return get_comment_delimited_block_content( + $block['blockName'], + $block['attrs'], + $block_content + ); +} + +/** + * Returns a joined string of the aggregate serialization of the given parsed + * blocks. + * + * @since 5.3.1 + * + * @param WP_Block_Parser_Block[] $blocks Parsed block objects. + * @return string String of rendered HTML. + */ +function serialize_blocks( $blocks ) { + return implode( '', array_map( 'serialize_block', $blocks ) ); +} + +/** + * Filters and sanitizes block content to remove non-allowable HTML from + * parsed block attribute values. + * + * @since 5.3.1 + * + * @param string $text Text that may contain block content. + * @param array[]|string $allowed_html An array of allowed HTML elements + * and attributes, or a context name + * such as 'post'. + * @param string[] $allowed_protocols Array of allowed URL protocols. + * @return string The filtered and sanitized content result. + */ +function filter_block_content( $text, $allowed_html = 'post', $allowed_protocols = array() ) { + $result = ''; + + $blocks = parse_blocks( $text ); + foreach ( $blocks as $block ) { + $block = filter_block_kses( $block, $allowed_html, $allowed_protocols ); + $result .= serialize_block( $block ); + } + + return $result; +} + +/** + * Filters and sanitizes a parsed block to remove non-allowable HTML from block + * attribute values. + * + * @since 5.3.1 + * + * @param WP_Block_Parser_Block $block The parsed block object. + * @param array[]|string $allowed_html An array of allowed HTML + * elements and attributes, or a + * context name such as 'post'. + * @param string[] $allowed_protocols Allowed URL protocols. + * @return array The filtered and sanitized block object result. + */ +function filter_block_kses( $block, $allowed_html, $allowed_protocols = array() ) { + $block['attrs'] = filter_block_kses_value( $block['attrs'], $allowed_html, $allowed_protocols ); + + if ( is_array( $block['innerBlocks'] ) ) { + foreach ( $block['innerBlocks'] as $i => $inner_block ) { + $block['innerBlocks'][ $i ] = filter_block_kses( $inner_block, $allowed_html, $allowed_protocols ); + } + } + + return $block; +} + +/** + * Filters and sanitizes a parsed block attribute value to remove non-allowable + * HTML. + * + * @since 5.3.1 + * + * @param mixed $value The attribute value to filter. + * @param array[]|string $allowed_html An array of allowed HTML elements + * and attributes, or a context name + * such as 'post'. + * @param string[] $allowed_protocols Array of allowed URL protocols. + * @return array The filtered and sanitized result. + */ +function filter_block_kses_value( $value, $allowed_html, $allowed_protocols = array() ) { + if ( is_array( $value ) ) { + foreach ( $value as $key => $inner_value ) { + $filtered_key = filter_block_kses_value( $key, $allowed_html, $allowed_protocols ); + $filtered_value = filter_block_kses_value( $inner_value, $allowed_html, $allowed_protocols ); + + if ( $filtered_key !== $key ) { + unset( $value[ $key ] ); + } + + $value[ $filtered_key ] = $filtered_value; + } + } elseif ( is_string( $value ) ) { + return wp_kses( $value, $allowed_html, $allowed_protocols ); + } + + return $value; +} + /** * Parses blocks out of a content string, and renders those appropriate for the excerpt. * diff --git a/wp-includes/default-filters.php b/wp-includes/default-filters.php index c1393cb066..6d72fdefdd 100644 --- a/wp-includes/default-filters.php +++ b/wp-includes/default-filters.php @@ -224,30 +224,31 @@ foreach ( array( 'publish_post', 'publish_page', 'wp_ajax_save-widget', 'wp_ajax } // Misc filters -add_filter( 'option_ping_sites', 'privacy_ping_filter' ); -add_filter( 'option_blog_charset', '_wp_specialchars' ); // IMPORTANT: This must not be wp_specialchars() or esc_html() or it'll cause an infinite loop -add_filter( 'option_blog_charset', '_canonical_charset' ); -add_filter( 'option_home', '_config_wp_home' ); -add_filter( 'option_siteurl', '_config_wp_siteurl' ); -add_filter( 'tiny_mce_before_init', '_mce_set_direction' ); -add_filter( 'teeny_mce_before_init', '_mce_set_direction' ); -add_filter( 'pre_kses', 'wp_pre_kses_less_than' ); -add_filter( 'sanitize_title', 'sanitize_title_with_dashes', 10, 3 ); -add_action( 'check_comment_flood', 'check_comment_flood_db', 10, 4 ); -add_filter( 'comment_flood_filter', 'wp_throttle_comment_flood', 10, 3 ); -add_filter( 'pre_comment_content', 'wp_rel_nofollow', 15 ); -add_filter( 'comment_email', 'antispambot' ); -add_filter( 'option_tag_base', '_wp_filter_taxonomy_base' ); -add_filter( 'option_category_base', '_wp_filter_taxonomy_base' ); -add_filter( 'the_posts', '_close_comments_for_old_posts', 10, 2); -add_filter( 'comments_open', '_close_comments_for_old_post', 10, 2 ); -add_filter( 'pings_open', '_close_comments_for_old_post', 10, 2 ); -add_filter( 'editable_slug', 'urldecode' ); -add_filter( 'editable_slug', 'esc_textarea' ); -add_filter( 'nav_menu_meta_box_object', '_wp_nav_menu_meta_box_object' ); -add_filter( 'pingback_ping_source_uri', 'pingback_ping_source_uri' ); -add_filter( 'xmlrpc_pingback_error', 'xmlrpc_pingback_error' ); -add_filter( 'title_save_pre', 'trim' ); +add_filter( 'option_ping_sites', 'privacy_ping_filter' ); +add_filter( 'option_blog_charset', '_wp_specialchars' ); // IMPORTANT: This must not be wp_specialchars() or esc_html() or it'll cause an infinite loop +add_filter( 'option_blog_charset', '_canonical_charset' ); +add_filter( 'option_home', '_config_wp_home' ); +add_filter( 'option_siteurl', '_config_wp_siteurl' ); +add_filter( 'tiny_mce_before_init', '_mce_set_direction' ); +add_filter( 'teeny_mce_before_init', '_mce_set_direction' ); +add_filter( 'pre_kses', 'wp_pre_kses_less_than' ); +add_filter( 'pre_kses', 'wp_pre_kses_block_attributes', 10, 3 ); +add_filter( 'sanitize_title', 'sanitize_title_with_dashes', 10, 3 ); +add_action( 'check_comment_flood', 'check_comment_flood_db', 10, 4 ); +add_filter( 'comment_flood_filter', 'wp_throttle_comment_flood', 10, 3 ); +add_filter( 'pre_comment_content', 'wp_rel_nofollow', 15 ); +add_filter( 'comment_email', 'antispambot' ); +add_filter( 'option_tag_base', '_wp_filter_taxonomy_base' ); +add_filter( 'option_category_base', '_wp_filter_taxonomy_base' ); +add_filter( 'the_posts', '_close_comments_for_old_posts', 10, 2 ); +add_filter( 'comments_open', '_close_comments_for_old_post', 10, 2 ); +add_filter( 'pings_open', '_close_comments_for_old_post', 10, 2 ); +add_filter( 'editable_slug', 'urldecode' ); +add_filter( 'editable_slug', 'esc_textarea' ); +add_filter( 'nav_menu_meta_box_object', '_wp_nav_menu_meta_box_object' ); +add_filter( 'pingback_ping_source_uri', 'pingback_ping_source_uri' ); +add_filter( 'xmlrpc_pingback_error', 'xmlrpc_pingback_error' ); +add_filter( 'title_save_pre', 'trim' ); add_action( 'transition_comment_status', '_clear_modified_cache_on_transition_comment_status', 10, 2 ); diff --git a/wp-includes/formatting.php b/wp-includes/formatting.php index 51eeb6e926..9181c6cf39 100644 --- a/wp-includes/formatting.php +++ b/wp-includes/formatting.php @@ -4402,6 +4402,31 @@ function wp_pre_kses_less_than_callback( $matches ) { return $matches[0]; } +/** + * Remove non-allowable HTML from parsed block attribute values when filtering + * in the post context. + * + * @since 5.3.1 + * + * @param string $string Content to be run through KSES. + * @param array[]|string $allowed_html An array of allowed HTML elements + * and attributes, or a context name + * such as 'post'. + * @param string[] $allowed_protocols Array of allowed URL protocols. + * @return string Filtered text to run through KSES. + */ +function wp_pre_kses_block_attributes( $string, $allowed_html, $allowed_protocols ) { + /* + * `filter_block_content` is expected to call `wp_kses`. Temporarily remove + * the filter to avoid recursion. + */ + remove_filter( 'pre_kses', 'wp_pre_kses_block_attributes', 10 ); + $string = filter_block_content( $string, $allowed_html, $allowed_protocols ); + add_filter( 'pre_kses', 'wp_pre_kses_block_attributes', 10, 3 ); + + return $string; +} + /** * WordPress implementation of PHP sprintf() with filters. * diff --git a/wp-includes/kses.php b/wp-includes/kses.php index 1adb340a98..204142325f 100644 --- a/wp-includes/kses.php +++ b/wp-includes/kses.php @@ -1408,9 +1408,9 @@ function wp_kses_html_error($string) { */ function wp_kses_bad_protocol_once($string, $allowed_protocols, $count = 1 ) { $string = preg_replace( '/(�*58(?![;0-9])|�*3a(?![;a-f0-9]))/i', '$1;', $string ); - $string2 = preg_split( '/:|�*58;|�*3a;/i', $string, 2 ); - if ( isset($string2[1]) && ! preg_match('%/\?%', $string2[0]) ) { - $string = trim( $string2[1] ); + $string2 = preg_split( '/:|�*58;|�*3a;|:/i', $string, 2 ); + if ( isset( $string2[1] ) && ! preg_match( '%/\?%', $string2[0] ) ) { + $string = trim( $string2[1] ); $protocol = wp_kses_bad_protocol_once2( $string2[0], $allowed_protocols ); if ( 'feed:' == $protocol ) { if ( $count > 2 ) diff --git a/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php b/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php index 1316771274..194131af72 100644 --- a/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php +++ b/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php @@ -491,7 +491,7 @@ class WP_REST_Posts_Controller extends WP_REST_Controller { return new WP_Error( 'rest_cannot_edit_others', __( 'Sorry, you are not allowed to create posts as this user.' ), array( 'status' => rest_authorization_required_code() ) ); } - if ( ! empty( $request['sticky'] ) && ! current_user_can( $post_type->cap->edit_others_posts ) ) { + if ( ! empty( $request['sticky'] ) && ! current_user_can( $post_type->cap->edit_others_posts ) && ! current_user_can( $post_type->cap->publish_posts ) ) { return new WP_Error( 'rest_cannot_assign_sticky', __( 'Sorry, you are not allowed to make posts sticky.' ), array( 'status' => rest_authorization_required_code() ) ); } @@ -646,7 +646,7 @@ class WP_REST_Posts_Controller extends WP_REST_Controller { return new WP_Error( 'rest_cannot_edit_others', __( 'Sorry, you are not allowed to update posts as this user.' ), array( 'status' => rest_authorization_required_code() ) ); } - if ( ! empty( $request['sticky'] ) && ! current_user_can( $post_type->cap->edit_others_posts ) ) { + if ( ! empty( $request['sticky'] ) && ! current_user_can( $post_type->cap->edit_others_posts ) && ! current_user_can( $post_type->cap->publish_posts ) ) { return new WP_Error( 'rest_cannot_assign_sticky', __( 'Sorry, you are not allowed to make posts sticky.' ), array( 'status' => rest_authorization_required_code() ) ); } @@ -944,7 +944,7 @@ class WP_REST_Posts_Controller extends WP_REST_Controller { * @return stdClass|WP_Error Post object or WP_Error. */ protected function prepare_item_for_database( $request ) { - $prepared_post = new stdClass; + $prepared_post = new stdClass(); // Post ID. if ( isset( $request['id'] ) ) {