Make WordPress Core

Changeset 46915


Ignore:
Timestamp:
12/12/2019 06:51:11 PM (5 years ago)
Author:
whyisjake
Message:

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.

Location:
branches/5.0
Files:
2 added
8 edited

Legend:

Unmodified
Added
Removed
  • branches/5.0

  • branches/5.0/src/wp-includes/blocks.php

    r43924 r46915  
    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.0/src/wp-includes/default-filters.php

    r43930 r46915  
    225225
    226226// Misc filters
    227 add_filter( 'option_ping_sites',        'privacy_ping_filter'                 );
    228 add_filter( 'option_blog_charset',      '_wp_specialchars'                    ); // IMPORTANT: This must not be wp_specialchars() or esc_html() or it'll cause an infinite loop
    229 add_filter( 'option_blog_charset',      '_canonical_charset'                  );
    230 add_filter( 'option_home',              '_config_wp_home'                     );
    231 add_filter( 'option_siteurl',           '_config_wp_siteurl'                  );
    232 add_filter( 'tiny_mce_before_init',     '_mce_set_direction'                  );
    233 add_filter( 'teeny_mce_before_init',    '_mce_set_direction'                  );
    234 add_filter( 'pre_kses',                 'wp_pre_kses_less_than'               );
    235 add_filter( 'sanitize_title',           'sanitize_title_with_dashes',   10, 3 );
    236 add_action( 'check_comment_flood',      'check_comment_flood_db',       10, 4 );
    237 add_filter( 'comment_flood_filter',     'wp_throttle_comment_flood',    10, 3 );
    238 add_filter( 'pre_comment_content',      'wp_rel_nofollow',              15    );
    239 add_filter( 'comment_email',            'antispambot'                         );
    240 add_filter( 'option_tag_base',          '_wp_filter_taxonomy_base'            );
    241 add_filter( 'option_category_base',     '_wp_filter_taxonomy_base'            );
    242 add_filter( 'the_posts',                '_close_comments_for_old_posts', 10, 2);
    243 add_filter( 'comments_open',            '_close_comments_for_old_post', 10, 2 );
    244 add_filter( 'pings_open',               '_close_comments_for_old_post', 10, 2 );
    245 add_filter( 'editable_slug',            'urldecode'                           );
    246 add_filter( 'editable_slug',            'esc_textarea'                        );
    247 add_filter( 'nav_menu_meta_box_object', '_wp_nav_menu_meta_box_object'        );
    248 add_filter( 'pingback_ping_source_uri', 'pingback_ping_source_uri'            );
    249 add_filter( 'xmlrpc_pingback_error',    'xmlrpc_pingback_error'               );
    250 add_filter( 'title_save_pre',           'trim'                                );
     227add_filter( 'option_ping_sites', 'privacy_ping_filter' );
     228add_filter( 'option_blog_charset', '_wp_specialchars' ); // IMPORTANT: This must not be wp_specialchars() or esc_html() or it'll cause an infinite loop
     229add_filter( 'option_blog_charset', '_canonical_charset' );
     230add_filter( 'option_home', '_config_wp_home' );
     231add_filter( 'option_siteurl', '_config_wp_siteurl' );
     232add_filter( 'tiny_mce_before_init', '_mce_set_direction' );
     233add_filter( 'teeny_mce_before_init', '_mce_set_direction' );
     234add_filter( 'pre_kses', 'wp_pre_kses_less_than' );
     235add_filter( 'pre_kses', 'wp_pre_kses_block_attributes', 10, 3 );
     236add_filter( 'sanitize_title', 'sanitize_title_with_dashes', 10, 3 );
     237add_action( 'check_comment_flood', 'check_comment_flood_db', 10, 4 );
     238add_filter( 'comment_flood_filter', 'wp_throttle_comment_flood', 10, 3 );
     239add_filter( 'pre_comment_content', 'wp_rel_nofollow', 15 );
     240add_filter( 'comment_email', 'antispambot' );
     241add_filter( 'option_tag_base', '_wp_filter_taxonomy_base' );
     242add_filter( 'option_category_base', '_wp_filter_taxonomy_base' );
     243add_filter( 'the_posts', '_close_comments_for_old_posts', 10, 2 );
     244add_filter( 'comments_open', '_close_comments_for_old_post', 10, 2 );
     245add_filter( 'pings_open', '_close_comments_for_old_post', 10, 2 );
     246add_filter( 'editable_slug', 'urldecode' );
     247add_filter( 'editable_slug', 'esc_textarea' );
     248add_filter( 'nav_menu_meta_box_object', '_wp_nav_menu_meta_box_object' );
     249add_filter( 'pingback_ping_source_uri', 'pingback_ping_source_uri' );
     250add_filter( 'xmlrpc_pingback_error', 'xmlrpc_pingback_error' );
     251add_filter( 'title_save_pre', 'trim' );
    251252
    252253add_action( 'transition_comment_status', '_clear_modified_cache_on_transition_comment_status', 10, 2 );
  • branches/5.0/src/wp-includes/formatting.php

    r45993 r46915  
    44044404
    44054405/**
     4406 * Remove non-allowable HTML from parsed block attribute values when filtering
     4407 * in the post context.
     4408 *
     4409 * @since 5.3.1
     4410 *
     4411 * @param string         $string            Content to be run through KSES.
     4412 * @param array[]|string $allowed_html      An array of allowed HTML elements
     4413 *                                          and attributes, or a context name
     4414 *                                          such as 'post'.
     4415 * @param string[]       $allowed_protocols Array of allowed URL protocols.
     4416 * @return string Filtered text to run through KSES.
     4417 */
     4418function wp_pre_kses_block_attributes( $string, $allowed_html, $allowed_protocols ) {
     4419    /*
     4420     * `filter_block_content` is expected to call `wp_kses`. Temporarily remove
     4421     * the filter to avoid recursion.
     4422     */
     4423    remove_filter( 'pre_kses', 'wp_pre_kses_block_attributes', 10 );
     4424    $string = filter_block_content( $string, $allowed_html, $allowed_protocols );
     4425    add_filter( 'pre_kses', 'wp_pre_kses_block_attributes', 10, 3 );
     4426
     4427    return $string;
     4428}
     4429
     4430/**
    44064431 * WordPress implementation of PHP sprintf() with filters.
    44074432 *
  • branches/5.0/src/wp-includes/kses.php

    r46004 r46915  
    14091409function wp_kses_bad_protocol_once($string, $allowed_protocols, $count = 1 ) {
    14101410    $string  = preg_replace( '/(&#0*58(?![;0-9])|&#x0*3a(?![;a-f0-9]))/i', '$1;', $string );
    1411     $string2 = preg_split( '/:|&#0*58;|&#x0*3a;/i', $string, 2 );
    1412     if ( isset($string2[1]) && ! preg_match('%/\?%', $string2[0]) ) {
    1413         $string = trim( $string2[1] );
     1411    $string2 = preg_split( '/:|&#0*58;|&#x0*3a;|&colon;/i', $string, 2 );
     1412    if ( isset( $string2[1] ) && ! preg_match( '%/\?%', $string2[0] ) ) {
     1413        $string   = trim( $string2[1] );
    14141414        $protocol = wp_kses_bad_protocol_once2( $string2[0], $allowed_protocols );
    14151415        if ( 'feed:' == $protocol ) {
  • branches/5.0/src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php

    r43862 r46915  
    492492        }
    493493
    494         if ( ! empty( $request['sticky'] ) && ! current_user_can( $post_type->cap->edit_others_posts ) ) {
     494        if ( ! empty( $request['sticky'] ) && ! current_user_can( $post_type->cap->edit_others_posts ) && ! current_user_can( $post_type->cap->publish_posts ) ) {
    495495            return new WP_Error( 'rest_cannot_assign_sticky', __( 'Sorry, you are not allowed to make posts sticky.' ), array( 'status' => rest_authorization_required_code() ) );
    496496        }
     
    647647        }
    648648
    649         if ( ! empty( $request['sticky'] ) && ! current_user_can( $post_type->cap->edit_others_posts ) ) {
     649        if ( ! empty( $request['sticky'] ) && ! current_user_can( $post_type->cap->edit_others_posts ) && ! current_user_can( $post_type->cap->publish_posts ) ) {
    650650            return new WP_Error( 'rest_cannot_assign_sticky', __( 'Sorry, you are not allowed to make posts sticky.' ), array( 'status' => rest_authorization_required_code() ) );
    651651        }
     
    945945     */
    946946    protected function prepare_item_for_database( $request ) {
    947         $prepared_post = new stdClass;
     947        $prepared_post = new stdClass();
    948948
    949949        // Post ID.
  • branches/5.0/tests/phpunit/tests/blocks/block-type.php

    r43918 r46915  
    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
  • branches/5.0/tests/phpunit/tests/kses.php

    r46004 r46915  
    171171        }
    172172
     173        $bad_not_normalized = array(
     174            'dummy&colon;alert(1)',
     175            'javascript&colon;alert(1)',
     176            'javascript&CoLon;alert(1)',
     177            'javascript&COLON;alert(1);',
     178            'javascript&#58;alert(1);',
     179            'javascript&#0058;alert(1);',
     180            'javascript&#0000058alert(1);',
     181            'jav    ascript&COLON;alert(1);',
     182            'javascript&#58;javascript&colon;alert(1);',
     183            'javascript&#58;javascript&colon;alert(1);',
     184            'javascript&#0000058javascript&colon;alert(1);',
     185            'javascript&#58;javascript&#0000058alert(1);',
     186            'javascript&#58alert(1)',
     187        );
     188        foreach ( $bad_not_normalized as $k => $x ) {
     189            $result = wp_kses_bad_protocol( $x, wp_allowed_protocols() );
     190            if ( ! empty( $result ) && 'alert(1);' !== $result && 'alert(1)' !== $result ) {
     191                $this->fail( "wp_kses_bad_protocol failed on $k, $x. Result: $result" );
     192            }
     193        }
     194
    173195        $safe = array(
    174196            'dummy:alert(1)',
Note: See TracChangeset for help on using the changeset viewer.