Make WordPress Core

Changeset 63103


Ignore:
Timestamp:
08/06/2026 07:44:27 PM (5 weeks ago)
Author:
desrosj
Message:

Security: Backport the WordPress 7.0.3 security fixes to the 5.9 branch.

  • Users: Ensure a proper email address is used before sending email confirmations.
  • Formatting: Prevent stack overflow in safecss_filter_attr.
  • Multisite: Enforce the active signup policy for existing users.
  • HTTP API: Improve compliance with IPv4 Special-Purpose Address Space.
  • Users: Prevent Usernames from mangling HTML
  • Canonical: Only redirect for publicly viewable post types.
  • Administration: When wp_is_large_user_count(), ensure that the post author is always added to author dropdown.
  • Editor: Fix output for post date.

Merges [63060],[63061],[63062],[63063],[63064],[63065],[63067] to the 5.9 branch.

Props xknown, westonruter, jeremyfelt, peterwilsoncc, paulkevan, lucasbustamante, jorbin, desrosj, vortfu, dmsnell, johnbillion, ehtis, batmoo, lancewillett, jonsurrell, isabel_brison, bernhard-reiter, tyxla, aduth.

Location:
branches/5.9
Files:
11 edited

Legend:

Unmodified
Added
Removed
  • branches/5.9/src/js/_enqueues/admin/inline-edit-post.js

    r50547 r63103  
    278278
    279279                        // The post author no longer has edit capabilities, so we need to add them to the list of authors.
    280                         $(':input[name="post_author"]', editRow).prepend('<option value="' + $('.post_author', rowData).text() + '">' + $('#' + t.type + '-' + id + ' .author').text() + '</option>');
     280                        $(':input[name="post_author"]', editRow).prepend(
     281                                new Option(
     282                                        $('#' + t.type + '-' + id + ' .author').text(),
     283                                        $('.post_author', rowData).text()
     284                                )
     285                        );
    281286                }
    282287                if ( $( ':input[name="post_author"] option', editRow ).length === 1 ) {
  • branches/5.9/src/wp-admin/includes/user.php

    r56875 r63103  
    4545        }
    4646
     47        $errors = new WP_Error();
     48
    4749        $pass1 = '';
    4850        $pass2 = '';
     
    7981
    8082        if ( isset( $_POST['email'] ) ) {
    81                 $user->user_email = sanitize_text_field( wp_unslash( $_POST['email'] ) );
     83                $maybe_email = wp_unslash( $_POST['email'] );
     84                if ( is_string( $maybe_email ) && is_email( $maybe_email ) ) {
     85                        $user->user_email = $maybe_email;
     86                } else {
     87                        $errors->add( 'invalid_email', __( '<strong>Error</strong>: The email address isn&#8217;t correct.' ), array( 'form-field' => 'email' ) );
     88                }
    8289        }
    8390        if ( isset( $_POST['url'] ) ) {
     
    139146                $user->use_ssl = 1;
    140147        }
    141 
    142         $errors = new WP_Error();
    143148
    144149        /* checking that username has been typed */
  • branches/5.9/src/wp-includes/canonical.php

    r51125 r63103  
    913913
    914914        if ( get_query_var( 'name' ) ) {
     915                $publicly_viewable_post_types = array_filter( get_post_types( array( 'exclude_from_search' => false ) ), 'is_post_type_viewable' );
     916
    915917                /**
    916918                 * Filters whether to perform a strict guess for a 404 redirect.
     
    933935                if ( get_query_var( 'post_type' ) ) {
    934936                        if ( is_array( get_query_var( 'post_type' ) ) ) {
    935                                 // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
    936                                 $where .= " AND post_type IN ('" . join( "', '", esc_sql( get_query_var( 'post_type' ) ) ) . "')";
     937                                $post_types = array_intersect( get_query_var( 'post_type' ), $publicly_viewable_post_types );
     938                                if ( empty( $post_types ) ) {
     939                                        return false;
     940                                }
     941                                $where .= " AND post_type IN ('" . implode( "', '", esc_sql( $post_types ) ) . "')";
    937942                        } else {
     943                                if ( ! in_array( get_query_var( 'post_type' ), $publicly_viewable_post_types, true ) ) {
     944                                        return false;
     945                                }
    938946                                $where .= $wpdb->prepare( ' AND post_type = %s', get_query_var( 'post_type' ) );
    939947                        }
    940948                } else {
    941                         $where .= " AND post_type IN ('" . implode( "', '", get_post_types( array( 'public' => true ) ) ) . "')";
     949                        $where .= " AND post_type IN ('" . implode( "', '", esc_sql( $publicly_viewable_post_types ) ) . "')";
    942950                }
    943951
  • branches/5.9/src/wp-includes/http.php

    r52441 r63103  
    554554                if ( $ip ) {
    555555                        $parts = array_map( 'intval', explode( '.', $ip ) );
    556                         if ( 127 === $parts[0] || 10 === $parts[0] || 0 === $parts[0]
    557                                 || ( 172 === $parts[0] && 16 <= $parts[1] && 31 >= $parts[1] )
    558                                 || ( 192 === $parts[0] && 168 === $parts[1] )
     556
     557                        /*
     558                         * These IP address ranges are not considered valid external hosts for HTTP requests.
     559                         *
     560                         * If the host resolves to an IP address in these ranges, the request will be rejected unless the 'http_request_host_is_external' filter allows it.
     561                         *
     562                         * References:
     563                         *
     564                         * - IPv4 Special-Purpose Address Space: https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml
     565                         * - IPv4 Multicast Address Assignments: https://www.rfc-editor.org/rfc/rfc5771.html
     566                         */
     567                        if ( 127 === $parts[0] || 10 === $parts[0] || 0 === $parts[0]          // 127.0.0.0/8 (loopback), 10.0.0.0/8 (private), 0.0.0.0/8 (this network).
     568                                || ( 172 === $parts[0] && 16 <= $parts[1] && 31 >= $parts[1] )     // 172.16.0.0/12 (private).
     569                                || ( 192 === $parts[0] && 168 === $parts[1] )                      // 192.168.0.0/16 (private).
     570                                || ( 192 === $parts[0] && 0 === $parts[1] && 0 === $parts[2] )     // 192.0.0.0/24 (IETF protocol assignments).
     571                                || ( 192 === $parts[0] && 0 === $parts[1] && 2 === $parts[2] )     // 192.0.2.0/24 (TEST-NET-1).
     572                                || ( 192 === $parts[0] && 88 === $parts[1] && 99 === $parts[2] )   // 192.88.99.0/24 (6to4 relay anycast).
     573                                || ( 198 === $parts[0] && 51 === $parts[1] && 100 === $parts[2] )  // 198.51.100.0/24 (TEST-NET-2).
     574                                || ( 203 === $parts[0] && 0 === $parts[1] && 113 === $parts[2] )   // 203.0.113.0/24 (TEST-NET-3).
     575                                || ( 169 === $parts[0] && 254 === $parts[1] )                      // 169.254.0.0/16 (link-local and cloud metadata).
     576                                || ( 100 === $parts[0] && 64 <= $parts[1] && 127 >= $parts[1] )    // 100.64.0.0/10 (CGNAT).
     577                                || ( 198 === $parts[0] && 18 <= $parts[1] && 19 >= $parts[1] )     // 198.18.0.0/15 (benchmarking).
     578                                || ( 224 <= $parts[0] && 239 >= $parts[0] )                        // 224.0.0.0/4 (multicast).
     579                                || 240 <= $parts[0]                                                // 240.0.0.0/4 (reserved, includes 255.255.255.255 broadcast).
    559580                        ) {
    560581                                // If host appears local, reject unless specifically allowed.
  • branches/5.9/src/wp-includes/kses.php

    r61951 r63103  
    25152515                        // Allow CSS calc().
    25162516                        $css_test_string = preg_replace( '/calc\(((?:\([^()]*\)?|[^()])*)\)/', '', $css_test_string );
     2517                        if ( null === $css_test_string ) {
     2518                                continue;
     2519                        }
     2520
    25172521                        // Allow CSS var().
    25182522                        $css_test_string = preg_replace( '/\(?var\(--[a-zA-Z0-9_-]*\)/', '', $css_test_string );
     2523                        if ( null === $css_test_string ) {
     2524                                continue;
     2525                        }
    25192526
    25202527                        // Check for any CSS containing \ ( & } = or comments,
    25212528                        // except for url(), calc(), or var() usage checked above.
    2522                         $allow_css = ! preg_match( '%[\\\(&=}]|/\*%', $css_test_string );
     2529                        $allow_css = 0 === preg_match( '%[\\\(&=}]|/\*%', $css_test_string );
    25232530
    25242531                        /**
  • branches/5.9/src/wp-includes/user.php

    r54545 r63103  
    153153                                /* translators: %s: User name. */
    154154                                __( '<strong>Error</strong>: The username <strong>%s</strong> is not registered on this site. If you are unsure of your username, try your email address instead.' ),
    155                                 $username
     155                                esc_html( $username )
    156156                        )
    157157                );
     
    178178                                /* translators: %s: User name. */
    179179                                __( '<strong>Error</strong>: The password you entered for the username %s is incorrect.' ),
    180                                 '<strong>' . $username . '</strong>'
     180                                '<strong>' . esc_html( $username ) . '</strong>'
    181181                        ) .
    182182                        ' <a href="' . wp_lostpassword_url() . '">' .
     
    250250                                /* translators: %s: Email address. */
    251251                                __( '<strong>Error</strong>: The password you entered for the email address %s is incorrect.' ),
    252                                 '<strong>' . $email . '</strong>'
     252                                '<strong>' . esc_html( $email ) . '</strong>'
    253253                        ) .
    254254                        ' <a href="' . wp_lostpassword_url() . '">' .
     
    31043104                                /* translators: %s: Link to the login page. */
    31053105                                __( '<strong>Error:</strong> This email address is already registered. <a href="%s">Log in</a> with this address or choose another one.' ),
    3106                                 wp_login_url()
     3106                                esc_url( wp_login_url() )
    31073107                        )
    31083108                );
     
    31523152                                /* translators: %s: Admin email address. */
    31533153                                __( '<strong>Error</strong>: Couldn&#8217;t register you&hellip; please contact the <a href="mailto:%s">site admin</a>!' ),
    3154                                 get_option( 'admin_email' )
     3154                                esc_attr( get_option( 'admin_email' ) )
    31553155                        )
    31563156                );
     
    33753375 * @since 3.0.0
    33763376 * @since 4.9.0 This function was moved from wp-admin/includes/ms.php so it's no longer Multisite specific.
     3377 * @since 7.0.3 Added the `$user_id` parameter, which is sent with the `personal_options_update` action.
     3378 *
     3379 * @param int $user_id Optional. The ID of the user whose email is being changed. Defaults to `$_POST['user_id']` if set, otherwise 0.
    33773380 *
    33783381 * @global WP_Error $errors WP_Error object.
    33793382 */
    3380 function send_confirmation_on_profile_email() {
     3383function send_confirmation_on_profile_email( $user_id = 0 ) {
    33813384        global $errors;
     3385
     3386        // Maintain backward compatibility for those relying on a check based on $_POST['user_id'].
     3387        if ( ! $user_id && isset( $_POST['user_id'] ) ) {
     3388                $user_id = (int) $_POST['user_id'];
     3389        }
    33823390
    33833391        $current_user = wp_get_current_user();
     
    33863394        }
    33873395
    3388         if ( $current_user->ID != $_POST['user_id'] ) {
     3396        if ( 0 === $current_user->ID || $current_user->ID !== (int) $user_id ) {
    33893397                return false;
    33903398        }
     
    34003408                        );
    34013409
     3410                        $_POST['email'] = addslashes( $current_user->user_email );
    34023411                        return;
    34033412                }
     
    34133422                        delete_user_meta( $current_user->ID, '_new_email' );
    34143423
     3424                        $_POST['email'] = addslashes( $current_user->user_email );
    34153425                        return;
    34163426                }
  • branches/5.9/src/wp-login.php

    r52435 r63103  
    11101110                                        /* translators: %s: Link to the login page. */
    11111111                                        __( 'Check your email for the confirmation link, then visit the <a href="%s">login page</a>.' ),
    1112                                         wp_login_url()
     1112                                        esc_url( wp_login_url() )
    11131113                                ),
    11141114                                'message'
     
    11201120                                        /* translators: %s: Link to the login page. */
    11211121                                        __( 'Registration complete. Please check your email, then visit the <a href="%s">login page</a>.' ),
    1122                                         wp_login_url()
     1122                                        esc_url( wp_login_url() )
    11231123                                ),
    11241124                                'message'
  • branches/5.9/src/wp-signup.php

    r51930 r63103  
    965965                        break;
    966966                case 'gimmeanotherblog':
    967                         validate_another_blog_signup();
     967                        if ( 'all' === $active_signup || 'blog' === $active_signup ) {
     968                                validate_another_blog_signup();
     969                        } else {
     970                                _e( 'Site registration has been disabled.' );
     971                        }
    968972                        break;
    969973                case 'default':
  • branches/5.9/tests/phpunit/tests/auth.php

    r52157 r63103  
    395395                $check = check_password_reset_key( '', $this->user->user_login );
    396396                $this->assertInstanceOf( 'WP_Error', $check );
     397        }
     398
     399        /**
     400         * @dataProvider data_wp_authenticate_username_password_with_invalid_login_messages
     401         *
     402         * @ticket security-1307
     403         *
     404         * @param string $username         Username to attempt to authenticate with.
     405         * @param string $expected_message Expected error message.
     406         */
     407        public function test_wp_authenticate_username_password_with_invalid_login_messages( $username, $expected_message ) {
     408                $result = wp_authenticate_username_password( null, $username, 'password' );
     409                $this->assertInstanceOf( 'WP_Error', $result );
     410                $this->assertSame( $expected_message, $result->get_error_message() );
     411        }
     412
     413        public function data_wp_authenticate_username_password_with_invalid_login_messages() {
     414                return array(
     415                        'invalid_username' => array(
     416                                'invalid_username',
     417                                '<strong>Error</strong>: The username <strong>invalid_username</strong> is not registered on this site. If you are unsure of your username, try your email address instead.',
     418                        ),
     419                        'xxs_username'     => array(
     420                                '<script>alert(1);</script>',
     421                                '<strong>Error</strong>: The username <strong>&lt;script&gt;alert(1);&lt;/script&gt;</strong> is not registered on this site. If you are unsure of your username, try your email address instead.',
     422                        ),
     423                );
    397424        }
    398425
  • branches/5.9/tests/phpunit/tests/canonical.php

    r52010 r63103  
    1111class Tests_Canonical extends WP_Canonical_UnitTestCase {
    1212
     13        public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) {
     14                // Set up fixtures in WP_Canonical_UnitTestCase.
     15                parent::wpSetUpBeforeClass( $factory );
     16
     17                self::set_up_custom_post_types();
     18
     19                $factory->post->create(
     20                        array(
     21                                'post_type'  => 'wp_tests_excluded',
     22                                'post_title' => 'private-cpt-post',
     23                        )
     24                );
     25        }
     26
    1327        public function set_up() {
    1428                parent::set_up();
    1529                wp_set_current_user( self::$author_id );
     30        }
     31
     32        /**
     33         * Register custom post types for tests.
     34         *
     35         * Register non publicly queryable post type with public set to true.
     36         *
     37         * These arguments are intentionally contradictory for the test associated
     38         * with ticket #59795.
     39         */
     40        public static function set_up_custom_post_types() {
     41                register_post_type(
     42                        'wp_tests_excluded',
     43                        array(
     44                                'publicly_queryable'  => true,
     45                                'exclude_from_search' => true,
     46                        )
     47                );
    1648        }
    1749
     
    277309
    278310        /**
    279          * Ensure multiple post types do not throw a notice.
     311         * Ensure redirect guessing searches only requested, public, searchable post types.
    280312         *
    281313         * @ticket 43056
    282          */
    283         public function test_redirect_guess_404_permalink_post_types() {
    284                 /*
    285                  * Sample-page is intentionally missspelt as sample-pag to ensure
    286                  * the 404 post permalink guessing runs.
    287                  *
    288                  * Please do not correct the apparent typo.
    289                  */
    290 
    291                 // String format post type.
    292                 $this->assertCanonical( '/?name=sample-pag&post_type=page', '/sample-page/' );
    293                 // Array formatted post type or types.
    294                 $this->assertCanonical( '/?name=sample-pag&post_type[]=page', '/sample-page/' );
    295                 $this->assertCanonical( '/?name=sample-pag&post_type[]=page&post_type[]=post', '/sample-page/' );
     314         * @ticket 59795
     315         * @ticket security-1301
     316         *
     317         * @dataProvider data_redirect_guess_404_permalink_post_types
     318         */
     319        public function test_redirect_guess_404_permalink_post_types( $original_url, $expected ) {
     320                $this->assertCanonical( $original_url, $expected );
     321        }
     322
     323        /**
     324         * Data provider for test_redirect_guess_404_permalink_post_types().
     325         *
     326         * In the original URLs the post names are intentionally misspelled
     327         * to test the redirection.
     328         *
     329         * Please do not correct the apparent typos.
     330         *
     331         * @return array[]
     332         */
     333        public function data_redirect_guess_404_permalink_post_types() {
     334                return array(
     335                        'single string formatted post type'    => array(
     336                                'original_url' => '/?name=sample-pag&post_type=page',
     337                                'expected'     => '/sample-page/',
     338                        ),
     339                        'single array formatted post type'     => array(
     340                                'original_url' => '/?name=sample-pag&post_type[]=page',
     341                                'expected'     => '/sample-page/',
     342                        ),
     343                        'do not search unrequested post types' => array(
     344                                'original_url' => '/?name=sample-pag&post_type[]=post',
     345                                'expected'     => '/?name=sample-pag&post_type[]=post',
     346                        ),
     347                        'multiple array formatted post type'   => array(
     348                                'original_url' => '/?name=sample-pag&post_type[]=page&post_type[]=post',
     349                                'expected'     => '/sample-page/',
     350                        ),
     351                        'do not redirect to private post type' => array(
     352                                'original_url' => '/?name=private-cpt-po&post_type[]=wp_tests_private',
     353                                'expected'     => '/?name=private-cpt-po&post_type[]=wp_tests_private',
     354                        ),
     355                        'mixed public and excluded post types' => array(
     356                                'original_url' => '/?name=excluded-cpt-po&post_type[]=post&post_type[]=wp_tests_excluded',
     357                                'expected'     => '/?name=excluded-cpt-po&post_type[]=post&post_type[]=wp_tests_excluded',
     358                        ),
     359                        'mixed post types with public match'   => array(
     360                                'original_url' => '/?name=sample-pag&post_type[]=page&post_type[]=wp_tests_excluded',
     361                                'expected'     => '/sample-page/',
     362                        ),
     363                );
    296364        }
    297365
  • branches/5.9/tests/phpunit/tests/kses.php

    r54764 r63103  
    934934
    935935        /**
     936         * Tests that CSS is rejected when recursive function stripping triggers a PCRE error.
     937         *
     938         * This preserves the current behavior for normal CSS and only fails closed when
     939         * the sanitizer cannot safely evaluate an extreme nested-function payload.
     940         *
     941         * @ticket security-1186
     942         */
     943        public function test_safecss_filter_attr_rejects_css_when_pcre_error_occurs() {
     944                // Force the recursive function-stripping regex to fail deterministically, regardless of
     945                // whether PCRE JIT is enabled or how large its stack is, by lowering the backtrack limit.
     946                $backtrack_limit = ini_set( 'pcre.backtrack_limit', '100' );
     947                $this->assertNotFalse( $backtrack_limit, 'Failed to call ini_set().' );
     948
     949                $css = 'color:var(' . str_repeat( '(', 200 ) . str_repeat( ')', 200 ) . ')';
     950
     951                try {
     952                        $this->assertSame( '', safecss_filter_attr( $css ) );
     953                } finally {
     954                        ini_set( 'pcre.backtrack_limit', $backtrack_limit );
     955                }
     956        }
     957
     958        /**
    936959         * Data Provider for test_safecss_filter_attr().
    937960         *
Note: See TracChangeset for help on using the changeset viewer.