WordPress.org

Make WordPress Core

Changeset 46896


Ignore:
Timestamp:
12/12/2019 06:00:45 PM (4 months ago)
Author:
whyisjake
Message:

Prevent stored XSS in the block editor.

Prevent escaped unicode characters become unescaped in unsafe HTML during JSON decoding.

Props: aduth, epiqueras,

Location:
trunk
Files:
4 edited

Legend:

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

    r46662 r46896  
    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
  • trunk/src/wp-includes/default-filters.php

    r46432 r46896  
    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 );
  • trunk/src/wp-includes/formatting.php

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

    r46586 r46896  
    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.