From b35f6a867b202d666eaf4a8bfdef4af87eb01a73 Mon Sep 17 00:00:00 2001 From: Friederich Loheide Date: Wed, 23 Sep 2026 17:20:02 +0000 Subject: [PATCH] Close button-only bypass via XML-RPC system.multicall The credential-based exemption used did_action( 'application_password_did_authenticate'), which is request-global. In a system.multicall, a first boxcar authenticated with any application password let later boxcars sign in other users with a normal password. The exemption now applies only to the user the application password authenticated in the same authenticate pass (reset at priority 0). Co-Authored-By: Claude Opus 5.5 (1M context) --- includes/class-m365-login-auth.php | 34 +++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/includes/class-m365-login-auth.php b/includes/class-m365-login-auth.php index b0997b7..0a24efd 100644 --- a/includes/class-m365-login-auth.php +++ b/includes/class-m365-login-auth.php @@ -37,6 +37,13 @@ class M365_Login_Auth { */ private $graph; + /** + * User authenticated by an application password in the current authenticate pass (0 = none). + * + * @var int + */ + private $app_password_user = 0; + /** * Constructor. * @@ -55,6 +62,9 @@ class M365_Login_Auth { add_action( 'init', array( $this, 'maybe_accept_fallback_key' ), 6 ); // Runs after core's username/password handlers (priority 20), which would otherwise overwrite an early WP_Error. add_filter( 'authenticate', array( $this, 'block_password_login' ), 99, 3 ); + // Track application-password sign-ins per authenticate pass (XML-RPC multicall runs several passes per request). + add_filter( 'authenticate', array( $this, 'reset_app_password_user' ), 0 ); + add_action( 'application_password_did_authenticate', array( $this, 'remember_app_password_user' ) ); // No session cookies from API contexts (XML-RPC, REST) while password sign-in is disabled. add_filter( 'send_auth_cookies', array( $this, 'block_api_auth_cookies' ), 99, 4 ); @@ -164,7 +174,9 @@ class M365_Login_Auth { // Exempt by credential, not by request context: application passwords (XML-RPC, REST) and // WP-CLI keep working. A normal password is refused everywhere – also in forms of other // plugins that happen to run inside xmlrpc.php or a REST request. - if ( ( defined( 'WP_CLI' ) && WP_CLI ) || ( $user instanceof WP_User && did_action( 'application_password_did_authenticate' ) ) ) { + // The application password must have authenticated exactly this user in this pass – did_action() + // is request-global and would let a later multicall boxcar through with a normal password. + if ( ( defined( 'WP_CLI' ) && WP_CLI ) || ( $user instanceof WP_User && $this->app_password_user && $this->app_password_user === $user->ID ) ) { return $user; } @@ -182,6 +194,26 @@ class M365_Login_Auth { return new WP_Error( 'm365_login_button_only', __( 'Password sign-in is disabled on this site. Please use the Microsoft button.', 'm365-login' ) ); } + /** + * Starts a new authenticate pass (runs first on the authenticate filter). + * + * @param null|WP_User|WP_Error $user Result so far (unchanged). + * @return null|WP_User|WP_Error + */ + public function reset_app_password_user( $user ) { + $this->app_password_user = 0; + return $user; + } + + /** + * Remembers which user an application password authenticated in the current pass. + * + * @param WP_User $user Authenticated user. + */ + public function remember_app_password_user( $user ) { + $this->app_password_user = $user instanceof WP_User ? (int) $user->ID : 0; + } + /** * While button-only mode is active, API requests (XML-RPC, REST) never receive session cookies. *