Ticket #46595: 46595.8.patch
File 46595.8.patch, 15.3 KB (added by , 6 years ago) |
---|
-
src/wp-includes/class-wp-recovery-mode-key-service.php
8 8 9 9 /** 10 10 * 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 11 12 * 12 13 * @since 5.2.0 13 14 */ … … 14 15 final class WP_Recovery_Mode_Key_Service { 15 16 16 17 /** 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 /** 17 36 * Creates a recovery mode key. 18 37 * 19 38 * @since 5.2.0 … … 20 39 * 21 40 * @global PasswordHash $wp_hasher 22 41 * 23 * @return string Recovery mode key.42 * @return string $key Recovery mode key. 24 43 */ 25 public function generate_and_store_recovery_mode_key( ) {44 public function generate_and_store_recovery_mode_key( $token ) { 26 45 27 46 global $wp_hasher; 28 47 29 48 $key = wp_generate_password( 22, false ); 30 49 31 /**32 * Fires when a recovery mode key is generated for a user.33 *34 * @since 5.2.035 *36 * @param string $key The recovery mode key.37 */38 do_action( 'generate_recovery_mode_key', $key );39 40 50 if ( empty( $wp_hasher ) ) { 41 51 require_once ABSPATH . WPINC . '/class-phpass.php'; 42 52 $wp_hasher = new PasswordHash( 8, true ); … … 44 54 45 55 $hashed = $wp_hasher->HashPassword( $key ); 46 56 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(), 53 62 ); 54 63 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 55 76 return $key; 56 77 } 57 78 58 79 /** 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 /** 59 92 * 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 60 95 * 61 96 * @since 5.2.0 62 97 * 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. 66 102 */ 67 public function validate_recovery_mode_key( $key, $t tl ) {103 public function validate_recovery_mode_key( $key, $token, $ttl ) { 68 104 69 $record = get_option( 'recovery_key');105 $records = get_option( $this->option_id, array() ); 70 106 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.' ) ); 73 109 } 74 110 111 $record = $records[ $token ]; 112 113 $this->clean_key($token ); 114 $this->clean_expired_keys( $ttl ); 115 75 116 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.' ) ); 77 118 } 78 119 79 120 if ( ! wp_check_password( $key, $record['hashed_key'] ) ) { … … 86 127 87 128 return true; 88 129 } 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 } 89 169 } -
src/wp-includes/class-wp-recovery-mode-link-service.php
53 53 * @return string Generated URL. 54 54 */ 55 55 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 ); 57 58 58 return $this->get_recovery_mode_begin_url( $key );59 return $this->get_recovery_mode_begin_url( $key, $token ); 59 60 } 60 61 61 62 /** … … 70 71 return; 71 72 } 72 73 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'] ) { 74 75 return; 75 76 } 76 77 … … 78 79 require_once ABSPATH . WPINC . '/pluggable.php'; 79 80 } 80 81 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 ); 82 83 83 84 if ( is_wp_error( $validated ) ) { 84 85 wp_die( $validated, '' ); … … 97 98 * @since 5.2.0 98 99 * 99 100 * @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 * 100 104 * @return string Recovery mode begin URL. 101 105 */ 102 private function get_recovery_mode_begin_url( $key ) {106 private function get_recovery_mode_begin_url( $key, $token ) { 103 107 104 108 $url = add_query_arg( 105 109 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, 108 113 ), 109 114 wp_login_url() 110 115 ); … … 116 121 * 117 122 * @param string $url 118 123 * @param string $key 124 * @param string $token 119 125 */ 120 return apply_filters( 'recovery_mode_begin_url', $url, $key );126 return apply_filters( 'recovery_mode_begin_url', $url, $key, $token ); 121 127 } 122 128 } -
src/wp-includes/class-wp-recovery-mode.php
72 72 $this->cookie_service = new WP_Recovery_Mode_Cookie_Service(); 73 73 $this->link_service = new WP_Recovery_Mode_Link_Service( $this->cookie_service ); 74 74 $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 } 75 80 } 76 81 77 82 /** -
tests/phpunit/tests/error-protection/recovery-mode-key-service.php
10 10 */ 11 11 public function test_generate_and_store_recovery_mode_key_returns_recovery_key() { 12 12 $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 ); 14 15 15 16 $this->assertNotWPError( $key ); 16 17 } … … 20 21 */ 21 22 public function test_validate_recovery_mode_key_returns_wp_error_if_no_key_set() { 22 23 $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 ); 24 25 25 26 $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() ); 27 28 } 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' ); 28 34 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 29 42 /** 30 43 * @ticket 46130 31 44 */ 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 */ 32 59 public function test_validate_recovery_mode_key_returns_wp_error_if_stored_format_is_invalid() { 33 update_option( 'recovery_key', 'gibberish' );34 60 61 $token = wp_generate_password( 22, false ); 62 update_option( 'recovery_keys', array( $token => 'gibberish' ) ); 63 35 64 $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 ); 37 66 38 67 $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() ); 40 69 } 41 70 42 71 /** … … 44 73 */ 45 74 public function test_validate_recovery_mode_key_returns_wp_error_if_empty_key() { 46 75 $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 ); 49 79 50 80 $this->assertWPError( $error ); 51 81 $this->assertEquals( 'hash_mismatch', $error->get_error_code() ); … … 56 86 */ 57 87 public function test_validate_recovery_mode_key_returns_wp_error_if_hash_mismatch() { 58 88 $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 ); 61 92 62 93 $this->assertWPError( $error ); 63 94 $this->assertEquals( 'hash_mismatch', $error->get_error_code() ); … … 68 99 */ 69 100 public function test_validate_recovery_mode_key_returns_wp_error_if_expired() { 70 101 $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 ); 72 104 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 ); 76 108 77 $error = $service->validate_recovery_mode_key( $key, HOUR_IN_SECONDS );109 $error = $service->validate_recovery_mode_key( $key, $token, HOUR_IN_SECONDS ); 78 110 79 111 $this->assertWPError( $error ); 80 112 $this->assertEquals( 'key_expired', $error->get_error_code() ); … … 85 117 */ 86 118 public function test_validate_recovery_mode_key_returns_true_for_valid_key() { 87 119 $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 ) ); 90 123 } 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 91 184 }