Make WordPress Core

Ticket #46595: 46595.8.patch

File 46595.8.patch, 15.3 KB (added by pbearne, 6 years ago)

changes from @flixos90 comments

  • src/wp-includes/class-wp-recovery-mode-key-service.php

     
    88
    99/**
    1010 * Core class used to generate and validate keys used to enter Recovery Mode.
     11 * The token is used to index the array of hashed_key and created_at stored in options
    1112 *
    1213 * @since 5.2.0
    1314 */
     
    1415final class WP_Recovery_Mode_Key_Service {
    1516
    1617        /**
     18         * The ID option used to store the keys.
     19         *
     20         * @since 5.2.0
     21         * @var string
     22         */
     23        private $option_id = 'recovery_keys';
     24
     25        /**
     26         * WP_Recovery_Mode_Key_Service constructor.
     27         *
     28         * @since 5.2.0
     29         */
     30        public function __construct() {
     31
     32                add_action( 'WP_Recovery_Mode_Key_Service_clean_expired_keys', array( $this, 'clean_expired_keys' ) );
     33        }
     34
     35        /**
    1736         * Creates a recovery mode key.
    1837         *
    1938         * @since 5.2.0
     
    2039         *
    2140         * @global PasswordHash $wp_hasher
    2241         *
    23          * @return string Recovery mode key.
     42         * @return string $key Recovery mode key.
    2443         */
    25         public function generate_and_store_recovery_mode_key() {
     44        public function generate_and_store_recovery_mode_key( $token ) {
    2645
    2746                global $wp_hasher;
    2847
    2948                $key = wp_generate_password( 22, false );
    3049
    31                 /**
    32                  * Fires when a recovery mode key is generated for a user.
    33                  *
    34                  * @since 5.2.0
    35                  *
    36                  * @param string $key The recovery mode key.
    37                  */
    38                 do_action( 'generate_recovery_mode_key', $key );
    39 
    4050                if ( empty( $wp_hasher ) ) {
    4151                        require_once ABSPATH . WPINC . '/class-phpass.php';
    4252                        $wp_hasher = new PasswordHash( 8, true );
     
    4454
    4555                $hashed = $wp_hasher->HashPassword( $key );
    4656
    47                 update_option(
    48                         'recovery_key',
    49                         array(
    50                                 'hashed_key' => $hashed,
    51                                 'created_at' => time(),
    52                         )
     57                $records = get_option( $this->option_id, array() );
     58
     59                $records[ $token ] = array(
     60                        'hashed_key' => $hashed,
     61                        'created_at' => time(),
    5362                );
    5463
     64                update_option( $this->option_id, $records );
     65
     66                /**
     67                 * Fires when a recovery mode key is generated for a user.
     68                 *
     69                 * @since 5.2.0
     70                 *
     71                 * @param string $key   The recovery mode key.
     72                 * @param string $token The recovery data index.
     73                 */
     74                do_action( 'generate_recovery_mode_key', $key, $token );
     75
    5576                return $key;
    5677        }
    5778
    5879        /**
     80         * Get a token
     81         *
     82         * @since 5.2.0
     83         *
     84         * @return string $token used to key the keys in options
     85         */
     86        public function generate_recovery_mode_token(){
     87
     88                return wp_generate_password( 22, false );
     89        }
     90
     91        /**
    5992         * Verifies if the recovery mode key is correct.
     93         * Removes the key  passed from the possible key to make it a one-time key
     94         * Removes any old keys and the key passed to it
    6095         *
    6196         * @since 5.2.0
    6297         *
    63          * @param string $key The unhashed key.
    64          * @param int    $ttl Time in seconds for the key to be valid for.
    65          * @return true|WP_Error True on success, error object on failure.
     98         * @param  string $key    The unhashed key.
     99         * @param  string $token  The data index
     100         * @param  int    $ttl    Time in seconds for the key to be valid for.
     101         * @return true|WP_Error  True on success, error object on failure.
    66102         */
    67         public function validate_recovery_mode_key( $key, $ttl ) {
     103        public function validate_recovery_mode_key( $key, $token, $ttl ) {
    68104
    69                 $record = get_option( 'recovery_key' );
     105                $records = get_option( $this->option_id, array() );
    70106
    71                 if ( ! $record ) {
    72                         return new WP_Error( 'no_recovery_key_set', __( 'Recovery Mode not initialized.' ) );
     107                if ( ! isset( $records[ $token ] ) ) {
     108                        return new WP_Error( 'recovery_keys_data_missing', __( 'Recovery Mode not initialized.' ) );
    73109                }
    74110
     111                $record = $records[ $token ];
     112
     113                $this->clean_key($token );
     114                $this->clean_expired_keys( $ttl );
     115
    75116                if ( ! is_array( $record ) || ! isset( $record['hashed_key'], $record['created_at'] ) ) {
    76                         return new WP_Error( 'invalid_recovery_key_format', __( 'Invalid recovery key format.' ) );
     117                        return new WP_Error( 'invalid_recovery_keys_format', __( 'Invalid recovery key format.' ) );
    77118                }
    78119
    79120                if ( ! wp_check_password( $key, $record['hashed_key'] ) ) {
     
    86127
    87128                return true;
    88129        }
     130
     131        /**
     132         * Removes expired recovery keys.
     133         * Runs hourly
     134         *
     135         * @since 5.2.0
     136         *
     137         * @param int $ttl  Number of seconds the links should be valid for
     138         */
     139        public function clean_expired_keys( $ttl ) {
     140
     141                $records = get_option( $this->option_id );
     142
     143                foreach ( $records as $key => $record ) {
     144                        if ( ! isset( $record['created_at'] ) || time() > $record['created_at'] + $ttl ) {
     145                                unset( $records[ $key ] );
     146                        }
     147                }
     148
     149                update_option( $this->option_id, $records );
     150        }
     151
     152        /**
     153         * Removes a used recovery key.
     154         *
     155         * @since 5.2.0
     156         *
     157         * @param string $token The data index
     158         */
     159        public function clean_key( $token ) {
     160
     161                $records = get_option( $this->option_id );
     162
     163                if ( '' !== $token ) {
     164                        unset( $records[ $token ] );
     165                }
     166
     167                update_option( $this->option_id, $records );
     168        }
    89169}
  • src/wp-includes/class-wp-recovery-mode-link-service.php

     
    5353         * @return string Generated URL.
    5454         */
    5555        public function generate_url() {
    56                 $key = $this->key_service->generate_and_store_recovery_mode_key();
     56                $token = $this->key_service->generate_recovery_mode_token();
     57                $key   = $this->key_service->generate_and_store_recovery_mode_key( $token );
    5758
    58                 return $this->get_recovery_mode_begin_url( $key );
     59                return $this->get_recovery_mode_begin_url( $key, $token );
    5960        }
    6061
    6162        /**
     
    7071                        return;
    7172                }
    7273
    73                 if ( ! isset( $_GET['action'], $_GET['rm_key'] ) || self::LOGIN_ACTION_ENTER !== $_GET['action'] ) {
     74                if ( ! isset( $_GET['action'], $_GET['rm_key'], $_GET['rm_token'] ) || self::LOGIN_ACTION_ENTER !== $_GET['action'] ) {
    7475                        return;
    7576                }
    7677
     
    7879                        require_once ABSPATH . WPINC . '/pluggable.php';
    7980                }
    8081
    81                 $validated = $this->key_service->validate_recovery_mode_key( $_GET['rm_key'], $ttl );
     82                $validated = $this->key_service->validate_recovery_mode_key( $_GET['rm_key'], $_GET['rm_token'], $ttl );
    8283
    8384                if ( is_wp_error( $validated ) ) {
    8485                        wp_die( $validated, '' );
     
    9798         * @since 5.2.0
    9899         *
    99100         * @param string $key Recovery Mode key created by {@see generate_and_store_recovery_mode_key()}
     101         *
     102         * @param string $token Recovery Mode data index created by {@see generate_and_store_recovery_mode_key()}
     103         *
    100104         * @return string Recovery mode begin URL.
    101105         */
    102         private function get_recovery_mode_begin_url( $key ) {
     106        private function get_recovery_mode_begin_url( $key, $token ) {
    103107
    104108                $url = add_query_arg(
    105109                        array(
    106                                 'action' => self::LOGIN_ACTION_ENTER,
    107                                 'rm_key' => $key,
     110                                'action'   => self::LOGIN_ACTION_ENTER,
     111                                'rm_key'   => $key,
     112                                'rm_token' => $token,
    108113                        ),
    109114                        wp_login_url()
    110115                );
     
    116121                 *
    117122                 * @param string $url
    118123                 * @param string $key
     124                 * @param string $token
    119125                 */
    120                 return apply_filters( 'recovery_mode_begin_url', $url, $key );
     126                return apply_filters( 'recovery_mode_begin_url', $url, $key, $token );
    121127        }
    122128}
  • src/wp-includes/class-wp-recovery-mode.php

     
    7272                $this->cookie_service = new WP_Recovery_Mode_Cookie_Service();
    7373                $this->link_service   = new WP_Recovery_Mode_Link_Service( $this->cookie_service );
    7474                $this->email_service  = new WP_Recovery_Mode_Email_Service( $this->link_service );
     75
     76                $args = array( $this->get_link_ttl() );
     77                if (! wp_next_scheduled ( 'WP_Recovery_Mode_Key_Service_clean_expired_keys', $args ) ) {
     78                        wp_schedule_event( time(), 'hourly', 'WP_Recovery_Mode_Key_Service_clean_expired_keys', $args );
     79                }
    7580        }
    7681
    7782        /**
  • tests/phpunit/tests/error-protection/recovery-mode-key-service.php

     
    1010         */
    1111        public function test_generate_and_store_recovery_mode_key_returns_recovery_key() {
    1212                $service = new WP_Recovery_Mode_Key_Service();
    13                 $key     = $service->generate_and_store_recovery_mode_key();
     13                $token = $service->generate_recovery_mode_token();
     14                $key   = $service->generate_and_store_recovery_mode_key( $token );
    1415
    1516                $this->assertNotWPError( $key );
    1617        }
     
    2021         */
    2122        public function test_validate_recovery_mode_key_returns_wp_error_if_no_key_set() {
    2223                $service = new WP_Recovery_Mode_Key_Service();
    23                 $error   = $service->validate_recovery_mode_key( 'abcd', HOUR_IN_SECONDS );
     24                $error   = $service->validate_recovery_mode_key( 'abcd', '', HOUR_IN_SECONDS );
    2425
    2526                $this->assertWPError( $error );
    26                 $this->assertEquals( 'no_recovery_key_set', $error->get_error_code() );
     27                $this->assertEquals( 'recovery_keys_data_missing', $error->get_error_code() );
    2728        }
     29        /**
     30         * @ticket 46130
     31         */
     32        public function test_validate_recovery_mode_key_returns_wp_error_if_data_missing() {
     33                update_option( 'recovery_keys', 'gibberish' );
    2834
     35                $service = new WP_Recovery_Mode_Key_Service();
     36                $error   = $service->validate_recovery_mode_key( 'abcd', '', HOUR_IN_SECONDS );
     37
     38                $this->assertWPError( $error );
     39                $this->assertEquals( 'recovery_keys_data_missing', $error->get_error_code() );
     40        }
     41
    2942        /**
    3043         * @ticket 46130
    3144         */
     45        public function test_validate_recovery_mode_key_returns_wp_error_if_bad() {
     46                update_option( 'recovery_keys', array( 'token' => 'gibberish' ) );
     47
     48                $service = new WP_Recovery_Mode_Key_Service();
     49                $error   = $service->validate_recovery_mode_key( 'abcd', 'token', HOUR_IN_SECONDS );
     50
     51                $this->assertWPError( $error );
     52                $this->assertEquals( 'invalid_recovery_keys_format', $error->get_error_code() );
     53        }
     54
     55
     56        /**
     57         * @ticket 46130
     58         */
    3259        public function test_validate_recovery_mode_key_returns_wp_error_if_stored_format_is_invalid() {
    33                 update_option( 'recovery_key', 'gibberish' );
    3460
     61                $token =  wp_generate_password( 22, false );
     62                update_option( 'recovery_keys', array( $token => 'gibberish' ) );
     63
    3564                $service = new WP_Recovery_Mode_Key_Service();
    36                 $error   = $service->validate_recovery_mode_key( 'abcd', HOUR_IN_SECONDS );
     65                $error   = $service->validate_recovery_mode_key( 'abcd', $token, HOUR_IN_SECONDS );
    3766
    3867                $this->assertWPError( $error );
    39                 $this->assertEquals( 'invalid_recovery_key_format', $error->get_error_code() );
     68                $this->assertEquals( 'invalid_recovery_keys_format', $error->get_error_code() );
    4069        }
    4170
    4271        /**
     
    4473         */
    4574        public function test_validate_recovery_mode_key_returns_wp_error_if_empty_key() {
    4675                $service = new WP_Recovery_Mode_Key_Service();
    47                 $service->generate_and_store_recovery_mode_key();
    48                 $error = $service->validate_recovery_mode_key( '', HOUR_IN_SECONDS );
     76                $token = $service->generate_recovery_mode_token();
     77                $service->generate_and_store_recovery_mode_key( $token );
     78                $error = $service->validate_recovery_mode_key( '', $token, HOUR_IN_SECONDS );
    4979
    5080                $this->assertWPError( $error );
    5181                $this->assertEquals( 'hash_mismatch', $error->get_error_code() );
     
    5686         */
    5787        public function test_validate_recovery_mode_key_returns_wp_error_if_hash_mismatch() {
    5888                $service = new WP_Recovery_Mode_Key_Service();
    59                 $service->generate_and_store_recovery_mode_key();
    60                 $error = $service->validate_recovery_mode_key( 'abcd', HOUR_IN_SECONDS );
     89                $token = $service->generate_recovery_mode_token();
     90                $service->generate_and_store_recovery_mode_key( $token );
     91                $error = $service->validate_recovery_mode_key( 'abcd', $token, HOUR_IN_SECONDS );
    6192
    6293                $this->assertWPError( $error );
    6394                $this->assertEquals( 'hash_mismatch', $error->get_error_code() );
     
    6899         */
    69100        public function test_validate_recovery_mode_key_returns_wp_error_if_expired() {
    70101                $service = new WP_Recovery_Mode_Key_Service();
    71                 $key     = $service->generate_and_store_recovery_mode_key();
     102                $token = $service->generate_recovery_mode_token();
     103                $key   = $service->generate_and_store_recovery_mode_key( $token );
    72104
    73                 $record               = get_option( 'recovery_key' );
    74                 $record['created_at'] = time() - HOUR_IN_SECONDS - 30;
    75                 update_option( 'recovery_key', $record );
     105                $records                         = get_option( 'recovery_keys' );
     106                $records[ $token ]['created_at'] = time() - HOUR_IN_SECONDS - 30;
     107                update_option( 'recovery_keys', $records );
    76108
    77                 $error = $service->validate_recovery_mode_key( $key, HOUR_IN_SECONDS );
     109                $error = $service->validate_recovery_mode_key( $key, $token, HOUR_IN_SECONDS );
    78110
    79111                $this->assertWPError( $error );
    80112                $this->assertEquals( 'key_expired', $error->get_error_code() );
     
    85117         */
    86118        public function test_validate_recovery_mode_key_returns_true_for_valid_key() {
    87119                $service = new WP_Recovery_Mode_Key_Service();
    88                 $key     = $service->generate_and_store_recovery_mode_key();
    89                 $this->assertTrue( $service->validate_recovery_mode_key( $key, HOUR_IN_SECONDS ) );
     120                $token = $service->generate_recovery_mode_token();
     121                $key   = $service->generate_and_store_recovery_mode_key( $token );
     122                $this->assertTrue( $service->validate_recovery_mode_key( $key, $token, HOUR_IN_SECONDS ) );
    90123        }
     124
     125
     126        /**
     127         * @ticket 46130
     128         */
     129        public function test_validate_recovery_mode_key_returns_error_if_token_used_more_than_once() {
     130                $service = new WP_Recovery_Mode_Key_Service();
     131                $token = $service->generate_recovery_mode_token();
     132                $key   = $service->generate_and_store_recovery_mode_key( $token );
     133
     134                $this->assertTrue( $service->validate_recovery_mode_key( $key, $token, HOUR_IN_SECONDS ) );
     135
     136                // data should be remove by first call
     137                $error  = $service->validate_recovery_mode_key( $key, $token, HOUR_IN_SECONDS );
     138
     139                $this->assertWPError( $error );
     140                $this->assertEquals( 'recovery_keys_data_missing', $error->get_error_code() );
     141        }
     142
     143
     144        /**
     145         * @ticket 46130
     146         */
     147        public function test_validate_recovery_mode_key_returns_error_if_token_used_more_than_once_more_than_key_stored() {
     148                $service = new WP_Recovery_Mode_Key_Service();
     149
     150                //              create an extra key
     151                $token = $service->generate_recovery_mode_token();
     152                $service->generate_and_store_recovery_mode_key( $token );
     153
     154                $token = $service->generate_recovery_mode_token();
     155                $key   = $service->generate_and_store_recovery_mode_key( $token );
     156
     157                $this->assertTrue( $service->validate_recovery_mode_key( $key, $token, HOUR_IN_SECONDS ) );
     158
     159                // data should be remove by first call
     160                $error  = $service->validate_recovery_mode_key( $key, $token, HOUR_IN_SECONDS );
     161
     162                $this->assertWPError( $error );
     163                $this->assertEquals( 'recovery_keys_data_missing', $error->get_error_code() );
     164        }
     165
     166
     167        /**
     168         * @ticket 46130
     169         */
     170        public function test_clean_expired_keys() {
     171                $service = new WP_Recovery_Mode_Key_Service();
     172                $token = $service->generate_recovery_mode_token();
     173                $service->generate_and_store_recovery_mode_key( $token );
     174
     175                $records                         = get_option( 'recovery_keys' );
     176                $records[ $token ]['created_at'] = time() - HOUR_IN_SECONDS - 30;
     177                update_option( 'recovery_keys', $records );
     178
     179                $service->clean_expired_keys(  HOUR_IN_SECONDS );
     180
     181                $this->assertEmpty( get_option( 'recovery_keys' ) );
     182        }
     183
    91184}