Skip to content

Commit a31a90d

Browse files
committed
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. git-svn-id: https://develop.svn.wordpress.org/branches/5.0@46915 602fd350-edb4-49c9-b593-d223f7449a82
1 parent 12be6cd commit a31a90d

File tree

9 files changed

+480
-33
lines changed

9 files changed

+480
-33
lines changed

src/wp-includes/blocks.php

Lines changed: 227 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,11 @@ function has_blocks( $post = null ) {
7474
* @since 5.0.0
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 ) {
81+
function has_block( $block_name, $post = null ) {
8282
if ( ! has_blocks( $post ) ) {
8383
return false;
8484
}
@@ -90,7 +90,30 @@ function has_block( $block_type, $post = null ) {
9090
}
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

96119
/**
@@ -113,6 +136,207 @@ function get_dynamic_block_names() {
113136
return $dynamic_block_names;
114137
}
115138

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+
*/
152+
function 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+
*/
173+
function 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+
*/
191+
function 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+
*/
226+
function 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+
*/
254+
function 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+
*/
271+
function 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+
*/
296+
function 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+
*/
321+
function 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;
338+
}
339+
116340
/**
117341
* Parses blocks out of a content string, and renders those appropriate for the excerpt.
118342
*

src/wp-includes/default-filters.php

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -224,30 +224,31 @@
224224
}
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' );
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( 'pre_kses', 'wp_pre_kses_block_attributes', 10, 3 );
236+
add_filter( 'sanitize_title', 'sanitize_title_with_dashes', 10, 3 );
237+
add_action( 'check_comment_flood', 'check_comment_flood_db', 10, 4 );
238+
add_filter( 'comment_flood_filter', 'wp_throttle_comment_flood', 10, 3 );
239+
add_filter( 'pre_comment_content', 'wp_rel_nofollow', 15 );
240+
add_filter( 'comment_email', 'antispambot' );
241+
add_filter( 'option_tag_base', '_wp_filter_taxonomy_base' );
242+
add_filter( 'option_category_base', '_wp_filter_taxonomy_base' );
243+
add_filter( 'the_posts', '_close_comments_for_old_posts', 10, 2 );
244+
add_filter( 'comments_open', '_close_comments_for_old_post', 10, 2 );
245+
add_filter( 'pings_open', '_close_comments_for_old_post', 10, 2 );
246+
add_filter( 'editable_slug', 'urldecode' );
247+
add_filter( 'editable_slug', 'esc_textarea' );
248+
add_filter( 'nav_menu_meta_box_object', '_wp_nav_menu_meta_box_object' );
249+
add_filter( 'pingback_ping_source_uri', 'pingback_ping_source_uri' );
250+
add_filter( 'xmlrpc_pingback_error', 'xmlrpc_pingback_error' );
251+
add_filter( 'title_save_pre', 'trim' );
251252

252253
add_action( 'transition_comment_status', '_clear_modified_cache_on_transition_comment_status', 10, 2 );
253254

src/wp-includes/formatting.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4402,6 +4402,31 @@ function wp_pre_kses_less_than_callback( $matches ) {
44024402
return $matches[0];
44034403
}
44044404

4405+
/**
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+
*/
4418+
function 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+
44054430
/**
44064431
* WordPress implementation of PHP sprintf() with filters.
44074432
*

src/wp-includes/kses.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1408,9 +1408,9 @@ function wp_kses_html_error($string) {
14081408
*/
14091409
function 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 ) {
14161416
if ( $count > 2 )

src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -491,7 +491,7 @@ public function create_item_permissions_check( $request ) {
491491
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() ) );
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
}
497497

@@ -646,7 +646,7 @@ public function update_item_permissions_check( $request ) {
646646
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() ) );
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
}
652652

@@ -944,7 +944,7 @@ protected function prepare_date_response( $date_gmt, $date = null ) {
944944
* @return stdClass|WP_Error Post object or WP_Error.
945945
*/
946946
protected function prepare_item_for_database( $request ) {
947-
$prepared_post = new stdClass;
947+
$prepared_post = new stdClass();
948948

949949
// Post ID.
950950
if ( isset( $request['id'] ) ) {

0 commit comments

Comments
 (0)