CVE-2023-1979: Incorrect Authorization (CWE-863)
value: The Web Stories for WordPress plugin supports the WordPress built-in functionality of protecting content with a password. The content is then only accessible to website visitors after entering the password. In WordPress, users with the "Author" role can create stories, but don't have the ability to edit password protected stories. The vulnerability allowed users with said role to bypass this permission check when trying to duplicate the protected story in the plugin's own dashboard, giving them access to the seemingly protected content. We recommend upgrading to version 1.32 or beyond commit  ad49781c2a35c5c92ef704d4b621ab4e5cb77d68 https://github.com/GoogleForCreators/web-stories-wp/commit/ad49781c2a35c5c92ef704d4b621ab4e5cb77d68 

CWE description: The product performs an authorization check when an actor attempts to access a resource or perform an action, but it does not correctly perform the check. This allows attackers to bypass intended access restrictions.
CVSS v3 base score: 6.5
Published: 2023-05-08T17:15Z
Repository: https://github.com/GoogleForCreators/web-stories-wp
Language: PHP
Files changed: includes/REST_API/Stories_Base_Controller.php, includes/REST_API/Stories_Controller.php, tests/phpunit/integration/tests/REST_API/Stories_Controller.php
Commit date: 2023-05-02 15:26:03 +0200
Commit message: Merge pull request from GHSA-xcx6-4gm7-wrfm

* Update permission check when duplicating stories

Check for update instead of read permission

* Disallow view context for `style_presets` field

