Make WordPress Core

Changeset 46900


Ignore:
Timestamp:
12/12/2019 06:10:56 PM (7 years ago)
Author:
whyisjake
Message:

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.

Location:
branches/5.3
Files:
4 edited

Legend:

Unmodified
Added
Removed
  • branches/5.3/src/wp-includes/blocks.php

    r46232 r46900  
    7575 * @see parse_blocks()
    7676 *
    77  * @param string                  $block_type Full Block type to look for.
     77 * @param string                  $block_name Full Block type to look for.
    7878 * @param int|string|WP_Post|null $post Optional. Post content, post ID, or post object. Defaults to global $post.
    7979 * @return bool Whether the post content contains the specified block.
    8080 */
    81 function has_block( $block_type, $post = null ) {
     81function has_block( $block_name, $post = null ) {
    8282        if ( ! has_blocks( $post ) ) {
    8383                return false;
     
    9191        }
    9292
    93         return false !== strpos( $post, '<!-- wp:' . $block_type . ' ' );
     93        /*
     94         * Normalize block name to include namespace, if provided as non-namespaced.
     95         * This matches behavior for WordPress 5.0.0 - 5.3.0 in matching blocks by
     96         * their serialized names.
     97         */
     98        if ( false === strpos( $block_name, '/' ) ) {
     99                $block_name = 'core/' . $block_name;
     100        }
     101
     102        // Test for existence of block by its fully qualified name.
     103        $has_block = false !== strpos( $post, '<!-- wp:' . $block_name . ' ' );
     104
     105        if ( ! $has_block ) {
     106                /*
     107                 * If the given block name would serialize to a different name, test for
     108                 * existence by the serialized form.
     109                 */
     110                $serialized_block_name = strip_core_block_namespace( $block_name );
     111                if ( $serialized_block_name !== $block_name ) {
     112                        $has_block = false !== strpos( $post, '<!-- wp:' . $serialized_block_name . ' ' );
     113                }
     114        }
     115
     116        return $has_block;
    94117}
    95118
     
    112135
    113136        return $dynamic_block_names;
     137}
     138
     139/**
     140 * Given an array of attributes, returns a string in the serialized attributes
     141 * format prepared for post content.
     142 *
     143 * The serialized result is a JSON-encoded string, with unicode escape sequence
     144 * substitution for characters which might otherwise interfere with embedding
     145 * the result in an HTML comment.
     146 *
     147 * @since 5.3.1
     148 *
     149 * @param array $attributes Attributes object.
     150 * @return string Serialized attributes.
     151 */
     152function serialize_block_attributes( $block_attributes ) {
     153        $encoded_attributes = json_encode( $block_attributes );
     154        $encoded_attributes = preg_replace( '/--/', '\\u002d\\u002d', $encoded_attributes );
     155        $encoded_attributes = preg_replace( '/</', '\\u003c', $encoded_attributes );
     156        $encoded_attributes = preg_replace( '/>/', '\\u003e', $encoded_attributes );
     157        $encoded_attributes = preg_replace( '/&/', '\\u0026', $encoded_attributes );
     158        // Regex: /\\"/
     159        $encoded_attributes = preg_replace( '/\\\\"/', '\\u0022', $encoded_attributes );
     160
     161        return $encoded_attributes;
     162}
     163
     164/**
     165 * Returns the block name to use for serialization. This will remove the default
     166 * "core/" namespace from a block name.
     167 *
     168 * @since 5.3.1
     169 *
     170 * @param string $block_name Original block name.
     171 * @return string Block name to use for serialization.
     172 */
     173function strip_core_block_namespace( $block_name = null ) {
     174        if ( is_string( $block_name ) && 0 === strpos( $block_name, 'core/' ) ) {
     175                return substr( $block_name, 5 );
     176        }
     177
     178        return $block_name;
     179}
     180
     181/**
     182 * Returns the content of a block, including comment delimiters.
     183 *
     184 * @since 5.3.1
     185 *
     186 * @param string $block_name Block name.
     187 * @param array  $attributes Block attributes.
     188 * @param string $content    Block save content.
     189 * @return string Comment-delimited block content.
     190 */
     191function get_comment_delimited_block_content( $block_name = null, $block_attributes, $block_content ) {
     192        if ( is_null( $block_name ) ) {
     193                return $block_content;
     194        }
     195
     196        $serialized_block_name = strip_core_block_namespace( $block_name );
     197        $serialized_attributes = empty( $block_attributes ) ? '' : serialize_block_attributes( $block_attributes ) . ' ';
     198
     199        if ( empty( $block_content ) ) {
     200                return sprintf( '<!-- wp:%s %s/-->', $serialized_block_name, $serialized_attributes );
     201        }
     202
     203        return sprintf(
     204                '<!-- wp:%s %s-->%s<!-- /wp:%s -->',
     205                $serialized_block_name,
     206                $serialized_attributes,
     207                $block_content,
     208                $serialized_block_name
     209        );
     210}
     211
     212/**
     213 * Returns the content of a block, including comment delimiters, serializing all
     214 * attributes from the given parsed block.
     215 *
     216 * This should be used when preparing a block to be saved to post content.
     217 * Prefer `render_block` when preparing a block for display. Unlike
     218 * `render_block`, this does not evaluate a block's `render_callback`, and will
     219 * instead preserve the markup as parsed.
     220 *
     221 * @since 5.3.1
     222 *
     223 * @param WP_Block_Parser_Block $block A single parsed block object.
     224 * @return string String of rendered HTML.
     225 */
     226function serialize_block( $block ) {
     227        $block_content = '';
     228
     229        $index = 0;
     230        foreach ( $block['innerContent'] as $chunk ) {
     231                $block_content .= is_string( $chunk ) ? $chunk : serialize_block( $block['innerBlocks'][ $index++ ] );
     232        }
     233
     234        if ( ! is_array( $block['attrs'] ) ) {
     235                $block['attrs'] = array();
     236        }
     237
     238        return get_comment_delimited_block_content(
     239                $block['blockName'],
     240                $block['attrs'],
     241                $block_content
     242        );
     243}
     244
     245/**
     246 * Returns a joined string of the aggregate serialization of the given parsed
     247 * blocks.
     248 *
     249 * @since 5.3.1
     250 *
     251 * @param WP_Block_Parser_Block[] $blocks Parsed block objects.
     252 * @return string String of rendered HTML.
     253 */
     254function serialize_blocks( $blocks ) {
     255        return implode( '', array_map( 'serialize_block', $blocks ) );
     256}
     257
     258/**
     259 * Filters and sanitizes block content to remove non-allowable HTML from
     260 * parsed block attribute values.
     261 *
     262 * @since 5.3.1
     263 *
     264 * @param string         $text              Text that may contain block content.
     265 * @param array[]|string $allowed_html      An array of allowed HTML elements
     266 *                                          and attributes, or a context name
     267 *                                          such as 'post'.
     268 * @param string[]       $allowed_protocols Array of allowed URL protocols.
     269 * @return string The filtered and sanitized content result.
     270 */
     271function filter_block_content( $text, $allowed_html = 'post', $allowed_protocols = array() ) {
     272        $result = '';
     273
     274        $blocks = parse_blocks( $text );
     275        foreach ( $blocks as $block ) {
     276                $block   = filter_block_kses( $block, $allowed_html, $allowed_protocols );
     277                $result .= serialize_block( $block );
     278        }
     279
     280        return $result;
     281}
     282
     283/**
     284 * Filters and sanitizes a parsed block to remove non-allowable HTML from block
     285 * attribute values.
     286 *
     287 * @since 5.3.1
     288 *
     289 * @param WP_Block_Parser_Block $block             The parsed block object.
     290 * @param array[]|string        $allowed_html      An array of allowed HTML
     291 *                                                 elements and attributes, or a
     292 *                                                 context name such as 'post'.
     293 * @param string[]              $allowed_protocols Allowed URL protocols.
     294 * @return array The filtered and sanitized block object result.
     295 */
     296function filter_block_kses( $block, $allowed_html, $allowed_protocols = array() ) {
     297        $block['attrs'] = filter_block_kses_value( $block['attrs'], $allowed_html, $allowed_protocols );
     298
     299        if ( is_array( $block['innerBlocks'] ) ) {
     300                foreach ( $block['innerBlocks'] as $i => $inner_block ) {
     301                        $block['innerBlocks'][ $i ] = filter_block_kses( $inner_block, $allowed_html, $allowed_protocols );
     302                }
     303        }
     304
     305        return $block;
     306}
     307
     308/**
     309 * Filters and sanitizes a parsed block attribute value to remove non-allowable
     310 * HTML.
     311 *
     312 * @since 5.3.1
     313 *
     314 * @param mixed          $value             The attribute value to filter.
     315 * @param array[]|string $allowed_html      An array of allowed HTML elements
     316 *                                          and attributes, or a context name
     317 *                                          such as 'post'.
     318 * @param string[]       $allowed_protocols Array of allowed URL protocols.
     319 * @return array The filtered and sanitized result.
     320 */
     321function filter_block_kses_value( $value, $allowed_html, $allowed_protocols = array() ) {
     322        if ( is_array( $value ) ) {
     323                foreach ( $value as $key => $inner_value ) {
     324                        $filtered_key   = filter_block_kses_value( $key, $allowed_html, $allowed_protocols );
     325                        $filtered_value = filter_block_kses_value( $inner_value, $allowed_html, $allowed_protocols );
     326
     327                        if ( $filtered_key !== $key ) {
     328                                unset( $value[ $key ] );
     329                        }
     330
     331                        $value[ $filtered_key ] = $filtered_value;
     332                }
     333        } elseif ( is_string( $value ) ) {
     334                return wp_kses( $value, $allowed_html, $allowed_protocols );
     335        }
     336
     337        return $value;
    114338}
    115339
  • branches/5.3/src/wp-includes/default-filters.php

    r46432 r46900  
    244244add_filter( 'teeny_mce_before_init', '_mce_set_direction' );
    245245add_filter( 'pre_kses', 'wp_pre_kses_less_than' );
     246add_filter( 'pre_kses', 'wp_pre_kses_block_attributes', 10, 3 );
    246247add_filter( 'sanitize_title', 'sanitize_title_with_dashes', 10, 3 );
    247248add_action( 'check_comment_flood', 'check_comment_flood_db', 10, 4 );
  • branches/5.3/src/wp-includes/formatting.php

    r46898 r46900  
    49044904
    49054905/**
     4906 * Remove non-allowable HTML from parsed block attribute values when filtering
     4907 * in the post context.
     4908 *
     4909 * @since 5.3.1
     4910 *
     4911 * @param string         $string            Content to be run through KSES.
     4912 * @param array[]|string $allowed_html      An array of allowed HTML elements
     4913 *                                          and attributes, or a context name
     4914 *                                          such as 'post'.
     4915 * @param string[]       $allowed_protocols Array of allowed URL protocols.
     4916 * @return string Filtered text to run through KSES.
     4917 */
     4918function wp_pre_kses_block_attributes( $string, $allowed_html, $allowed_protocols ) {
     4919        /*
     4920         * `filter_block_content` is expected to call `wp_kses`. Temporarily remove
     4921         * the filter to avoid recursion.
     4922         */
     4923        remove_filter( 'pre_kses', 'wp_pre_kses_block_attributes', 10 );
     4924        $string = filter_block_content( $string, $allowed_html, $allowed_protocols );
     4925        add_filter( 'pre_kses', 'wp_pre_kses_block_attributes', 10, 3 );
     4926
     4927        return $string;
     4928}
     4929
     4930/**
    49064931 * WordPress implementation of PHP sprintf() with filters.
    49074932 *
  • branches/5.3/tests/phpunit/tests/blocks/block-type.php

    r44269 r46900  
    303303                // even if it detects a proper $post global it should still be false for a missing block.
    304304                $this->assertFalse( has_block( 'core/fake' ) );
     305        }
     306
     307        public function test_post_has_block_serialized_name() {
     308                $content = '<!-- wp:serialized /--><!-- wp:core/normalized /--><!-- wp:plugin/third-party /-->';
     309
     310                $this->assertTrue( has_block( 'core/serialized', $content ) );
     311
     312                /*
     313                 * Technically, `has_block` should receive a "full" (normalized, parsed)
     314                 * block name. But this test conforms to expected pre-5.3.1 behavior.
     315                 */
     316                $this->assertTrue( has_block( 'serialized', $content ) );
     317                $this->assertTrue( has_block( 'core/normalized', $content ) );
     318                $this->assertTrue( has_block( 'normalized', $content ) );
     319                $this->assertFalse( has_block( 'plugin/normalized', $content ) );
     320                $this->assertFalse( has_block( 'plugin/serialized', $content ) );
     321                $this->assertFalse( has_block( 'third-party', $content ) );
     322                $this->assertFalse( has_block( 'core/third-party', $content ) );
    305323        }
    306324
Note: See TracChangeset for help on using the changeset viewer.