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:
parent
b35f6a867b
commit
9b893e42bc
9 changed files with 582 additions and 104 deletions
|
|
@ -20,6 +20,7 @@ class M365_Login_Auth {
|
|||
const STATE_TTL = 600; // 10 minutes.
|
||||
const META_OID = '_m365_login_oid';
|
||||
const META_LAST_LOGIN = '_m365_login_last_login';
|
||||
const META_TID = '_m365_login_tid'; // Tenant the object ID belongs to.
|
||||
const JWKS_CACHE_TTL = 12 * HOUR_IN_SECONDS;
|
||||
const HTTP_TIMEOUT = 15;
|
||||
|
||||
|
|
@ -220,14 +221,16 @@ class M365_Login_Auth {
|
|||
* Core can set cookies there, e.g. when an application password is used to change the account
|
||||
* password via REST, or when another plugin's login handler runs inside xmlrpc.php.
|
||||
*
|
||||
* @param bool $send Whether to send the cookies.
|
||||
* @param int $expire Expiry (unused).
|
||||
* @param int $expiration Expiration (unused).
|
||||
* @param int $user_id User ID (0 when cookies are cleared).
|
||||
* @param bool $send Whether to send the cookies.
|
||||
* @param int $expire Expiry (unused).
|
||||
* @param int $expiration Expiration (unused).
|
||||
* @param int|null $user_id User ID (0 when cookies are cleared; not passed before WordPress 6.2).
|
||||
* @return bool
|
||||
*/
|
||||
public function block_api_auth_cookies( $send, $expire = 0, $expiration = 0, $user_id = 0 ) {
|
||||
if ( ! $send || ! $user_id || ! $this->settings->button_only() || $this->fallback_active() ) {
|
||||
public function block_api_auth_cookies( $send, $expire = 0, $expiration = 0, $user_id = null ) {
|
||||
// Before WordPress 6.2 the filter gets no user ID: block in API contexts anyway (also blocks
|
||||
// clearing cookies there, which is harmless).
|
||||
if ( ! $send || 0 === $user_id || ! $this->settings->button_only() || $this->fallback_active() ) {
|
||||
return $send;
|
||||
}
|
||||
if ( ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) {
|
||||
|
|
@ -537,9 +540,11 @@ class M365_Login_Auth {
|
|||
}
|
||||
}
|
||||
|
||||
// Privileged accounts that are not bound yet: only the user principal name of a member
|
||||
// account may claim them (its domain is verified in the tenant, the e-mail attribute is not).
|
||||
if ( '' === $stored && M365_Login_Sync::is_privileged( $user ) && ! $this->may_claim_privileged( $claims, $user ) ) {
|
||||
// Privileged accounts are only safe through a verified binding (bind_oid on and the stored
|
||||
// object ID matches – checked above). Otherwise only the user principal name of a member
|
||||
// account may claim them: its domain is verified in the tenant, the e-mail attribute is not.
|
||||
$bound = $this->settings->get( 'bind_oid' ) && '' !== $stored;
|
||||
if ( ! $bound && M365_Login_Sync::is_privileged( $user ) && ! $this->may_claim_privileged( $claims, $user ) ) {
|
||||
$this->log( sprintf( 'Refused first sign-in of privileged user #%d without a matching user principal name.', $user->ID ) );
|
||||
$this->fail( 'privileged_unlinked' );
|
||||
}
|
||||
|
|
@ -561,6 +566,9 @@ class M365_Login_Auth {
|
|||
$this->fail( 'oid_mismatch' );
|
||||
}
|
||||
update_user_meta( $user->ID, self::META_OID, $oid );
|
||||
if ( isset( $claims['tid'] ) && M365_Login_Settings::is_guid( (string) $claims['tid'] ) ) {
|
||||
update_user_meta( $user->ID, self::META_TID, strtolower( (string) $claims['tid'] ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue