Fix the findings of the third security audit

- Privileged accounts: the UPN rule also applies when bind_oid is off or
  the account is not bound; privileges are checked on every site of a
  multisite user, include code/HTML capabilities (unfiltered_html,
  plugins, themes, users) and the remembered roles of deactivated
  accounts.
- send_auth_cookies protection also works on WordPress 6.0/6.1.
- Run lock via INSERT IGNORE (atomic), refreshed during long runs; a
  shutdown handler reports fatal errors and frees the lock.
- Deprovisioning only for accounts linked in the current tenant (tenant
  recorded per account; legacy links not found are left alone).
- Safety stop based on the accounts linked before the run; new safety
  stop for removals of administrative roles.
- Disable is idempotent; row-action nonces are bound to the state.
- Profile photos are re-encoded to 240 px (drops EXIF and appended
  data), size-limited while downloading, removed on deactivation;
  index.php guard in the photo folder.
- Privacy exporter and eraser for the copied data.
- One-time migration hardens accounts deactivated by 1.0 and cleans a
  stored certificate bundle; the .cer download is always re-exported.
- Password fields hidden in button-only mode even when the connection
  is broken; settings written non-autoloaded; robust user ID queries.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Friederich Loheide 2026-09-24 03:52:13 +00:00
parent b35f6a867b
commit 9b893e42bc
9 changed files with 582 additions and 104 deletions

View file

@ -167,14 +167,7 @@ class M365_Login_Button {
* @return bool
*/
private function should_render() {
if ( ! $this->settings->is_configured() ) {
return false;
}
// phpcs:disable WordPress.Security.NonceVerification.Recommended -- read-only routing check.
$action = isset( $_REQUEST['action'] ) ? sanitize_key( wp_unslash( $_REQUEST['action'] ) ) : 'login';
$interim = ! empty( $_REQUEST['interim-login'] );
// phpcs:enable WordPress.Security.NonceVerification.Recommended
if ( $interim || ! in_array( $action, array( '', 'login' ), true ) ) {
if ( ! $this->settings->is_configured() || ! $this->is_login_action() ) {
return false;
}
/**
@ -185,13 +178,31 @@ class M365_Login_Button {
return (bool) apply_filters( 'm365_login_show_button', true );
}
/**
* Whether the current wp-login.php request shows the sign-in form (not interim login or another action).
*
* @return bool
*/
private function is_login_action() {
// phpcs:disable WordPress.Security.NonceVerification.Recommended -- read-only routing check.
$action = isset( $_REQUEST['action'] ) ? sanitize_key( wp_unslash( $_REQUEST['action'] ) ) : 'login';
$interim = ! empty( $_REQUEST['interim-login'] );
// phpcs:enable WordPress.Security.NonceVerification.Recommended
return ! $interim && in_array( $action, array( '', 'login' ), true );
}
/**
* Whether the password form is hidden for this request.
*
* Also while the connection is broken: password sign-in is refused anyway, the fields would only mislead.
*
* @return bool
*/
private function form_hidden() {
return $this->should_render() && $this->settings->button_only() && ! M365_Login::instance()->auth->fallback_active();
if ( ! $this->settings->button_only() || M365_Login::instance()->auth->fallback_active() || ! $this->is_login_action() ) {
return false;
}
return $this->should_render() || ! $this->settings->is_configured();
}
/**