class-wp-recovery-mode-cookie-service.php 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. <?php
  2. /**
  3. * Error Protection API: WP_Recovery_Mode_Cookie_Service class
  4. *
  5. * @package WordPress
  6. * @since 5.2.0
  7. */
  8. /**
  9. * Core class used to set, validate, and clear cookies that identify a Recovery Mode session.
  10. *
  11. * @since 5.2.0
  12. */
  13. #[AllowDynamicProperties]
  14. final class WP_Recovery_Mode_Cookie_Service {
  15. /**
  16. * Checks whether the recovery mode cookie is set.
  17. *
  18. * @since 5.2.0
  19. *
  20. * @return bool True if the cookie is set, false otherwise.
  21. */
  22. public function is_cookie_set() {
  23. return ! empty( $_COOKIE[ RECOVERY_MODE_COOKIE ] );
  24. }
  25. /**
  26. * Sets the recovery mode cookie.
  27. *
  28. * This must be immediately followed by exiting the request.
  29. *
  30. * @since 5.2.0
  31. */
  32. public function set_cookie() {
  33. $value = $this->generate_cookie();
  34. /**
  35. * Filters the length of time a Recovery Mode cookie is valid for.
  36. *
  37. * @since 5.2.0
  38. *
  39. * @param int $length Length in seconds.
  40. */
  41. $length = apply_filters( 'recovery_mode_cookie_length', WEEK_IN_SECONDS );
  42. $expire = time() + $length;
  43. setcookie( RECOVERY_MODE_COOKIE, $value, $expire, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true );
  44. if ( COOKIEPATH !== SITECOOKIEPATH ) {
  45. setcookie( RECOVERY_MODE_COOKIE, $value, $expire, SITECOOKIEPATH, COOKIE_DOMAIN, is_ssl(), true );
  46. }
  47. }
  48. /**
  49. * Clears the recovery mode cookie.
  50. *
  51. * @since 5.2.0
  52. */
  53. public function clear_cookie() {
  54. setcookie( RECOVERY_MODE_COOKIE, ' ', time() - YEAR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN );
  55. setcookie( RECOVERY_MODE_COOKIE, ' ', time() - YEAR_IN_SECONDS, SITECOOKIEPATH, COOKIE_DOMAIN );
  56. }
  57. /**
  58. * Validates the recovery mode cookie.
  59. *
  60. * @since 5.2.0
  61. *
  62. * @param string $cookie Optionally specify the cookie string.
  63. * If omitted, it will be retrieved from the super global.
  64. * @return true|WP_Error True on success, error object on failure.
  65. */
  66. public function validate_cookie( $cookie = '' ) {
  67. if ( ! $cookie ) {
  68. if ( empty( $_COOKIE[ RECOVERY_MODE_COOKIE ] ) ) {
  69. return new WP_Error( 'no_cookie', __( 'No cookie present.' ) );
  70. }
  71. $cookie = $_COOKIE[ RECOVERY_MODE_COOKIE ];
  72. }
  73. $parts = $this->parse_cookie( $cookie );
  74. if ( is_wp_error( $parts ) ) {
  75. return $parts;
  76. }
  77. list( , $created_at, $random, $signature ) = $parts;
  78. if ( ! ctype_digit( $created_at ) ) {
  79. return new WP_Error( 'invalid_created_at', __( 'Invalid cookie format.' ) );
  80. }
  81. /** This filter is documented in wp-includes/class-wp-recovery-mode-cookie-service.php */
  82. $length = apply_filters( 'recovery_mode_cookie_length', WEEK_IN_SECONDS );
  83. if ( time() > $created_at + $length ) {
  84. return new WP_Error( 'expired', __( 'Cookie expired.' ) );
  85. }
  86. $to_sign = sprintf( 'recovery_mode|%s|%s', $created_at, $random );
  87. $hashed = $this->recovery_mode_hash( $to_sign );
  88. if ( ! hash_equals( $signature, $hashed ) ) {
  89. return new WP_Error( 'signature_mismatch', __( 'Invalid cookie.' ) );
  90. }
  91. return true;
  92. }
  93. /**
  94. * Gets the session identifier from the cookie.
  95. *
  96. * The cookie should be validated before calling this API.
  97. *
  98. * @since 5.2.0
  99. *
  100. * @param string $cookie Optionally specify the cookie string.
  101. * If omitted, it will be retrieved from the super global.
  102. * @return string|WP_Error Session ID on success, or error object on failure.
  103. */
  104. public function get_session_id_from_cookie( $cookie = '' ) {
  105. if ( ! $cookie ) {
  106. if ( empty( $_COOKIE[ RECOVERY_MODE_COOKIE ] ) ) {
  107. return new WP_Error( 'no_cookie', __( 'No cookie present.' ) );
  108. }
  109. $cookie = $_COOKIE[ RECOVERY_MODE_COOKIE ];
  110. }
  111. $parts = $this->parse_cookie( $cookie );
  112. if ( is_wp_error( $parts ) ) {
  113. return $parts;
  114. }
  115. list( , , $random ) = $parts;
  116. return sha1( $random );
  117. }
  118. /**
  119. * Parses the cookie into its four parts.
  120. *
  121. * @since 5.2.0
  122. *
  123. * @param string $cookie Cookie content.
  124. * @return array|WP_Error Cookie parts array, or error object on failure.
  125. */
  126. private function parse_cookie( $cookie ) {
  127. $cookie = base64_decode( $cookie );
  128. $parts = explode( '|', $cookie );
  129. if ( 4 !== count( $parts ) ) {
  130. return new WP_Error( 'invalid_format', __( 'Invalid cookie format.' ) );
  131. }
  132. return $parts;
  133. }
  134. /**
  135. * Generates the recovery mode cookie value.
  136. *
  137. * The cookie is a base64 encoded string with the following format:
  138. *
  139. * recovery_mode|iat|rand|signature
  140. *
  141. * Where "recovery_mode" is a constant string,
  142. * iat is the time the cookie was generated at,
  143. * rand is a randomly generated password that is also used as a session identifier
  144. * and signature is an hmac of the preceding 3 parts.
  145. *
  146. * @since 5.2.0
  147. *
  148. * @return string Generated cookie content.
  149. */
  150. private function generate_cookie() {
  151. $to_sign = sprintf( 'recovery_mode|%s|%s', time(), wp_generate_password( 20, false ) );
  152. $signed = $this->recovery_mode_hash( $to_sign );
  153. return base64_encode( sprintf( '%s|%s', $to_sign, $signed ) );
  154. }
  155. /**
  156. * Gets a form of `wp_hash()` specific to Recovery Mode.
  157. *
  158. * We cannot use `wp_hash()` because it is defined in `pluggable.php` which is not loaded until after plugins are loaded,
  159. * which is too late to verify the recovery mode cookie.
  160. *
  161. * This tries to use the `AUTH` salts first, but if they aren't valid specific salts will be generated and stored.
  162. *
  163. * @since 5.2.0
  164. *
  165. * @param string $data Data to hash.
  166. * @return string|false The hashed $data, or false on failure.
  167. */
  168. private function recovery_mode_hash( $data ) {
  169. $default_keys = array_unique(
  170. array(
  171. 'put your unique phrase here',
  172. /*
  173. * translators: This string should only be translated if wp-config-sample.php is localized.
  174. * You can check the localized release package or
  175. * https://i18n.svn.wordpress.org/<locale code>/branches/<wp version>/dist/wp-config-sample.php
  176. */
  177. __( 'put your unique phrase here' ),
  178. )
  179. );
  180. if ( ! defined( 'AUTH_KEY' ) || in_array( AUTH_KEY, $default_keys, true ) ) {
  181. $auth_key = get_site_option( 'recovery_mode_auth_key' );
  182. if ( ! $auth_key ) {
  183. if ( ! function_exists( 'wp_generate_password' ) ) {
  184. require_once ABSPATH . WPINC . '/pluggable.php';
  185. }
  186. $auth_key = wp_generate_password( 64, true, true );
  187. update_site_option( 'recovery_mode_auth_key', $auth_key );
  188. }
  189. } else {
  190. $auth_key = AUTH_KEY;
  191. }
  192. if ( ! defined( 'AUTH_SALT' ) || in_array( AUTH_SALT, $default_keys, true ) || AUTH_SALT === $auth_key ) {
  193. $auth_salt = get_site_option( 'recovery_mode_auth_salt' );
  194. if ( ! $auth_salt ) {
  195. if ( ! function_exists( 'wp_generate_password' ) ) {
  196. require_once ABSPATH . WPINC . '/pluggable.php';
  197. }
  198. $auth_salt = wp_generate_password( 64, true, true );
  199. update_site_option( 'recovery_mode_auth_salt', $auth_salt );
  200. }
  201. } else {
  202. $auth_salt = AUTH_SALT;
  203. }
  204. $secret = $auth_key . $auth_salt;
  205. return hash_hmac( 'sha1', $data, $secret );
  206. }
  207. }