* Do not return `story_data` for password protected stories
Diff: diff --git a/includes/REST_API/Stories_Base_Controller.php b/includes/REST_API/Stories_Base_Controller.php
index e3443439d9..1d8b0ea075 100644
--- a/includes/REST_API/Stories_Base_Controller.php
+++ b/includes/REST_API/Stories_Base_Controller.php
@@ -132,11 +132,11 @@ class Stories_Base_Controller extends WP_REST_Posts_Controller {
 		 */
 		$data = $response->get_data();
 
 		if ( ! empty( $schema['properties']['story_data'] ) && rest_is_field_included( 'story_data', $fields ) ) {
 			$post_story_data    = json_decode( $post->post_content_filtered, true );
-			$data['story_data'] = rest_sanitize_value_from_schema( $post_story_data, $schema['properties']['story_data'] );
+			$data['story_data'] = post_password_required( $post ) ? (object) [] : rest_sanitize_value_from_schema( $post_story_data, $schema['properties']['story_data'] );
 		}
 
 		/**
 		 * Request context.
 		 *
@@ -182,11 +182,11 @@ class Stories_Base_Controller extends WP_REST_Posts_Controller {
 		$original_post = $this->get_post( $original_id );
 		if ( is_wp_error( $original_post ) ) {
 			return $original_post;
 		}
 
-		if ( ! $this->check_read_permission( $original_post ) ) {
+		if ( ! $this->check_update_permission( $original_post ) ) {
 			return new \WP_Error(
 				'rest_cannot_create',
 				__( 'Sorry, you are not allowed to duplicate this story.', 'web-stories' ),
 				[ 'status' => rest_authorization_required_code() ]
 			);
diff --git a/includes/REST_API/Stories_Controller.php b/includes/REST_API/Stories_Controller.php
index 23b5570008..422291d62b 100644
--- a/includes/REST_API/Stories_Controller.php
+++ b/includes/REST_API/Stories_Controller.php
@@ -213,11 +213,11 @@ class Stories_Controller extends Stories_Base_Controller {
 		$schema = parent::get_item_schema();
 
 		$schema['properties']['style_presets'] = [
 			'description' => __( 'Style presets used by all stories', 'web-stories' ),
 			'type'        => 'object',
-			'context'     => [ 'view', 'edit' ],
+			'context'     => [ 'edit' ],
 		];
 
 		$schema['properties']['preview_link'] = [
 			'description' => __( 'Preview Link.', 'web-stories' ),
 			'type'        => 'string',
diff --git a/tests/phpunit/integration/tests/REST_API/Stories_Controller.php b/tests/phpunit/integration/tests/REST_API/Stories_Controller.php
index a804515482..5901ee2adf 100644
--- a/tests/phpunit/integration/tests/REST_API/Stories_Controller.php
+++ b/tests/phpunit/integration/tests/REST_API/Stories_Controller.php
@@ -409,10 +409,40 @@ class Stories_Controller extends DependencyInjectedRestTestCase {
 
 		$this->assertArrayHasKey( 'https://api.w.org/lockuser', $links );
 		$this->assertArrayHasKey( 'https://api.w.org/lock', $links );
 	}
 
+	/**
+	 * @covers ::get_item
+	 * @covers ::prepare_item_for_response
+	 * @covers \Google\Web_Stories\REST_API\Stories_Base_Controller::prepare_item_for_response
+	 */
+	public function test_get_item_no_story_data_for_password_protected_post(): void {
+		$this->controller->register_routes();
+
+		$story = self::factory()->post->create(
+			[
+				'post_type'     => Story_Post_Type::POST_TYPE_SLUG,
+				'post_status'   => 'publish',
+				'post_password' => 'Top Secret',
+				'post_author'   => self::$user_id,
+			]
+		);
+
+		wp_set_current_user( self::$author_id );
+
+		$request  = new WP_REST_Request( \WP_REST_Server::READABLE, '/web-stories/v1/web-story/' . $story );
+		$response = rest_get_server()->dispatch( $request );
+
+		$data = $response->get_data();
+
+		$this->assertIsArray( $data );
+		$this->assertArrayHasKey( 'story_data', $data );
+		$this->assertIsObject( $data['story_data'] );
+		$this->assertEmpty( (array) $data['story_data'] );
+	}
+
 	/**
 	 * @covers ::get_item
 	 * @covers \Google\Web_Stories\REST_API\Stories_Base_Controller::get_available_actions
 	 */
 	public function test_get_available_actions(): void {
@@ -1068,11 +1098,11 @@ class Stories_Controller extends DependencyInjectedRestTestCase {
 	}
 
 	/**
 	 * @covers ::create_item
 	 */
-	public function test_create_item_duplicate_id_permission(): void {
+	public function test_create_item_duplicate_id_no_permission_for_private_post(): void {
 		$this->controller->register_routes();
 
 		$unsanitized_content    = file_get_contents( WEB_STORIES_TEST_DATA_DIR . '/story_post_content.html' );
 		$unsanitized_story_data = wp_json_encode( [ 'pages' => [] ] );
 		$original_id            = self::factory()->post->create(
@@ -1105,10 +1135,52 @@ class Stories_Controller extends DependencyInjectedRestTestCase {
 
 		$response = rest_get_server()->dispatch( $request );
 		$this->assertErrorResponse( 'rest_cannot_create', $response, 403 );
 	}
 
+
+	/**
+	 * @covers ::create_item
+	 */
+	public function test_create_item_duplicate_id_no_permission_for_password_protected_post(): void {
+		$this->controller->register_routes();
+
+		$unsanitized_content    = file_get_contents( WEB_STORIES_TEST_DATA_DIR . '/story_post_content.html' );
+		$unsanitized_story_data = wp_json_encode( [ 'pages' => [] ] );
+		$original_id            = self::factory()->post->create(
+			[
+				'post_type'             => Story_Post_Type::POST_TYPE_SLUG,
+				'post_content'          => $unsanitized_content,
+				'post_title'            => 'Example title',
+				'post_excerpt'          => 'Example excerpt',
+				'post_author'           => self::$user_id,
+				'post_status'           => 'publish',
+				'post_password'         => 'Top Secret',
+				'post_content_filtered' => $unsanitized_story_data,
+			]
+		);
+
+		$attachment_id = self::factory()->attachment->create_upload_object( WEB_STORIES_TEST_DATA_DIR . '/attachment.jpg' );
+
+		$this->assertNotWPError( $attachment_id );
+
+		set_post_thumbnail( $original_id, $attachment_id );
+
+		wp_set_current_user( self::$author_id );
+		$this->kses_int();
+
+		$request = new WP_REST_Request( \WP_REST_Server::CREATABLE, '/web-stories/v1/web-story' );
+		$request->set_body_params(
+			[
+				'original_id' => $original_id,
+			]
+		);
+
+		$response = rest_get_server()->dispatch( $request );
+		$this->assertErrorResponse( 'rest_cannot_create', $response, 403 );
+	}
+
 	/**
 	 * @covers ::update_item
 	 * @covers \Google\Web_Stories\REST_API\Stories_Base_Controller::update_item
 	 */
 	public function test_update_item_as_author_should_not_strip_markup(): void {

Vulnerable code: 			$data['story_data'] = rest_sanitize_value_from_schema( $post_story_data, $schema['properties']['story_data'] );
		if ( ! $this->check_read_permission( $original_post ) ) {
			'context'     => [ 'view', 'edit' ],
	public function test_create_item_duplicate_id_permission(): void {
Fixed code: 			$data['story_data'] = post_password_required( $post ) ? (object) [] : rest_sanitize_value_from_schema( $post_story_data, $schema['properties']['story_data'] );
		if ( ! $this->check_update_permission( $original_post ) ) {
			'context'     => [ 'edit' ],
	/**
	 * @covers ::get_item
	 * @covers ::prepare_item_for_response
	 * @covers \Google\Web_Stories\REST_API\Stories_Base_Controller::prepare_item_for_response
	 */
	public function test_get_item_no_story_data_for_password_protected_post(): void {
		$this->controller->register_routes();

		$story = self::factory()->post->create(
			[
				'post_type'     => Story_Post_Type::POST_TYPE_SLUG,
				'post_status'   => 'publish',
				'post_password' => 'Top Secret',
				'post_author'   => self::$user_id,
			]
		);

		wp_set_current_user( self::$author_id );

		$request  = new WP_REST_Request( \WP_REST_Server::READABLE, '/web-stories/v1/web-story/' . $story );
		$response = rest_get_server()->dispatch( $request );

		$data = $response->get_data();

		$this->assertIsArray( $data );
		$this->assertArrayHasKey( 'story_data', $data );
		$this->assertIsObject( $data['story_data'] );
		$this->assertEmpty( (array) $data['story_data'] );
	}

	public function test_create_item_duplicate_id_no_permission_for_private_post(): void {

	/**
	 * @covers ::create_item
	 */
	public function test_create_item_duplicate_id_no_permission_for_password_protected_post(): void {
		$this->controller->register_routes();

		$unsanitized_content    = file_get_contents( WEB_STORIES_TEST_DATA_DIR . '/story_post_content.html' );
		$unsanitized_story_data = wp_json_encode( [ 'pages' => [] ] );
		$original_id            = self::factory()->post->create(
			[
				'post_type'             => Story_Post_Type::POST_TYPE_SLUG,
				'post_content'          => $unsanitized_content,
				'post_title'            => 'Example title',
				'post_excerpt'          => 'Example excerpt',
				'post_author'           => self::$user_id,
				'post_status'           => 'publish',
				'post_password'         => 'Top Secret',
				'post_content_filtered' => $unsanitized_story_data,
			]
		);

		$attachment_id = self::factory()->attachment->create_upload_object( WEB_STORIES_TEST_DATA_DIR . '/attachment.jpg' );

		$this->assertNotWPError( $attachment_id );

		set_post_thumbnail( $original_id, $attachment_id );

		wp_set_current_user( self::$author_id );
		$this->kses_int();

		$request = new WP_REST_Request( \WP_REST_Server::CREATABLE, '/web-stories/v1/web-story' );
		$request->set_body_params(
			[
				'original_id' => $original_id,
			]
		);

		$response = rest_get_server()->dispatch( $request );
		$this->assertErrorResponse( 'rest_cannot_create', $response, 403 );
	}