From 6c9ba6811ea142d11826e3db6b1a6c54e7b1a07b Mon Sep 17 00:00:00 2001 From: Joseph Scott Date: Thu, 9 Jul 2026 09:34:39 -0600 Subject: [PATCH 1/3] XML-RPC: wp.getUsersBlogs, enforce arg string requirements https://core.trac.wordpress.org/ticket/65600 --- src/wp-includes/class-wp-xmlrpc-server.php | 5 +++ .../phpunit/tests/xmlrpc/wp/getUsersBlogs.php | 42 +++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 tests/phpunit/tests/xmlrpc/wp/getUsersBlogs.php diff --git a/src/wp-includes/class-wp-xmlrpc-server.php b/src/wp-includes/class-wp-xmlrpc-server.php index 1fff1bba65adf..9c0ba96199909 100644 --- a/src/wp-includes/class-wp-xmlrpc-server.php +++ b/src/wp-includes/class-wp-xmlrpc-server.php @@ -717,6 +717,11 @@ public function wp_getUsersBlogs( $args ) { return $this->error; } + if ( ! is_string( $args[0] ) || ! is_string( $args[1] ) ) { + $this->error = new IXR_Error( 400, __( 'Invalid arguments passed to this XML-RPC method. Requires two strings.' ) ); + return $this->error; + } + // If this isn't on WPMU then just use blogger_getUsersBlogs(). if ( ! is_multisite() ) { array_unshift( $args, 1 ); diff --git a/tests/phpunit/tests/xmlrpc/wp/getUsersBlogs.php b/tests/phpunit/tests/xmlrpc/wp/getUsersBlogs.php new file mode 100644 index 0000000000000..dbc98039e7c76 --- /dev/null +++ b/tests/phpunit/tests/xmlrpc/wp/getUsersBlogs.php @@ -0,0 +1,42 @@ +make_user_by_role( 'subscriber' ); + + $result = $this->myxmlrpcserver->wp_getUsersBlogs( array( $username, $password ) ); + + $this->assertIXRError( $result ); + $this->assertSame( 400, $result->code ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_non_string_credentials() { + return array( + 'an array as password' => array( 'subscriber', array() ), + 'an array as username' => array( array(), 'subscriber' ), + 'arrays as username and password' => array( array(), array() ), + 'an integer as password' => array( 'subscriber', 12345 ), + ); + } +} From 9d8c7dfb6689042772562b0d0a2db710eb33e4a1 Mon Sep 17 00:00:00 2001 From: Joseph Scott Date: Thu, 9 Jul 2026 09:39:33 -0600 Subject: [PATCH 2/3] Fix lint --- tests/phpunit/tests/xmlrpc/wp/getUsersBlogs.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/phpunit/tests/xmlrpc/wp/getUsersBlogs.php b/tests/phpunit/tests/xmlrpc/wp/getUsersBlogs.php index dbc98039e7c76..0f9650a376ed0 100644 --- a/tests/phpunit/tests/xmlrpc/wp/getUsersBlogs.php +++ b/tests/phpunit/tests/xmlrpc/wp/getUsersBlogs.php @@ -33,10 +33,10 @@ public function test_non_string_credentials_should_return_error( $username, $pas */ public function data_non_string_credentials() { return array( - 'an array as password' => array( 'subscriber', array() ), - 'an array as username' => array( array(), 'subscriber' ), - 'arrays as username and password' => array( array(), array() ), - 'an integer as password' => array( 'subscriber', 12345 ), + 'an array as password' => array( 'subscriber', array() ), + 'an array as username' => array( array(), 'subscriber' ), + 'arrays as username and password' => array( array(), array() ), + 'an integer as password' => array( 'subscriber', 12345 ), ); } } From b198464c479d786c01536d216422744693207954 Mon Sep 17 00:00:00 2001 From: Joseph Scott Date: Thu, 6 Aug 2026 15:50:41 -0600 Subject: [PATCH 3/3] Address issues from Opus 5 review https://github.com/WordPress/wordpress-develop/pull/12462#pullrequestreview-4850580790 --- src/wp-includes/class-wp-xmlrpc-server.php | 9 +-- tests/phpunit/tests/xmlrpc/basic.php | 56 ++++++++++++++ .../phpunit/tests/xmlrpc/wp/getUsersBlogs.php | 77 +++++++++++++++---- 3 files changed, 124 insertions(+), 18 deletions(-) diff --git a/src/wp-includes/class-wp-xmlrpc-server.php b/src/wp-includes/class-wp-xmlrpc-server.php index cc588062b7ebe..479b44b159434 100644 --- a/src/wp-includes/class-wp-xmlrpc-server.php +++ b/src/wp-includes/class-wp-xmlrpc-server.php @@ -287,6 +287,7 @@ public function addTwoNumbers( $args ) { * Logs user in. * * @since 2.8.0 + * @since 7.2.0 Non-scalar credentials are treated as a failed login. * * @param string $username User's username. * @param string $password User's password. @@ -304,6 +305,9 @@ public function login( if ( $this->auth_failed ) { $user = new WP_Error( 'login_prevented' ); + } elseif ( ! is_scalar( $username ) || ! is_scalar( $password ) ) { + // A request can supply an array or object, which wp_authenticate() cannot handle. + $user = new WP_Error( 'invalid_credentials' ); } else { $user = wp_authenticate( $username, $password ); } @@ -719,11 +723,6 @@ public function wp_getUsersBlogs( $args ) { return $this->error; } - if ( ! is_string( $args[0] ) || ! is_string( $args[1] ) ) { - $this->error = new IXR_Error( 400, __( 'Invalid arguments passed to this XML-RPC method. Requires two strings.' ) ); - return $this->error; - } - // If this isn't on WPMU then just use blogger_getUsersBlogs(). if ( ! is_multisite() ) { array_unshift( $args, 1 ); diff --git a/tests/phpunit/tests/xmlrpc/basic.php b/tests/phpunit/tests/xmlrpc/basic.php index a56a721a07fc6..a072a172f7b33 100644 --- a/tests/phpunit/tests/xmlrpc/basic.php +++ b/tests/phpunit/tests/xmlrpc/basic.php @@ -33,6 +33,62 @@ public function test_login_pass_bad() { $this->assertFalse( $this->myxmlrpcserver->login_pass_ok( 'subscriber', 'subscriber' ) ); } + /** + * Tests that non-scalar credentials are rejected before reaching wp_authenticate(). + * + * A non-scalar password triggers a fatal error there, and a non-scalar username + * an E_USER_WARNING. + * + * @ticket 65600 + * + * @dataProvider data_non_scalar_credentials + * + * @covers wp_xmlrpc_server::login + * + * @param mixed $username Username argument. + * @param mixed $password Password argument. + */ + public function test_login_with_non_scalar_credentials( $username, $password ): void { + $this->assertFalse( $this->myxmlrpcserver->login( $username, $password ), 'The login did not fail.' ); + $this->assertIXRError( $this->myxmlrpcserver->error ); + $this->assertSame( 403, $this->myxmlrpcserver->error->code, 'The error code was not 403.' ); + $this->assertSame( 'Incorrect username or password.', $this->myxmlrpcserver->error->message, 'The error message did not match.' ); + } + + /** + * Data provider for test_login_with_non_scalar_credentials. + * + * @return array[] + */ + public static function data_non_scalar_credentials(): array { + return array( + 'an array as password' => array( 'subscriber', array() ), + 'an array as username' => array( array(), 'subscriber' ), + 'an object as password' => array( 'subscriber', new IXR_Date( '20260806T00:00:00' ) ), + 'null as password' => array( 'subscriber', null ), + ); + } + + /** + * Tests that numeric credentials, which XML-RPC sends as `` values, + * continue to authenticate. + * + * @ticket 65600 + * + * @covers wp_xmlrpc_server::login + */ + public function test_login_with_numeric_credentials(): void { + self::factory()->user->create( + array( + 'user_login' => '12345', + 'user_pass' => '67890', + 'role' => 'subscriber', + ) + ); + + $this->assertInstanceOf( 'WP_User', $this->myxmlrpcserver->login( 12345, 67890 ) ); + } + /** * @ticket 34336 */ diff --git a/tests/phpunit/tests/xmlrpc/wp/getUsersBlogs.php b/tests/phpunit/tests/xmlrpc/wp/getUsersBlogs.php index 0f9650a376ed0..b8848db401a33 100644 --- a/tests/phpunit/tests/xmlrpc/wp/getUsersBlogs.php +++ b/tests/phpunit/tests/xmlrpc/wp/getUsersBlogs.php @@ -1,42 +1,93 @@ make_user_by_role( 'subscriber' ); - + public function test_non_scalar_credentials_should_return_error( $username, $password ): void { $result = $this->myxmlrpcserver->wp_getUsersBlogs( array( $username, $password ) ); $this->assertIXRError( $result ); - $this->assertSame( 400, $result->code ); + $this->assertSame( 403, $result->code ); + $this->assertSame( 'Incorrect username or password.', $result->message ); } /** - * Data provider. + * Data provider for test_non_scalar_credentials_should_return_error. * * @return array[] */ - public function data_non_string_credentials() { + public static function data_non_scalar_credentials(): array { return array( - 'an array as password' => array( 'subscriber', array() ), - 'an array as username' => array( array(), 'subscriber' ), - 'arrays as username and password' => array( array(), array() ), - 'an integer as password' => array( 'subscriber', 12345 ), + 'an array as password' => array( 'subscriber', array() ), + 'an array as username' => array( array(), 'subscriber' ), + 'an object as password' => array( 'subscriber', new IXR_Date( '20260806T00:00:00' ) ), + 'an object as username' => array( new IXR_Date( '20260806T00:00:00' ), 'subscriber' ), + 'null as password' => array( 'subscriber', null ), + 'null as username' => array( null, 'subscriber' ), ); } + + /** + * Tests that valid string credentials are still delegated to blogger_getUsersBlogs(). + * + * @ticket 65600 + * @group ms-excluded + */ + public function test_valid_credentials_should_return_blogs(): void { + $this->make_user_by_role( 'subscriber' ); + + $result = $this->myxmlrpcserver->wp_getUsersBlogs( array( 'subscriber', 'subscriber' ) ); + + $this->assertNotIXRError( $result, 'The result should not be an instance of IXR_Error.' ); + $this->assertIsArray( $result, 'The result should be an array.' ); + $this->assertCount( 1, $result, 'The result should contain a single blog.' ); + + $blog = $result[0]; + $this->assertSame( '1', $blog['blogid'], 'The blogid should be that of the only blog.' ); + $this->assertSame( get_option( 'blogname' ), $blog['blogName'], 'The blogName should match the site name.' ); + $this->assertFalse( $blog['isAdmin'], 'A subscriber should not be flagged as an administrator.' ); + } + + /** + * Tests that valid string credentials still return blogs on multisite, where + * wp_getUsersBlogs() handles the request itself rather than delegating. + * + * @ticket 65600 + * @group ms-required + * @group multisite + */ + public function test_valid_credentials_should_return_blogs_on_multisite(): void { + $this->make_user_by_role( 'subscriber' ); + + $result = $this->myxmlrpcserver->wp_getUsersBlogs( array( 'subscriber', 'subscriber' ) ); + + $this->assertNotIXRError( $result, 'The result should not be an instance of IXR_Error.' ); + $this->assertIsArray( $result, 'The result should be an array.' ); + $this->assertNotEmpty( $result, 'The result should not be empty.' ); + + $blog = $result[0]; + $this->assertArrayHasKey( 'url', $blog, 'The result should include the url field.' ); + $this->assertArrayHasKey( 'blogid', $blog, 'The result should include the blogid field.' ); + $this->assertArrayHasKey( 'blogName', $blog, 'The result should include the blogName field.' ); + $this->assertArrayHasKey( 'isPrimary', $blog, 'The result should include the isPrimary field.' ); + } }