Make WordPress Core

Changeset 63715


Ignore:
Timestamp:
09/17/2026 09:15:04 PM (3 hours ago)
Author:
desrosj
Message:

Security: Backport the WordPress 7.1.1 security fixes to the 4.7 branch.

  • Posts, Post Types: Reject a supplied post ID on the create path in _wp_translate_postdata().
  • XML-RPC: Reject writes to internal-only builtin post types.
  • Administration: Add authorization check to wp_ajax_sample_permalink().
  • Customize: Improve header_image_data theme mod sanitization.
  • Plugins: Require network plugin authority to Ajax-activate a network-only plugin.
  • Formatting: Prevent wpautop() moving a paragraph into an attribute of a blockquote.

Merges r63657, r63659, r63660, r63665, r63669, r63672 to the 4.7 branch.

Props xknown, westonruter, jorbin, vortfu, batmoo, davidbinda, jeremyfelt, johnbillion, peterwilsoncc, lancewillett, jonsurrell, dmsnell, whyisjake, buffer1024, joehoyle, rafiem.

Location:
branches/4.7
Files:
9 edited

Legend:

Unmodified
Added
Removed
  • branches/4.7

  • branches/4.7/src/wp-admin/custom-header.php

    r49399 r63715  
    502502                        $header_image_style .= 'height:' . $custom_header->height . 'px;';
    503503        ?>
    504         <div id="headimg" style="<?php echo $header_image_style; ?>">
     504        <div id="headimg" style="<?php echo esc_attr( $header_image_style ); ?>">
    505505                <?php
    506506                if ( display_header_text() )
     
    10251025                                'url'           => $choice['url'],
    10261026                                'thumbnail_url' => $choice['url'],
    1027                                 'height'        => $choice['height'],
    1028                                 'width'         => $choice['width'],
     1027                                'height'        => absint( $choice['height'] ),
     1028                                'width'         => absint( $choice['width'] ),
    10291029                        );
    10301030
     
    10531053                }
    10541054
    1055                 set_theme_mod( 'header_image', esc_url_raw( $header_image_data['url'] ) );
     1055                $header_image_data['url'] = esc_url_raw( $header_image_data['url'] );
     1056
     1057                if ( isset( $header_image_data['thumbnail_url'] ) ) {
     1058                        $header_image_data['thumbnail_url'] = esc_url_raw( $header_image_data['thumbnail_url'] );
     1059                }
     1060
     1061                set_theme_mod( 'header_image', $header_image_data['url'] );
    10561062                set_theme_mod( 'header_image_data', $header_image_data );
    10571063        }
     
    13701376
    13711377                foreach ( $header_images as &$header_image ) {
    1372                         $header_meta = get_post_meta( $header_image['attachment_id'] );
    1373                         $header_image['timestamp'] = isset( $header_meta[ $timestamp_key ] ) ? $header_meta[ $timestamp_key ] : '';
    1374                         $header_image['alt_text'] = isset( $header_meta[ $alt_text_key ] ) ? $header_meta[ $alt_text_key ] : '';
     1378                        $header_image['timestamp'] = get_post_meta( $header_image['attachment_id'], $timestamp_key, true );
     1379                        $header_image['alt_text'] = get_post_meta( $header_image['attachment_id'], $alt_text_key, true );
    13751380                }
    13761381
  • branches/4.7/src/wp-admin/includes/ajax-actions.php

    r56862 r63715  
    15811581        check_ajax_referer( 'getpermalink', 'getpermalinknonce' );
    15821582        $post_id = isset($_POST['post_id'])? intval($_POST['post_id']) : 0;
     1583        if ( ! $post_id ) {
     1584                // Bypass call to get_preview_post_link() for unspecified post ID.
     1585                wp_die( '' );
     1586        }
     1587        if ( ! current_user_can( 'edit_post', $post_id ) ) {
     1588                wp_die( -1 );
     1589        }
    15831590        wp_die( get_preview_post_link( $post_id ) );
    15841591}
     
    15921599        check_ajax_referer( 'samplepermalink', 'samplepermalinknonce' );
    15931600        $post_id = isset($_POST['post_id'])? intval($_POST['post_id']) : 0;
     1601        if ( ! $post_id ) {
     1602                // Bypass call to get_sample_permalink_html() for unspecified post ID.
     1603                wp_die( '' );
     1604        }
     1605        if ( ! current_user_can( 'edit_post', $post_id ) ) {
     1606                wp_die( -1 );
     1607        }
    15941608        $title = isset($_POST['new_title'])? $_POST['new_title'] : '';
    15951609        $slug = isset($_POST['new_slug'])? $_POST['new_slug'] : null;
  • branches/4.7/src/wp-admin/includes/plugin.php

    r38687 r63715  
    499499 *
    500500 * @since 3.0.0
    501  *
    502  * @param string $plugin Plugin to check
     501 * @since 7.1.1 The `$plugin` path is normalized with `plugin_basename()` and `trim()`,
     502 *              matching how `activate_plugin()` resolves it.
     503 *
     504 * @param string $plugin Path to the plugin file. Accepts a path relative to the plugins
     505 *                       directory, or an absolute path, with or without surrounding whitespace.
    503506 * @return bool True if plugin is network only, false otherwise.
    504507 */
    505508function is_network_only_plugin( $plugin ) {
     509        // Normalize the path the same way activate_plugin() does, so both agree on the file.
     510        $plugin = plugin_basename( trim( $plugin ) );
     511
    506512        $plugin_data = get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin );
    507513        if ( $plugin_data )
  • branches/4.7/src/wp-admin/includes/post.php

    r54566 r63715  
    2323        if ( empty($post_data) )
    2424                $post_data = &$_POST;
     25
     26        /*
     27         * A raw `ID` on the create path (no `post_ID`) is an attempt to overwrite an
     28         * existing post while bypassing the per-post capability checks below, which only
     29         * run on the update path. Reject it outright: legitimate post creation never
     30         * carries an `ID`.
     31         */
     32        if ( ! $update && ! empty( $post_data['ID'] ) ) {
     33                if ( 'page' === $post_data['post_type'] ) {
     34                        return new WP_Error( 'edit_others_pages', __( 'Sorry, you are not allowed to edit pages as this user.' ) );
     35                } else {
     36                        return new WP_Error( 'edit_others_posts', __( 'Sorry, you are not allowed to edit posts as this user.' ) );
     37                }
     38        }
    2539
    2640        if ( $update )
  • branches/4.7/src/wp-includes/class-wp-xmlrpc-server.php

    r49399 r63715  
    13311331                        return new IXR_Error( 403, __( 'Invalid post type.' ) );
    13321332
     1333                // Reject writes to internal-only builtin post types (e.g. customize_changeset)
     1334                // whose intended write path is a dedicated helper, not a generic post API.
     1335                $is_internal_only = (
     1336                        empty( $post_type->public )
     1337                        && empty( $post_type->show_in_rest )
     1338                        && ! empty( $post_type->_builtin )
     1339                );
     1340
     1341                /**
     1342                 * Filters whether a post type accepts writes via XML-RPC.
     1343                 *
     1344                 * Defaults to false for internal-only builtin post types (public=false,
     1345                 * show_in_rest=false, _builtin=true), such as customize_changeset, whose
     1346                 * writes are meant to flow through dedicated helpers. Return true to opt
     1347                 * a post type back in.
     1348                 *
     1349                 * @since 7.1.1
     1350                 *
     1351                 * @param bool         $allowed   Whether the post type accepts XML-RPC writes.
     1352                 * @param WP_Post_Type $post_type The post type object.
     1353                 */
     1354                $allowed = apply_filters( 'xmlrpc_allow_post_type_writes', ! $is_internal_only, $post_type );
     1355
     1356                if ( ! $allowed ) {
     1357                        return new IXR_Error( 403, __( 'Sorry, this post type is not supported over XML-RPC.' ) );
     1358                }
     1359
    13331360                $update = ! empty( $post_data['ID'] );
    13341361
  • branches/4.7/src/wp-includes/customize/class-wp-customize-header-image-setting.php

    r39412 r63715  
    1616 *
    1717 * @see WP_Customize_Setting
     18 *
     19 * @phpstan-type Header_Image_Data array{
     20 *     attachment_id?: int,
     21 *     url?: string,
     22 *     thumbnail_url?: string,
     23 *     timestamp?: int,
     24 *     width?: int,
     25 *     height?: int,
     26 *     alt_text?: string,
     27 *     attachment_parent?: int,
     28 * }
    1829 */
    1930final class WP_Customize_Header_Image_Setting extends WP_Customize_Setting {
     
    4960                        $custom_image_header->set_header_image( $value );
    5061        }
     62
     63        /**
     64         * Sanitizes a header value.
     65         *
     66         * The value is expected to be one of the following:
     67         *
     68         * - An array of header image data, with the keys `attachment_id`, `url`, `thumbnail_url`, `timestamp`, `width`,
     69         *   `height`, `alt_text`, and `attachment_parent`, as supplied by {@see get_uploaded_header_images()}. Any other
     70         *   key is discarded.
     71         * - An array with a `choice` key, being the legacy format in which any of the other accepted values is nested.
     72         * - The string `remove-header`, `random-default-image`, or `random-uploaded-image`.
     73         * - A string corresponding to one of the keys for the array returned by {@see get_uploaded_header_images()}, or
     74         *   one of the keys for the array passed into {@see register_default_headers()}.
     75         *
     76         * @since 7.1.1
     77         *
     78         * @see WP_Customize_Header_Image_Setting::update()
     79         * @see Custom_Image_Header::set_header_image()
     80         *
     81         * @param mixed $value Value to sanitize.
     82         * @return array|string|WP_Error|null Sanitized value, or `null`/`WP_Error` if invalid. The array holds
     83         *                                    the header image data, or that data nested under a `choice` key,
     84         *                                    before the `customize_sanitize_header_image_data` filter, which
     85         *                                    may return anything, is applied to it.
     86         *
     87         * @phpstan-return array<mixed, mixed>|string|WP_Error|null
     88         */
     89        public function sanitize( $value ) {
     90                /*
     91                 * The update() method unwraps the legacy `choice` format before handing the value off to
     92                 * Custom_Image_Header::set_header_image(), so the nested value is what must be sanitized.
     93                 */
     94                if ( is_array( $value ) && isset( $value['choice'] ) ) {
     95                        $choice = $this->sanitize_choice( $value['choice'] );
     96                        if ( is_null( $choice ) || is_wp_error( $choice ) ) {
     97                                return $choice;
     98                        }
     99                        $value = array( 'choice' => $choice );
     100                } else {
     101                        $value = $this->sanitize_choice( $value );
     102                        if ( is_null( $value ) || is_wp_error( $value ) ) {
     103                                return $value;
     104                        }
     105                }
     106
     107                return parent::sanitize( $value );
     108        }
     109
     110        /**
     111         * Sanitizes a header image choice.
     112         *
     113         * This is the value which is ultimately passed to {@see Custom_Image_Header::set_header_image()}, whether
     114         * supplied at the top level of the setting value or nested under its legacy `choice` key.
     115         *
     116         * @since 7.1.1
     117         *
     118         * @param mixed $value Value to sanitize.
     119         * @return array|string|WP_Error|null Sanitized value, or `null`/`WP_Error` if invalid.
     120         *
     121         * @phpstan-return Header_Image_Data|string|WP_Error|null
     122         */
     123        private function sanitize_choice( $value ) {
     124                // Custom_Image_Header::set_header_image() accepts an object in place of an array.
     125                if ( is_object( $value ) ) {
     126                        $value = (array) $value;
     127                }
     128
     129                if ( is_string( $value ) ) {
     130                        return sanitize_text_field( $value );
     131                }
     132
     133                if ( ! is_array( $value ) ) {
     134                        return null;
     135                }
     136
     137                /*
     138                 * The sanitized value is assembled member by member rather than filtered down from the
     139                 * supplied one, so that nothing but the members below can end up in it.
     140                 */
     141                $sanitized = array();
     142
     143                if ( isset( $value['attachment_id'] ) ) {
     144                        if ( ! is_scalar( $value['attachment_id'] ) ) {
     145                                return null;
     146                        }
     147                        $attachment_id = absint( $value['attachment_id'] );
     148
     149                        /*
     150                         * A supplied attachment must be an existing image, since its ID is written to postmeta and its
     151                         * data displayed. Note that an ID of zero must be skipped rather than looked up, as
     152                         * get_post_mime_type() falls back to the global post when passed an empty value.
     153                         */
     154                        if ( $attachment_id > 0 ) {
     155                                $mime_type = get_post_mime_type( $attachment_id );
     156                                if ( ! is_string( $mime_type ) || ! str_starts_with( $mime_type, 'image/' ) ) {
     157                                        return null;
     158                                }
     159                        }
     160
     161                        $sanitized['attachment_id'] = $attachment_id;
     162                }
     163
     164                if ( isset( $value['url'] ) ) {
     165                        if ( ! is_string( $value['url'] ) ) {
     166                                return null;
     167                        }
     168                        $sanitized['url'] = esc_url_raw( $value['url'] );
     169                        if ( '' === $sanitized['url'] ) {
     170                                return new WP_Error( 'invalid_url', __( 'Invalid URL.' ) );
     171                        }
     172                }
     173
     174                if ( isset( $value['thumbnail_url'] ) ) {
     175                        if ( ! is_string( $value['thumbnail_url'] ) ) {
     176                                return null;
     177                        }
     178                        $sanitized['thumbnail_url'] = esc_url_raw( $value['thumbnail_url'] );
     179                        if ( '' === $sanitized['thumbnail_url'] ) {
     180                                return new WP_Error( 'invalid_url', __( 'Invalid URL.' ) );
     181                        }
     182                }
     183
     184                if ( isset( $value['timestamp'] ) ) {
     185                        if ( ! is_scalar( $value['timestamp'] ) ) {
     186                                return null;
     187                        }
     188                        $sanitized['timestamp'] = absint( $value['timestamp'] );
     189                }
     190
     191                if ( isset( $value['width'] ) ) {
     192                        if ( ! is_scalar( $value['width'] ) ) {
     193                                return null;
     194                        }
     195                        $sanitized['width'] = absint( $value['width'] );
     196                }
     197
     198                if ( isset( $value['height'] ) ) {
     199                        if ( ! is_scalar( $value['height'] ) ) {
     200                                return null;
     201                        }
     202                        $sanitized['height'] = absint( $value['height'] );
     203                }
     204
     205                if ( isset( $value['alt_text'] ) ) {
     206                        if ( ! is_string( $value['alt_text'] ) ) {
     207                                return null;
     208                        }
     209                        $sanitized['alt_text'] = sanitize_text_field( $value['alt_text'] );
     210                }
     211
     212                if ( isset( $value['attachment_parent'] ) ) {
     213                        if ( ! is_scalar( $value['attachment_parent'] ) ) {
     214                                return null;
     215                        }
     216                        $sanitized['attachment_parent'] = absint( $value['attachment_parent'] );
     217                }
     218
     219                return $sanitized;
     220        }
    51221}
  • branches/4.7/src/wp-includes/formatting.php

    r52476 r63715  
    540540
    541541        // If a <blockquote> is wrapped with a <p>, move it inside the <blockquote>.
    542         $pee = preg_replace('|<p><blockquote([^>]*)>|i', "<blockquote$1><p>", $pee);
     542        $pee = preg_replace('!<p><blockquote((?:[^>"\']|"[^"]*"|\'[^\']*\')*)>!i', "<blockquote$1><p>", $pee);
    543543        $pee = str_replace('</blockquote></p>', '</p></blockquote>', $pee);
    544544
  • branches/4.7/src/wp-includes/theme.php

    r40338 r63715  
    12311231 *
    12321232 * @since 3.4.0
     1233 * @since 7.1.1 The `width` and `height` are cast to non-negative integers.
    12331234 *
    12341235 * @global array $_wp_default_headers
     
    12681269                'video'         => get_theme_support( 'custom-header', 'video' ),
    12691270        );
    1270         return (object) wp_parse_args( $data, $default );
     1271
     1272        if ( ! is_array( $data ) && ! is_object( $data ) ) {
     1273                $data = array();
     1274        }
     1275        $header         = (object) wp_parse_args( $data, $default );
     1276        $header->width  = absint( $header->width );
     1277        $header->height = absint( $header->height );
     1278        return $header;
    12711279}
    12721280
Note: See TracChangeset for help on using the changeset viewer.