Make WordPress Core

Ticket #46595: 46595.3.diff

File 46595.3.diff, 18.0 KB (added by flixos90, 5 years ago)
  • src/wp-includes/class-wp-recovery-mode-key-service.php

     
    11<?php
    22/**
    3  * Error Protection API: WP_Recovery_Mode_Key_service class
     3 * Error Protection API: WP_Recovery_Mode_Key_Service class
    44 *
    55 * @package WordPress
    66 * @since   5.2.0
     
    1414final class WP_Recovery_Mode_Key_Service {
    1515
    1616        /**
     17         * The option name used to store the keys.
     18         *
     19         * @since 5.2.0
     20         * @var string
     21         */
     22        private $option_name = 'recovery_keys';
     23
     24        /**
     25         * Creates a recovery mode token.
     26         *
     27         * @since 5.2.0
     28         *
     29         * @return string $token A random string to identify its associated key in storage.
     30         */
     31        public function generate_recovery_mode_token() {
     32                return wp_generate_password( 22, false );
     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         * @param string $token A token generated by {@see generate_recovery_mode_token()}.
     43         * @return string $key Recovery mode key.
    2444         */
    25         public function generate_and_store_recovery_mode_key() {
     45        public function generate_and_store_recovery_mode_key( $token ) {
    2646
    2747                global $wp_hasher;
    2848
    2949                $key = wp_generate_password( 22, false );
    3050
    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 
    4051                if ( empty( $wp_hasher ) ) {
    4152                        require_once ABSPATH . WPINC . '/class-phpass.php';
    4253                        $wp_hasher = new PasswordHash( 8, true );
     
    4455
    4556                $hashed = $wp_hasher->HashPassword( $key );
    4657
    47                 update_option(
    48                         'recovery_key',
    49                         array(
    50                                 'hashed_key' => $hashed,
    51                                 'created_at' => time(),
    52                         )
     58                $records = $this->get_keys();
     59
     60                $records[ $token ] = array(
     61                        'hashed_key' => $hashed,
     62                        'created_at' => time(),
    5363                );
    5464
     65                $this->update_keys( $records );
     66
     67                /**
     68                 * Fires when a recovery mode key is generated.
     69                 *
     70                 * @since 5.2.0
     71                 *
     72                 * @param string $token The recovery data token.
     73                 * @param string $key   The recovery mode key.
     74                 */
     75                do_action( 'generate_recovery_mode_key', $token, $key );
     76
    5577                return $key;
    5678        }
    5779
     
    5880        /**
    5981         * Verifies if the recovery mode key is correct.
    6082         *
     83         * Recovery mode keys can only be used once; the key will be consumed in the process.
     84         *
    6185         * @since 5.2.0
    6286         *
    63          * @param string $key The unhashed key.
    64          * @param int    $ttl Time in seconds for the key to be valid for.
     87         * @param string $token The token used when generating the given key.
     88         * @param string $key   The unhashed key.
     89         * @param int    $ttl   Time in seconds for the key to be valid for.
    6590         * @return true|WP_Error True on success, error object on failure.
    6691         */
    67         public function validate_recovery_mode_key( $key, $ttl ) {
     92        public function validate_recovery_mode_key( $token, $key, $ttl ) {
    6893
    69                 $record = get_option( 'recovery_key' );
     94                $records = $this->get_keys();
    7095
    71                 if ( ! $record ) {
    72                         return new WP_Error( 'no_recovery_key_set', __( 'Recovery Mode not initialized.' ) );
     96                if ( ! isset( $records[ $token ] ) ) {
     97                        return new WP_Error( 'token_not_found', __( 'Recovery Mode not initialized.' ) );
    7398                }
    7499
     100                $record = $records[ $token ];
     101
     102                $this->remove_key( $token );
     103
    75104                if ( ! is_array( $record ) || ! isset( $record['hashed_key'], $record['created_at'] ) ) {
    76105                        return new WP_Error( 'invalid_recovery_key_format', __( 'Invalid recovery key format.' ) );
    77106                }
     
    86115
    87116                return true;
    88117        }
     118
     119        /**
     120         * Removes expired recovery mode keys.
     121         *
     122         * @since 5.2.0
     123         *
     124         * @param int $ttl Time in seconds for the keys to be valid for.
     125         */
     126        public function clean_expired_keys( $ttl ) {
     127
     128                $records = $this->get_keys();
     129
     130                foreach ( $records as $key => $record ) {
     131                        if ( ! isset( $record['created_at'] ) || time() > $record['created_at'] + $ttl ) {
     132                                unset( $records[ $key ] );
     133                        }
     134                }
     135
     136                $this->update_keys( $records );
     137        }
     138
     139        /**
     140         * Removes a used recovery key.
     141         *
     142         * @since 5.2.0
     143         *
     144         * @param string $token The token used when generating a recovery mode key.
     145         */
     146        private function remove_key( $token ) {
     147
     148                $records = $this->get_keys();
     149
     150                if ( ! isset( $records[ $token ] ) ) {
     151                        return;
     152                }
     153
     154                unset( $records[ $token ] );
     155
     156                $this->update_keys( $records );
     157        }
     158
     159        /**
     160         * Gets the recovery key records.
     161         *
     162         * @since 5.2.0
     163         *
     164         * @return array Associative array of $token => $data pairs, where $data has keys 'hashed_key'
     165         *               and 'created_at'.
     166         */
     167        private function get_keys() {
     168                return (array) get_option( $this->option_name, array() );
     169        }
     170
     171        /**
     172         * Updates the recovery key records.
     173         *
     174         * @since 5.2.0
     175         *
     176         * @param array $keys Associative array of $token => $data pairs, where $data has keys 'hashed_key'
     177         *                    and 'created_at'.
     178         * @return bool True on success, false on failure.
     179         */
     180        private function update_keys( array $keys ) {
     181                return update_option( $this->option_name, $keys );
     182        }
    89183}
  • src/wp-includes/class-wp-recovery-mode-link-service.php

     
    3737         * @since 5.2.0
    3838         *
    3939         * @param WP_Recovery_Mode_Cookie_Service $cookie_service Service to handle setting the recovery mode cookie.
     40         * @param WP_Recovery_Mode_Key_Service    $key_service    Service to handle generating recovery mode keys.
    4041         */
    41         public function __construct( WP_Recovery_Mode_Cookie_Service $cookie_service ) {
     42        public function __construct( WP_Recovery_Mode_Cookie_Service $cookie_service, WP_Recovery_Mode_Key_Service $key_service ) {
    4243                $this->cookie_service = $cookie_service;
    43                 $this->key_service    = new WP_Recovery_Mode_Key_Service();
     44                $this->key_service    = $key_service;
    4445        }
    4546
    4647        /**
     
    5354         * @return string Generated URL.
    5455         */
    5556        public function generate_url() {
    56                 $key = $this->key_service->generate_and_store_recovery_mode_key();
     57                $token = $this->key_service->generate_recovery_mode_token();
     58                $key   = $this->key_service->generate_and_store_recovery_mode_key( $token );
    5759
    58                 return $this->get_recovery_mode_begin_url( $key );
     60                return $this->get_recovery_mode_begin_url( $token, $key );
    5961        }
    6062
    6163        /**
     
    7072                        return;
    7173                }
    7274
    73                 if ( ! isset( $_GET['action'], $_GET['rm_key'] ) || self::LOGIN_ACTION_ENTER !== $_GET['action'] ) {
     75                if ( ! isset( $_GET['action'], $_GET['rm_token'], $_GET['rm_key'] ) || self::LOGIN_ACTION_ENTER !== $_GET['action'] ) {
    7476                        return;
    7577                }
    7678
     
    7880                        require_once ABSPATH . WPINC . '/pluggable.php';
    7981                }
    8082
    81                 $validated = $this->key_service->validate_recovery_mode_key( $_GET['rm_key'], $ttl );
     83                $validated = $this->key_service->validate_recovery_mode_key( $_GET['rm_token'], $_GET['rm_key'], $ttl );
    8284
    8385                if ( is_wp_error( $validated ) ) {
    8486                        wp_die( $validated, '' );
     
    9698         *
    9799         * @since 5.2.0
    98100         *
    99          * @param string $key Recovery Mode key created by {@see generate_and_store_recovery_mode_key()}
     101         * @param string $token Recovery Mode token created by {@see generate_recovery_mode_token()}.
     102         * @param string $key   Recovery Mode key created by {@see generate_and_store_recovery_mode_key()}.
    100103         * @return string Recovery mode begin URL.
    101104         */
    102         private function get_recovery_mode_begin_url( $key ) {
     105        private function get_recovery_mode_begin_url( $token, $key ) {
    103106
    104107                $url = add_query_arg(
    105108                        array(
    106                                 'action' => self::LOGIN_ACTION_ENTER,
    107                                 'rm_key' => $key,
     109                                'action'   => self::LOGIN_ACTION_ENTER,
     110                                'rm_token' => $token,
     111                                'rm_key'   => $key,
    108112                        ),
    109113                        wp_login_url()
    110114                );
     
    114118                 *
    115119                 * @since 5.2.0
    116120                 *
    117                  * @param string $url
    118                  * @param string $key
     121                 * @param string $url   The generated recovery mode begin URL.
     122                 * @param string $token The token used to identify the key.
     123                 * @param string $key   The recovery mode key.
    119124                 */
    120                 return apply_filters( 'recovery_mode_begin_url', $url, $key );
     125                return apply_filters( 'recovery_mode_begin_url', $url, $token, $key );
    121126        }
    122127}
  • src/wp-includes/class-wp-recovery-mode.php

     
    1616        const EXIT_ACTION = 'exit_recovery_mode';
    1717
    1818        /**
    19          * Service to handle sending an email with a recovery mode link.
     19         * Service to handle cookies.
    2020         *
    2121         * @since 5.2.0
    22          * @var WP_Recovery_Mode_Email_Service
     22         * @var WP_Recovery_Mode_Cookie_Service
    2323         */
    24         private $email_service;
     24        private $cookie_service;
    2525
    2626        /**
     27         * Service to generate a recovery mode key.
     28         *
     29         * @since 5.2.0
     30         * @var WP_Recovery_Mode_Key_Service
     31         */
     32        private $key_service;
     33
     34        /**
    2735         * Service to generate and validate recovery mode links.
    2836         *
    2937         * @since 5.2.0
     
    3240        private $link_service;
    3341
    3442        /**
    35          * Service to handle cookies.
     43         * Service to handle sending an email with a recovery mode link.
    3644         *
    3745         * @since 5.2.0
    38          * @var WP_Recovery_Mode_Cookie_Service
     46         * @var WP_Recovery_Mode_Email_Service
    3947         */
    40         private $cookie_service;
     48        private $email_service;
    4149
    4250        /**
    4351         * Is recovery mode initialized.
     
    7078         */
    7179        public function __construct() {
    7280                $this->cookie_service = new WP_Recovery_Mode_Cookie_Service();
    73                 $this->link_service   = new WP_Recovery_Mode_Link_Service( $this->cookie_service );
     81                $this->key_service    = new WP_Recovery_Mode_Key_Service();
     82                $this->link_service   = new WP_Recovery_Mode_Link_Service( $this->cookie_service, $this->key_service );
    7483                $this->email_service  = new WP_Recovery_Mode_Email_Service( $this->link_service );
    7584        }
    7685
     
    8493
    8594                add_action( 'wp_logout', array( $this, 'exit_recovery_mode' ) );
    8695                add_action( 'login_form_' . self::EXIT_ACTION, array( $this, 'handle_exit_recovery_mode' ) );
     96                add_action( 'recovery_mode_clean_expired_keys', array( $this, 'clean_expired_keys' ) );
    8797
     98                if ( ! wp_next_scheduled( 'recovery_mode_clean_expired_keys' ) && ! wp_installing() ) {
     99                        wp_schedule_event( time(), 'daily', 'recovery_mode_clean_expired_keys' );
     100                }
     101
    88102                if ( defined( 'WP_RECOVERY_MODE_SESSION_ID' ) ) {
    89103                        $this->is_active  = true;
    90104                        $this->session_id = WP_RECOVERY_MODE_SESSION_ID;
     
    233247        }
    234248
    235249        /**
     250         * Cleans any recovery mode keys that have expired according to the link TTL.
     251         *
     252         * Executes on a daily cron schedule.
     253         *
     254         * @since 5.2.0
     255         */
     256        public function clean_expired_keys() {
     257                $this->key_service->clean_expired_keys( $this->get_link_ttl() );
     258        }
     259
     260        /**
    236261         * Handles checking for the recovery mode cookie and validating it.
    237262         *
    238263         * @since 5.2.0
  • 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( 'token_not_found', $error->get_error_code() );
    2728        }
    2829
    2930        /**
    3031         * @ticket 46130
    3132         */
     33        public function test_validate_recovery_mode_key_returns_wp_error_if_data_missing() {
     34                update_option( 'recovery_keys', 'gibberish' );
     35
     36                $service = new WP_Recovery_Mode_Key_Service();
     37                $error   = $service->validate_recovery_mode_key( '', 'abcd', HOUR_IN_SECONDS );
     38
     39                $this->assertWPError( $error );
     40                $this->assertEquals( 'token_not_found', $error->get_error_code() );
     41        }
     42
     43        /**
     44         * @ticket 46130
     45         */
     46        public function test_validate_recovery_mode_key_returns_wp_error_if_bad() {
     47                update_option( 'recovery_keys', array( 'token' => 'gibberish' ) );
     48
     49                $service = new WP_Recovery_Mode_Key_Service();
     50                $error   = $service->validate_recovery_mode_key( 'token', 'abcd', HOUR_IN_SECONDS );
     51
     52                $this->assertWPError( $error );
     53                $this->assertEquals( 'invalid_recovery_key_format', $error->get_error_code() );
     54        }
     55
     56
     57        /**
     58         * @ticket 46130
     59         */
    3260        public function test_validate_recovery_mode_key_returns_wp_error_if_stored_format_is_invalid() {
    33                 update_option( 'recovery_key', 'gibberish' );
    3461
     62                $token = wp_generate_password( 22, false );
     63                update_option( 'recovery_keys', array( $token => 'gibberish' ) );
     64
    3565                $service = new WP_Recovery_Mode_Key_Service();
    36                 $error   = $service->validate_recovery_mode_key( 'abcd', HOUR_IN_SECONDS );
     66                $error   = $service->validate_recovery_mode_key( $token, 'abcd', HOUR_IN_SECONDS );
    3767
    3868                $this->assertWPError( $error );
    3969                $this->assertEquals( 'invalid_recovery_key_format', $error->get_error_code() );
     
    4474         */
    4575        public function test_validate_recovery_mode_key_returns_wp_error_if_empty_key() {
    4676                $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 );
     77                $token   = $service->generate_recovery_mode_token();
     78                $service->generate_and_store_recovery_mode_key( $token );
     79                $error = $service->validate_recovery_mode_key( $token, '', HOUR_IN_SECONDS );
    4980
    5081                $this->assertWPError( $error );
    5182                $this->assertEquals( 'hash_mismatch', $error->get_error_code() );
     
    5687         */
    5788        public function test_validate_recovery_mode_key_returns_wp_error_if_hash_mismatch() {
    5889                $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 );
     90                $token   = $service->generate_recovery_mode_token();
     91                $service->generate_and_store_recovery_mode_key( $token );
     92                $error = $service->validate_recovery_mode_key( $token, 'abcd', HOUR_IN_SECONDS );
    6193
    6294                $this->assertWPError( $error );
    6395                $this->assertEquals( 'hash_mismatch', $error->get_error_code() );
     
    68100         */
    69101        public function test_validate_recovery_mode_key_returns_wp_error_if_expired() {
    70102                $service = new WP_Recovery_Mode_Key_Service();
    71                 $key     = $service->generate_and_store_recovery_mode_key();
     103                $token   = $service->generate_recovery_mode_token();
     104                $key     = $service->generate_and_store_recovery_mode_key( $token );
    72105
    73                 $record               = get_option( 'recovery_key' );
    74                 $record['created_at'] = time() - HOUR_IN_SECONDS - 30;
    75                 update_option( 'recovery_key', $record );
     106                $records                         = get_option( 'recovery_keys' );
     107                $records[ $token ]['created_at'] = time() - HOUR_IN_SECONDS - 30;
     108                update_option( 'recovery_keys', $records );
    76109
    77                 $error = $service->validate_recovery_mode_key( $key, HOUR_IN_SECONDS );
     110                $error = $service->validate_recovery_mode_key( $token, $key, HOUR_IN_SECONDS );
    78111
    79112                $this->assertWPError( $error );
    80113                $this->assertEquals( 'key_expired', $error->get_error_code() );
     
    85118         */
    86119        public function test_validate_recovery_mode_key_returns_true_for_valid_key() {
    87120                $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 ) );
     121                $token   = $service->generate_recovery_mode_token();
     122                $key     = $service->generate_and_store_recovery_mode_key( $token );
     123                $this->assertTrue( $service->validate_recovery_mode_key( $token, $key, HOUR_IN_SECONDS ) );
    90124        }
     125
     126        /**
     127         * @ticket 46595
     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( $token, $key, HOUR_IN_SECONDS ) );
     135
     136                // data should be remove by first call
     137                $error = $service->validate_recovery_mode_key( $token, $key, HOUR_IN_SECONDS );
     138
     139                $this->assertWPError( $error );
     140                $this->assertEquals( 'token_not_found', $error->get_error_code() );
     141        }
     142
     143        /**
     144         * @ticket 46595
     145         */
     146        public function test_validate_recovery_mode_key_returns_error_if_token_used_more_than_once_more_than_key_stored() {
     147                $service = new WP_Recovery_Mode_Key_Service();
     148
     149                // create an extra key
     150                $token = $service->generate_recovery_mode_token();
     151                $service->generate_and_store_recovery_mode_key( $token );
     152
     153                $token = $service->generate_recovery_mode_token();
     154                $key   = $service->generate_and_store_recovery_mode_key( $token );
     155
     156                $this->assertTrue( $service->validate_recovery_mode_key( $token, $key, HOUR_IN_SECONDS ) );
     157
     158                // data should be remove by first call
     159                $error = $service->validate_recovery_mode_key( $token, $key, HOUR_IN_SECONDS );
     160
     161                $this->assertWPError( $error );
     162                $this->assertEquals( 'token_not_found', $error->get_error_code() );
     163        }
     164
     165        /**
     166         * @ticket 46595
     167         */
     168        public function test_clean_expired_keys() {
     169                $service = new WP_Recovery_Mode_Key_Service();
     170                $token   = $service->generate_recovery_mode_token();
     171                $service->generate_and_store_recovery_mode_key( $token );
     172
     173                $records = get_option( 'recovery_keys' );
     174
     175                $records[ $token ]['created_at'] = time() - HOUR_IN_SECONDS - 30;
     176
     177                update_option( 'recovery_keys', $records );
     178
     179                $service->clean_expired_keys( HOUR_IN_SECONDS );
     180
     181                $this->assertEmpty( get_option( 'recovery_keys' ) );
     182        }
    91183}