Commit fb990bbb1f for wordpress.org

commit fb990bbb1f90294063b036304c496e71bf8a05eb
Author: adamsilverstein <adamsilverstein@git.wordpress.org>
Date:   Thu Sep 17 17:47:46 2026 +0000

    Comments: Enforce target post permissions when updating notes via REST.

    Props adamsilverstein, westonruter, ramonopoly, villanovachile.

    Built from https://develop.svn.wordpress.org/trunk@63675


    git-svn-id: http://core.svn.wordpress.org/trunk@62849 1a063a9b-81f0-0310-95a4-ce76da25c4cd

diff --git a/wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php b/wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php
index dfe4946595..8846f72788 100644
--- a/wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php
+++ b/wp-includes/rest-api/endpoints/class-wp-rest-comments-controller.php
@@ -560,12 +560,12 @@ class WP_REST_Comments_Controller extends WP_REST_Controller {
 			}
 		}

-		if ( $is_note && ! empty( $request['post'] ) && ! current_user_can( 'edit_post', (int) $request['post'] ) ) {
-			return new WP_Error(
-				'rest_cannot_create_note',
-				__( 'Sorry, you are not allowed to create notes for this post.' ),
-				array( 'status' => rest_authorization_required_code() )
-			);
+		if ( $is_note && ! empty( $request['post'] ) ) {
+			$target_check = $this->check_target_post_permission( (int) $request['post'], $request, true );
+
+			if ( is_wp_error( $target_check ) ) {
+				return $target_check;
+			}
 		}

 		$edit_cap = $is_note ? array( 'edit_post', (int) $request['post'] ) : array( 'moderate_comments' );
@@ -586,6 +586,16 @@ class WP_REST_Comments_Controller extends WP_REST_Controller {
 			);
 		}

+		/*
+		 * A note's target post was fully checked by check_target_post_permission()
+		 * above. Everything below applies to other comments only: a note is allowed
+		 * on a draft and on a post whose discussion is closed, and the rest would
+		 * repeat what that check already did.
+		 */
+		if ( $is_note ) {
+			return true;
+		}
+
 		$post = get_post( (int) $request['post'] );

 		if ( ! $post ) {
@@ -596,15 +606,7 @@ class WP_REST_Comments_Controller extends WP_REST_Controller {
 			);
 		}

