Fix the findings of a full second security audit
Some checks are pending
CI / PHP lint (7.4) (pull_request) Waiting to run
CI / PHP lint (8.0) (pull_request) Waiting to run
CI / PHP lint (8.1) (pull_request) Waiting to run
CI / PHP lint (8.2) (pull_request) Waiting to run
CI / PHP lint (8.3) (pull_request) Waiting to run
CI / PHP lint (8.4) (pull_request) Waiting to run
CI / WordPress Coding Standards (pull_request) Waiting to run
CI / WordPress.org Plugin Check (pull_request) Waiting to run
Some checks are pending
CI / PHP lint (7.4) (pull_request) Waiting to run
CI / PHP lint (8.0) (pull_request) Waiting to run
CI / PHP lint (8.1) (pull_request) Waiting to run
CI / PHP lint (8.2) (pull_request) Waiting to run
CI / PHP lint (8.3) (pull_request) Waiting to run
CI / PHP lint (8.4) (pull_request) Waiting to run
CI / WordPress Coding Standards (pull_request) Waiting to run
CI / WordPress.org Plugin Check (pull_request) Waiting to run
Four-part audit (OIDC/JWT/crypto, user sync, admin UI, login bypasses) with dynamic PoCs against a real WordPress install; every fix is covered by a regression test. Report: docs/security-audit.md, section 6. Critical/High - Multisite: settings, AJAX actions and certificate download require manage_network_options (site admins could sign in as super admin). - Privileged accounts are only linked (sync and first sign-in) via a matching UPN of a member account, never via the settable mail attribute; the sync never changes their e-mail address; e-mail change notifications stay on. - Button-only mode exempts by credential (application passwords, WP-CLI) instead of request context, closing bypasses through xmlrpc.php and REST login handlers; API requests never receive login cookies. - Multi-tenant mode refuses guest/external identities. Medium/Low - Same message for right and wrong passwords; button-only no longer switches off when the connection breaks; server-side fallback cookie expiry; correct fallback key beats IP lockouts; right-most proxy hop; higher start limit; one object ID per account. - Deactivation sets a random password, revokes application passwords and removes the role (restored on reactivation); disabled people are deactivated even when their mail vanished; duplicate bindings handled. - Sync: abort on empty directory answer, no deprovisioning right after a tenant change, atomic run lock, strict photo path validation. - Certificates: key bundles refused, clean re-exported certificate. - Array-safe sanitising, encoded redirect_to, per-action nonces, escaped role lists, no Graph sleeps during sign-in, warnings for public groups, multi-tenant group rules and missing salts, uninstall clears the token. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
791f43a80b
commit
850f0dcd54
18 changed files with 1908 additions and 1270 deletions
|
|
@ -402,7 +402,10 @@ class M365_Login_Settings {
|
|||
if ( defined( 'M365_LOGIN_DISABLE_BUTTON_ONLY' ) && M365_LOGIN_DISABLE_BUTTON_ONLY ) {
|
||||
return false;
|
||||
}
|
||||
return $this->is_configured() && (bool) $this->get( 'button_only' ) && '' !== $this->fallback_key();
|
||||
// Deliberately not tied to is_configured(): an expired certificate or rotated salts must not
|
||||
// silently re-enable password sign-in. The fallback link and the constant stay available.
|
||||
return '' !== (string) $this->get( 'tenant_id' ) && '' !== (string) $this->get( 'client_id' )
|
||||
&& (bool) $this->get( 'button_only' ) && '' !== $this->fallback_key();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -497,7 +500,7 @@ class M365_Login_Settings {
|
|||
$out['client_id'] = strtolower( $client_id );
|
||||
|
||||
// Client secret: only replaced when a new value was entered.
|
||||
$secret_input = isset( $input['client_secret'] ) ? (string) wp_unslash( $input['client_secret'] ) : '';
|
||||
$secret_input = self::scalar( $input, 'client_secret' );
|
||||
$secret_input = trim( $secret_input );
|
||||
if ( ! empty( $input['client_secret_clear'] ) ) {
|
||||
$out['client_secret'] = '';
|
||||
|
|
@ -520,8 +523,8 @@ class M365_Login_Settings {
|
|||
// Certificate: keep the stored pair unless a new one is pasted or removal is requested.
|
||||
$out['cert_private_key'] = $current['cert_private_key'];
|
||||
$out['cert_certificate'] = $current['cert_certificate'];
|
||||
$pasted_key = isset( $input['cert_key_pem'] ) ? trim( (string) wp_unslash( $input['cert_key_pem'] ) ) : '';
|
||||
$pasted_cert = isset( $input['cert_cert_pem'] ) ? trim( (string) wp_unslash( $input['cert_cert_pem'] ) ) : '';
|
||||
$pasted_key = trim( self::scalar( $input, 'cert_key_pem' ) );
|
||||
$pasted_cert = trim( self::scalar( $input, 'cert_cert_pem' ) );
|
||||
if ( ! empty( $input['cert_remove'] ) ) {
|
||||
$out['cert_private_key'] = '';
|
||||
$out['cert_certificate'] = '';
|
||||
|
|
@ -573,7 +576,7 @@ class M365_Login_Settings {
|
|||
$out['fallback_key'] = $key;
|
||||
|
||||
// Custom login page (must be on this site).
|
||||
$custom = isset( $input['custom_login_url'] ) ? esc_url_raw( trim( wp_unslash( $input['custom_login_url'] ) ) ) : '';
|
||||
$custom = esc_url_raw( trim( self::scalar( $input, 'custom_login_url' ) ) );
|
||||
if ( '' !== $custom ) {
|
||||
if ( 0 === strpos( $custom, '/' ) ) {
|
||||
$custom = home_url( $custom );
|
||||
|
|
@ -590,13 +593,13 @@ class M365_Login_Settings {
|
|||
$text = isset( $input['button_text'] ) ? sanitize_text_field( wp_unslash( $input['button_text'] ) ) : '';
|
||||
$out['button_text'] = '' === trim( $text ) ? $defaults['button_text'] : mb_substr( $text, 0, 80 );
|
||||
|
||||
$icon = isset( $input['button_icon'] ) ? esc_url_raw( trim( wp_unslash( $input['button_icon'] ) ) ) : '';
|
||||
$icon = esc_url_raw( trim( self::scalar( $input, 'button_icon' ) ) );
|
||||
$out['button_icon'] = self::is_safe_image_url( $icon ) ? $icon : '';
|
||||
|
||||
$out['button_show_icon'] = empty( $input['button_show_icon'] ) ? 0 : 1;
|
||||
|
||||
foreach ( array( 'button_bg', 'button_bg_hover', 'button_color', 'button_border' ) as $color_key ) {
|
||||
$color = isset( $input[ $color_key ] ) ? sanitize_hex_color( trim( wp_unslash( $input[ $color_key ] ) ) ) : '';
|
||||
$color = sanitize_hex_color( trim( self::scalar( $input, $color_key ) ) );
|
||||
$out[ $color_key ] = $color ? $color : $defaults[ $color_key ];
|
||||
}
|
||||
|
||||
|
|
@ -692,6 +695,17 @@ class M365_Login_Settings {
|
|||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unslashed string value of a posted field ('' for missing or non-scalar values such as arrays).
|
||||
*
|
||||
* @param array $input Raw input.
|
||||
* @param string $key Field.
|
||||
* @return string
|
||||
*/
|
||||
private static function scalar( $input, $key ) {
|
||||
return isset( $input[ $key ] ) && is_scalar( $input[ $key ] ) ? (string) wp_unslash( $input[ $key ] ) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitises a GUID => name list posted by a group picker.
|
||||
*
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue