CVE-2020-28036 (published 2020-11-02T21:15Z)

wp-includes/class-wp-xmlrpc-server.php in WordPress before 5.5.2 allows attackers to gain privileges by using XML-RPC to comment on a post.

Severity: HIGH
CVSS v2 base score: 7.5 (AV:N/AC:L/Au:N/C:P/I:P/A:P)
CVSS v3 base score: 9.8, CRITICAL (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: CWE-862
cwe_name: Missing Authorization
description: The product does not perform an authorization check when an actor attempts to access a resource or perform an action.
extended_description: 
            Assuming a user with a given identity, authorization is the process of determining whether that user can access a given resource, based on the user's privileges and any permissions or other access-control specifications that apply to the resource.
            When access control checks are not applied, users are able to access data or perform actions that they should not be allowed to perform. This can lead to a wide range of problems, including information exposures, denial of service, and arbitrary code execution.
         
         

Fix information:
repo_url: https://github.com/WordPress/wordpress-develop
commit_details: repo_url: https://github.com/WordPress/wordpress-develop
author: Jonathan Desrosiers
author_date: 2020-10-29 17:42:13+00:00
msg: XML-RPC: Improve error messages for unprivileged users.

Add specific permission checks to avoid ambiguous failure messages.

Props zieladam, peterwilsoncc, xknown, whyisjake.

git-svn-id: https://develop.svn.wordpress.org/trunk@49380 602fd350-edb4-49c9-b593-d223f7449a82
repository: repo_url: https://github.com/WordPress/wordpress-develop
repo_name: WordPress/wordpress-develop
description: WordPress Develop, Git-ified. Synced from git://develop.git.wordpress.org/, including branches and tags! This repository is just a mirror of the WordPress subversion repository. Please include a link to a pre-existing ticket on https://core.trac.wordpress.org/ with every pull request.
repo_language: PHP
owner: WordPress
file_changes: filename: class-wp-xmlrpc-server.php
change_type: ModificationType.MODIFY
diff: @@ -3876,6 +3876,21 @@ class wp_xmlrpc_server extends IXR_Server {
 			return new IXR_Error( 403, __( 'Sorry, comments are closed for this item.' ) );
 		}
 
+		if (
+			'publish' === get_post_status( $post_id ) &&
+			! current_user_can( 'edit_post', $post_id ) &&
+			post_password_required( $post_id )
+		) {
+			return new IXR_Error( 403, __( 'Sorry, you are not allowed to comment on this post.' ) );
+		}
+
+		if (
+			'private' === get_post_status( $post_id ) &&
+			! current_user_can( 'read_post', $post_id )
+		) {
+			return new IXR_Error( 403, __( 'Sorry, you are not allowed to comment on this post.' ) );
+		}
+
 		$comment = array(
 			'comment_post_ID' => $post_id,
 			'comment_content' => trim( $content_struct['content'] ),

programming_language: PHP
filename: newComment.php
change_type: ModificationType.MODIFY
diff: @@ -6,15 +6,45 @@
 class Tests_XMLRPC_wp_newComment extends WP_XMLRPC_UnitTestCase {
 
 	/**
-	 * Post object for shared fixture.
+	 * Array of posts.
 	 *
-	 * @var WP_Post
+	 * @var WP_Post[]
 	 */
-	public static $post;
+	public static $posts;
+
+	/**
+	 * User IDs.
+	 *
+	 * Array of user IDs keyed by role.
+	 *
+	 * @var int[]
+	 */
+	public static $user_ids;
 
 	public static function wpSetUpBeforeClass( $factory ) {
-		self::make_user_by_role( 'administrator' );
-		self::$post = $factory->post->create_and_get();
+		self::$user_ids                     = array(
+			'administrator' => self::make_user_by_role( 'administrator' ),
+			'contributor'   => self::make_user_by_role( 'contributor' ),
+		);
+		self::$posts['publish']             = $factory->post->create_and_get();
+		self::$posts['password']            = $factory->post->create_and_get(
+			array(
+				'post_password' => 'xmlrpc',
+				'post_author'   => self::$user_ids['administrator'],
+			)
+		);
+		self::$posts['private']             = $factory->post->create_and_get(
+			array(
+				'post_status' => 'private',
+				'post_author' => self::$user_ids['administrator'],
+			)
+		);
+		self::$posts['private_contributor'] = $factory->post->create_and_get(
+			array(
+				'post_status' => 'private',
+				'post_author' => self::$user_ids['contributor'],
+			)
+		);
 	}
 
 	function test_valid_comment() {
@@ -23,7 +53,7 @@ class Tests_XMLRPC_wp_newComment extends WP_XMLRPC_UnitTestCase {
 				1,
 				'administrator',
 				'administrator',
-				self::$post->ID,
+				self::$posts['publish']->ID,
 				array(
 					'content' => rand_str( 100 ),
 				),
@@ -39,7 +69,7 @@ class Tests_XMLRPC_wp_newComment extends WP_XMLRPC_UnitTestCase {
 				1,
 				'administrator',
 				'administrator',
-				self::$post->ID,
+				self::$posts['publish']->ID,
 				array(
 					'content' => '',
 				),
@@ -59,7 +89,7 @@ class Tests_XMLRPC_wp_newComment extends WP_XMLRPC_UnitTestCase {
 				1,
 				'administrator',
 				'administrator',
-				self::$post->ID,
+				self::$posts['publish']->ID,
 				array(
 					'content' => '   ',
 				),
@@ -79,7 +109,7 @@ class Tests_XMLRPC_wp_newComment extends WP_XMLRPC_UnitTestCase {
 				1,
 				'administrator',
 				'administrator',
-				self::$post->ID,
+				self::$posts['publish']->ID,
 				array(
 					'content' => '0',
 				),
@@ -99,7 +129,7 @@ class Tests_XMLRPC_wp_newComment extends WP_XMLRPC_UnitTestCase {
 				1,
 				'administrator',
 				'administrator',
-				self::$post->ID,
+				self::$posts['publish']->ID,
 				array(
 					'content' => '   ',
 				),
@@ -139,7 +169,7 @@ class Tests_XMLRPC_wp_newComment extends WP_XMLRPC_UnitTestCase {
 			1,
 			'administrator',
 			'administrator',
-			self::$post->ID,
+			self::$posts['publish']->ID,
 			array(
 				'content' => rand_str( 100 ),
 			),
@@ -168,7 +198,7 @@ class Tests_XMLRPC_wp_newComment extends WP_XMLRPC_UnitTestCase {
 			1,
 			'',
 			'',
-			self::$post->ID,
+			self::$posts['publish']->ID,
 			array(
 				'author'       => 'WordPress',
 				'author_email' => 'noreply@wordpress.org',
@@ -193,7 +223,7 @@ class Tests_XMLRPC_wp_newComment extends WP_XMLRPC_UnitTestCase {
 			1,
 			'',
 			'',
-			self::$post->ID,
+			self::$posts['publish']->ID,
 			array(
 				'author'       => 'WordPress',
 				'author_email' => 'noreply at wordpress.org',
@@ -218,7 +248,7 @@ class Tests_XMLRPC_wp_newComment extends WP_XMLRPC_UnitTestCase {
 			1,
 			'administrator',
 			'administrator',
-			self::$post->ID,
+			self::$posts['publish']->ID,
 			array(
 				'author'       => 'WordPress',
 				'author_email' => 'noreply at wordpress.org',
@@ -232,4 +262,131 @@ class Tests_XMLRPC_wp_newComment extends WP_XMLRPC_UnitTestCase {
 
 		$this->assertSame( $user_id, (int) $comment->user_id );
 	}
+
+	/**
+	 * Ensure users can only comment on posts they're permitted to access.
+	 *
+	 * @dataProvider data_comments_observe_post_permissions
+	 *
+	 * @param string $post_key      Post identifier from the self::$posts array.
+	 * @param string $username      Username leaving comment.
+	 * @param bool   $expected      Expected result. True: successfull comment. False: Refused comment.
+	 * @param string $anon_callback Optional. Allow anonymous comment callback. Default __return_false.
+	 */
+	function test_comments_observe_post_permissions( $post_key, $username, $expected, $anon_callback = '__return_false' ) {
+		add_filter( 'xmlrpc_allow_anonymous_comments', $anon_callback );
+
+		$comment_args = array(
+			1,
+			$username,
+			$username,
+			self::$posts[ $post_key ]->ID,
+			array(
+				'author'       => 'WordPress',
+				'author_email' => 'noreply@wordpress.org',
+				'content'      => 'Test Comment',
+			),
+		);
+
+		$result = $this->myxmlrpcserver->wp_newComment( $comment_args );
+		if ( $expected ) {
+			$this->assertInternalType( 'int', $result );
+			return;
+		}
+
+		$this->assertIXRError( $result );
+		$this->assertSame( 403, $result->code );
+	}
+
+	/**
+	 * Data provider for test_comments_observe_post_permissions.
+	 *
+	 * @return array[] {
+	 *     @type string Post identifier from the self::$posts array.
+	 *     @type string Username leaving comment.
+	 *     @type bool   Expected result. True: successfull comment. False: Refused comment.
+	 *     @type string Optional. Allow anonymous comment callback. Default __return_false.
+	 * }
+	 */
+	function data_comments_observe_post_permissions() {
+		return array(
+			// 0: Post author, password protected public post.
+			array(
+				'password',
+				'administrator',
+				true,
+			),
+			// 1: Low privileged non-author, password protected public post.
+			array(
+				'password',
+				'contributor',
+				false,
+			),
+			// 2: Anonymous user, password protected public post.
+			array(
+				'password',
+				'', // Anonymous user.
+				false,
+			),
+			// 3: Anonymous user, anon comments allowed, password protected public post.
+			array(
+				'password',
+				'', // Anonymous user.
+				false,
+				'__return_true',
+			),
+
+			// 4: Post author, private post.
+			array(
+				'private',
+				'administrator',
+				true,
+			),
+			// 5: Low privileged non-author, private post.
+			array(
+				'private',
+				'contributor',
+				false,
+			),
+			// 6: Anonymous user, private post.
+			array(
+				'private',
+				'', // Anonymous user.
+				false,
+			),
+			// 7: Anonymous user, anon comments allowed, private post.
+			array(
+				'private',
+				'', // Anonymous user.
+				false,
+				'__return_true',
+			),
+
+			// 8: High privileged non-author, private post.
+			array(
+				'private_contributor',
+				'administrator',
+				true,
+			),
+			// 9: Low privileged author, private post.
+			array(
+				'private_contributor',
+				'contributor',
+				true,
+			),
+			// 10: Anonymous user, private post.
+			array(
+				'private_contributor',
+				'', // Anonymous user.
+				false,
+			),
+			// 11: Anonymous user, anon comments allowed, private post.
+			array(
+				'private_contributor',
+				'', // Anonymous user.
+				false,
+				'__return_true',
+			),
+		);
+	}
 }

programming_language: PHP