[wp-trac] [WordPress Trac] #46130: Only pause extensions for users with recovery access

WordPress Trac noreply at wordpress.org
Tue Jan 29 22:50:20 UTC 2019


#46130: Only pause extensions for users with recovery access
------------------------------------------------+--------------------------
 Reporter:  flixos90                            |       Owner:
                                                |  TimothyBlynJacobs
     Type:  defect (bug)                        |      Status:  assigned
 Priority:  highest omg bbq                     |   Milestone:  5.1
Component:  Bootstrap/Load                      |     Version:  trunk
 Severity:  normal                              |  Resolution:
 Keywords:  needs-patch servehappy 2nd-opinion  |     Focuses:
------------------------------------------------+--------------------------

Comment (by TimothyBlynJacobs):

 I've uploaded a patch which requires users to enter into Recovery Mode via
 a link.

 The Recovery Mode is broken down into a number of parts.

 1) Requesting and Sending a link to enable recovery mode.
 2) Validating the recovery link.
 3) Handling the recovery cookie.

 ** Part 1**
 When a fatal error occurs, we add a link to `wp_die` generated by
 `get_recovery_mode_request_url()`.

 When the user visits that URL, `handle_recovery_mode_actions()` sends an
 email to the `admin_email` via `maybe_send_recovery_mode_email()`. This
 function rate limits the email to only be sent at most once an hour. We
 then immediately `wp_die()` with a message indicating whether the email
 was sent or not.

 The email generates a "recovery key" with
 `generate_and_store_recovery_mode_key()`. This generates a random string
 with `wp_generate_password()`. ( The length of the string is 20 to match
 the behavior in `get_password_reset_key()` but could be increased ). The
 string is then hashed by `PasswordHash` and that hash saved to the
 `recovery_key` site option along with the creation time.

 This un-hashed key is added as a query arg to `wp_login_url()` along with
 an `action` set to `begin_recovery_mode()`.

 ** Part 2 **
 When the user clicks the link, `handle_recovery_mode_actions()` verifies
 that the key is valid and not expired. The persistent recovery cookie is
 then set. We then `wp_die()` with a JavaScript based redirect to the login
 page.

 This is done so that the cookie can actually be persisted on the client's
 machine. In testing, just setting a cookie and redirecting didn't work.
 Additionally, from the brief research I had time to do, it looks like
 different cache configurations might make persisting a cookie and doing a
 redirect at once not possible.

 The user is redirect to the login page with an `action` set to
 `begun_recovery_mode`. This is so we can then print a message indicating
 that recovery mode was enabled successfully on the login page.

 ** Part 3 **
 The cookie value is generated by `generate_recovery_mode_cookie()`. The
 majority of the security related functions are defined in `pluggable.php`.
 When we load `pluggable.php` in other calls, we can immediately `die()` or
 `wp_redirect`. Since we have to validate this cookie on every page load,
 those functions aren't an option.

 After discussing with @aaroncampbell, it looked like making a version of
 `wp_salt` that has its own fallbacks for invalid salts was a variable
 option.

 A separate function, `recovery_mode_hash()` is introduced that performs a
 `hash_hmac` specific to recovery mode. This tries to use the `AUTH_KEY`
 and `AUTH_SALT` values, but if they aren't available, it will load
 `pluggable.php` and then generate random values and store them to site
 options, similarly to `wp_salt()`.

 The cookie value is an hmac with the data set to a string of
 `recovery_mode|%1$s|%2$s`. 1. is the current time, and 2. is a randomly
 generated value by `wp_generate_password()`. The signature generated by
 `hash_hmac` is then appended to the string.

 Validating the cookie is done by recalculating the expected hash, and
 checking if the hashes are equal. Additionally, the code checks that the
 cookie has not expired. By default the cookie value is valid for 7 days.
 The cookie itself expires at the end of the session. This should be
 discussed.

 The actual cookie implementation beyond using a version of `wp_salt` ''was
 not'' reviewed by @aaroncampbell. This should be reviewed and alternatives
 considered.

 Some possible alternatives:

 a) Use the `PasswordHash` library directly. The hash would be stored in a
 site option, with the creation time. This would require the secret to be
 stored in plain text in the cookie. This might not be terrible in this
 scenario since it's not a user's password, but a randomly generated value.
 b) Just use a shared plaintext secret that is stored in the DB with the
 creation time there.
 c) Same as b) but use `recovery_mode_hash` to hash the shared secret on
 either side so that the Recovery Mode sessions would expire when the
 `AUTH*` salts are changed.

 Moving on, if the cookie is not validated, it is cleared and we `wp_die`
 with the reason why and a prompt to request a new link.

 If the cookie is valid, set a constant `WP_RECOVERY_MODE` to `true`. The
 `wp_is_recovery_mode()` function checks for this constant, and allows
 filtering its output.

 The cookie is cleared when the user logs out by `wp_clear_auth_cookie()`.

 ** Additional Changes **
 - Plugins/Themes are not paused unless `wp_is_recovery_mode()`
 - The redirection mechanism to discover multiple plugins at fault in one
 go now only happens when `wp_is_recovery_mode()` otherwise no progress can
 be made since the plugins aren't being paused one by one.
 - All errors are now stored, regardless of if they occurred in a protected
 endpoint. Alongside the error we record if the error happened in a
 protected endpoint and adjust the language in the Plugins List Table.

 ----

 Additionally, after discussion with @flixos90, we decided to drop Network
 Activated plugins from pausing. There are a number of things that
 WordPress loads between MU/Network Activated plugins being loaded and
 regular plugins being loaded. Most importantly to us, the cookie
 constants. Also from @flixos90, pausing network plugins open their whole
 own permission/scope can of worms and is multisite is generally run by
 more technical users.

 When handling the error, the email is sent automatically. I'm not sure if
 this should stay or not, but I included it to demonstrate.

 `handle_recovery_mode_actions()` is called immediately before loading
 regular plugins.

 Initially, I tried to match this to a user and store the keys in user
 meta. After developing it further, it didn't appear workable because this
 code is executed before we can work with the current user and often
 without a user available ( such as when trying to login ).

 ----

 After writing this, I think we can drop the
 `recovery_mode_email_last_sent` option and just use the creation time from
 `generate_and_store_recovery_mode_key()`.

-- 
Ticket URL: <https://core.trac.wordpress.org/ticket/46130#comment:6>
WordPress Trac <https://core.trac.wordpress.org/>
WordPress publishing platform


More information about the wp-trac mailing list