-		if ( $is_note && ! $this->check_post_type_supports_notes( $post->post_type ) ) {
-			return new WP_Error(
-				'rest_comment_not_supported_post_type',
-				__( 'Sorry, this post type does not support notes.' ),
-				array( 'status' => 403 )
-			);
-		}
-
-		if ( 'draft' === $post->post_status && ! $is_note ) {
+		if ( 'draft' === $post->post_status ) {
 			return new WP_Error(
 				'rest_comment_draft_post',
 				__( 'Sorry, you are not allowed to create a comment on this post.' ),
@@ -628,7 +630,7 @@ class WP_REST_Comments_Controller extends WP_REST_Controller {
 			);
 		}

-		if ( ! comments_open( $post->ID ) && ! $is_note ) {
+		if ( ! comments_open( $post->ID ) ) {
 			return new WP_Error(
 				'rest_comment_closed',
 				__( 'Sorry, comments are closed for this item.' ),
@@ -862,6 +864,7 @@ class WP_REST_Comments_Controller extends WP_REST_Controller {
 	 * Checks if a given REST request has access to update a comment.
 	 *
 	 * @since 4.7.0
+	 * @since 7.1.1 Target post permissions are checked when a comment's parent post is changed.
 	 *
 	 * @param WP_REST_Request $request Full details about the request.
 	 * @return true|WP_Error True if the request has access to update the item, error object otherwise.
@@ -880,6 +883,26 @@ class WP_REST_Comments_Controller extends WP_REST_Controller {
 			);
 		}

+		/*
+		 * check_edit_permission() above only establishes that the comment may be
+		 * edited where it currently sits, because 'edit_comment' maps to 'edit_post'
+		 * on the comment's current parent. When the parent is being changed, the new
+		 * parent has to be authorized as well. Without this, a user holding
+		 * edit_comment on their own comment or note could reparent it onto any post,
+		 * including posts they can neither read nor edit.
+		 */
+		if ( isset( $request['post'] ) && (int) $request['post'] !== (int) $comment->comment_post_ID ) {
+			$target_check = $this->check_target_post_permission(
+				(int) $request['post'],
+				$request,
+				'note' === $comment->comment_type
+			);
+
+			if ( is_wp_error( $target_check ) ) {
+				return $target_check;
+			}
+		}
+
 		return true;
 	}

@@ -2041,6 +2064,106 @@ class WP_REST_Comments_Controller extends WP_REST_Controller {
 		return '' !== $check['comment_content'];
 	}

+	/**
+	 * Checks that a post can receive a comment or a note from the current user.
+	 *
+	 * Used when creating a note and when changing the parent post of an existing
+	 * comment or note, so that attaching content to a post is authorized the same
+	 * way whichever path it arrives by.
+	 *
+	 * @since 7.1.1
+	 *
+	 * @param int             $post_id Target post ID.
+	 * @param WP_REST_Request $request Full details about the request.
+	 * @param bool            $is_note Optional. Whether the comment is a note. Default false.
+	 * @return true|WP_Error True if the post can receive the comment, error object otherwise.
+	 */
+	protected function check_target_post_permission( int $post_id, WP_REST_Request $request, bool $is_note = false ) {
+		if ( ! $post_id ) {
+			return new WP_Error(
+				'rest_comment_invalid_post_id',
+				__( 'Sorry, you are not allowed to create this comment without a post.' ),
+				array( 'status' => 403 )
+			);
+		}
+
+		/*
+		 * Notes are editorial content, so they may only be attached to a post the
+		 * user can edit. Any other comment needs either comment moderation rights
+		 * or edit access to the post, which is what check_edit_permission() grants
+		 * on the post a comment is moving away from. Requiring the same at the
+		 * destination means both ends of a move are authorized alike.
+		 */
+		if ( $is_note ) {
+			$can_target_post = current_user_can( 'edit_post', $post_id );
+		} else {
+			$can_target_post = current_user_can( 'moderate_comments' ) || current_user_can( 'edit_post', $post_id );
+		}
+
+		if ( ! $can_target_post ) {
+			if ( $is_note ) {
+				return new WP_Error(
+					'rest_cannot_create_note',
+					__( 'Sorry, you are not allowed to create notes for this post.' ),
+					array( 'status' => rest_authorization_required_code() )
+				);
+			}
+
+			return new WP_Error(
+				'rest_cannot_edit',
+				__( 'Sorry, you are not allowed to edit this comment.' ),
+				array( 'status' => rest_authorization_required_code() )
+			);
+		}
+
+		$post = get_post( $post_id );
+
+		if ( ! $post ) {
+			return new WP_Error(
+				'rest_comment_invalid_post_id',
+				__( 'Sorry, you are not allowed to create this comment without a post.' ),
+				array( 'status' => 403 )
+			);
+		}
+
+		/*
+		 * The remaining rules mirror the create-time checks for notes only. They are
+		 * deliberately not applied to other comments, because moderators move comments
+		 * onto posts whose discussion has closed and onto drafts today. Enforcing the
+		 * create-time rules there would break that without blocking anything the
+		 * capability check above already permits.
+		 */
+		if ( ! $is_note ) {
+			return true;
+		}
+
+		if ( ! $this->check_post_type_supports_notes( $post->post_type ) ) {
+			return new WP_Error(
+				'rest_comment_not_supported_post_type',
+				__( 'Sorry, this post type does not support notes.' ),
+				array( 'status' => 403 )
+			);
+		}
+
+		if ( 'trash' === $post->post_status ) {
+			return new WP_Error(
+				'rest_comment_trash_post',
+				__( 'Sorry, you are not allowed to create a comment on this post.' ),
+				array( 'status' => 403 )
+			);
+		}
+
+		if ( ! $this->check_read_post_permission( $post, $request ) ) {
+			return new WP_Error(
+				'rest_cannot_read_post',
+				__( 'Sorry, you are not allowed to read the post for this comment.' ),
+				array( 'status' => rest_authorization_required_code() )
+			);
+		}
+
+		return true;
+	}
+
 	/**
 	 * Check if post type supports notes.
 	 *
diff --git a/wp-includes/version.php b/wp-includes/version.php
index 2995bd0538..cd8187ac44 100644
--- a/wp-includes/version.php
+++ b/wp-includes/version.php
@@ -16,7 +16,7 @@
  *
  * @global string $wp_version
  */
-$wp_version = '7.2-alpha-63672';
+$wp_version = '7.2-alpha-63675';

 /**
  * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.