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) <noreply@anthropic.com>
This commit is contained in:
Friederich Loheide 2026-09-23 17:20:02 +00:00
parent 850f0dcd54
commit b35f6a867b

View file

@ -37,6 +37,13 @@ class M365_Login_Auth {
*/ */
private $graph; private $graph;
/**
* User authenticated by an application password in the current authenticate pass (0 = none).
*
* @var int
*/
private $app_password_user = 0;
/** /**
* Constructor. * Constructor.
* *
@ -55,6 +62,9 @@ class M365_Login_Auth {
add_action( 'init', array( $this, 'maybe_accept_fallback_key' ), 6 ); 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. // 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 ); 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. // 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 ); 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 // 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 // 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. // 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; 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' ) ); 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. * While button-only mode is active, API requests (XML-RPC, REST) never receive session cookies.
* *