From 87063b8554b82541573030c21f9e253febc867d3 Mon Sep 17 00:00:00 2001 From: Adam Cassis Date: Wed, 8 Apr 2026 09:27:05 +0200 Subject: [PATCH 1/2] fix: add object-level authorization to corrections REST endpoint The corrections save endpoint only checked `edit_posts` capability, allowing any author/editor to add, modify, or delete corrections on posts they don't own. Now checks `edit_post` for the specific post. Co-Authored-By: Claude Opus 4.6 (1M context) --- includes/corrections/class-corrections.php | 4 +++ tests/unit-tests/corrections.php | 39 ++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/includes/corrections/class-corrections.php b/includes/corrections/class-corrections.php index 94f5a04f4b..ee2fb07da7 100644 --- a/includes/corrections/class-corrections.php +++ b/includes/corrections/class-corrections.php @@ -228,6 +228,10 @@ public static function rest_save_corrections( WP_REST_Request $request ) { return rest_ensure_response( new WP_Error( 'invalid_post_id', 'Invalid post ID.', [ 'status' => 400 ] ) ); } + if ( ! current_user_can( 'edit_post', $post_id ) ) { + return rest_ensure_response( new WP_Error( 'unauthorized', 'You do not have permission to edit this post.', [ 'status' => 403 ] ) ); + } + $existing_corrections = self::get_corrections( $post_id ); $existing_ids = wp_list_pluck( $existing_corrections, 'ID' ); diff --git a/tests/unit-tests/corrections.php b/tests/unit-tests/corrections.php index 014255655a..7fd56b07ca 100644 --- a/tests/unit-tests/corrections.php +++ b/tests/unit-tests/corrections.php @@ -21,6 +21,13 @@ class Test_Corrections extends WP_UnitTestCase { */ protected static $post_id; + /** + * Holds an editor user ID. + * + * @var int + */ + protected static $editor_id; + /** * Set up test fixtures. */ @@ -29,6 +36,9 @@ public function set_up() { Corrections::init(); + self::$editor_id = $this->factory()->user->create( [ 'role' => 'editor' ] ); + wp_set_current_user( self::$editor_id ); + self::$post_id = $this->factory()->post->create( [ 'post_type' => 'post' ] ); } @@ -696,4 +706,33 @@ function( $types ) { $filtered_types = Corrections::get_supported_post_types(); $this->assertContains( 'test_post_type', $filtered_types ); } + + /** + * Test that an author cannot save corrections for another user's post. + * + * @covers \Newspack\Corrections::rest_save_corrections + */ + public function test_cannot_save_corrections_for_other_users_post() { + $author_id = $this->factory()->user->create( [ 'role' => 'author' ] ); + wp_set_current_user( $author_id ); + + $corrections = [ + [ + 'id' => null, + 'content' => 'IDOR injected correction', + 'type' => 'correction', + 'date' => current_time( 'mysql' ), + 'priority' => 'high', + ], + ]; + + $request = new WP_REST_Request( WP_REST_Server::CREATABLE, '/' . NEWSPACK_API_NAMESPACE . '/corrections/' ); + $request->set_param( 'post_id', self::$post_id ); + $request->set_param( 'corrections', $corrections ); + + $response = Corrections::rest_save_corrections( $request ); + + $this->assertInstanceOf( 'WP_Error', $response, 'Authors should not be able to save corrections for posts they do not own.' ); + $this->assertEquals( 'unauthorized', $response->get_error_code() ); + } } From 161c988c1d9e272c9cc3e6b2faba4d1d44eabf74 Mon Sep 17 00:00:00 2001 From: Adam Cassis Date: Wed, 8 Apr 2026 14:06:34 +0200 Subject: [PATCH 2/2] fix: validate correction ownership before updating Co-Authored-By: Claude Opus 4.6 (1M context) --- includes/corrections/class-corrections.php | 4 ++ tests/unit-tests/corrections.php | 49 ++++++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/includes/corrections/class-corrections.php b/includes/corrections/class-corrections.php index ee2fb07da7..61f01aa4d2 100644 --- a/includes/corrections/class-corrections.php +++ b/includes/corrections/class-corrections.php @@ -247,6 +247,10 @@ public static function rest_save_corrections( WP_REST_Request $request ) { // ID will be null if it's a new correction. if ( ! empty( $correction_id ) ) { + // Verify the correction belongs to this post. + if ( ! in_array( $correction_id, $existing_ids, true ) ) { + return rest_ensure_response( new WP_Error( 'invalid_correction', 'The correction does not belong to this post.', [ 'status' => 400 ] ) ); + } // Update existing correction. self::update_correction( $post_id, $correction_id, $correction ); $processed_ids[] = $correction_id; diff --git a/tests/unit-tests/corrections.php b/tests/unit-tests/corrections.php index 7fd56b07ca..ae64590e84 100644 --- a/tests/unit-tests/corrections.php +++ b/tests/unit-tests/corrections.php @@ -734,5 +734,54 @@ public function test_cannot_save_corrections_for_other_users_post() { $this->assertInstanceOf( 'WP_Error', $response, 'Authors should not be able to save corrections for posts they do not own.' ); $this->assertEquals( 'unauthorized', $response->get_error_code() ); + $this->assertEquals( 403, $response->get_error_data()['status'] ); + } + + /** + * Test that a correction ID belonging to a different post cannot be updated via another post. + * + * @covers \Newspack\Corrections::rest_save_corrections + */ + public function test_cannot_update_correction_belonging_to_different_post() { + wp_set_current_user( self::$editor_id ); + + // Create a correction on the default post. + $correction_id = Corrections::add_correction( + self::$post_id, + [ + 'content' => 'Original correction', + 'type' => 'correction', + 'date' => current_time( 'mysql' ), + 'priority' => 'low', + ] + ); + + // Create a second post that the editor also owns. + $other_post_id = $this->factory()->post->create( [ 'post_author' => self::$editor_id ] ); + + // Attempt to update the first post's correction via the second post's endpoint. + $corrections = [ + [ + 'id' => $correction_id, + 'content' => 'Hijacked correction', + 'type' => 'correction', + 'date' => current_time( 'mysql' ), + 'priority' => 'high', + ], + ]; + + $request = new WP_REST_Request( WP_REST_Server::CREATABLE, '/' . NEWSPACK_API_NAMESPACE . '/corrections/' ); + $request->set_param( 'post_id', $other_post_id ); + $request->set_param( 'corrections', $corrections ); + + $response = Corrections::rest_save_corrections( $request ); + + $this->assertInstanceOf( 'WP_Error', $response, 'Should not allow updating a correction that belongs to a different post.' ); + $this->assertEquals( 'invalid_correction', $response->get_error_code() ); + $this->assertEquals( 400, $response->get_error_data()['status'] ); + + // Verify the original correction was not modified. + $original = get_post( $correction_id ); + $this->assertEquals( 'Original correction', $original->post_content ); } }