Commit bdf44bf2387 for woocommerce

commit bdf44bf2387b6693d4ad1e965264a9d7c813e815
Author: Yuliyan Slavchev <yuliyan.slavchev@gmail.com>
Date:   Fri Sep 4 13:59:36 2026 +0300

    Email Editor: Restrict email preview sending to registered email post types (#68335)

    * Email Editor: Restrict email preview sending to registered email post types

    * Ignore non-array results of the email post types filter

    * Remove filter before calling tearDown in Send_Preview_Email_Test

diff --git a/packages/php/email-editor/changelog/stomail-8437-security-email-editor-preview-route-sends-arbitrary-mail-as b/packages/php/email-editor/changelog/stomail-8437-security-email-editor-preview-route-sends-arbitrary-mail-as
new file mode 100644
index 00000000000..fe411f833a9
--- /dev/null
+++ b/packages/php/email-editor/changelog/stomail-8437-security-email-editor-preview-route-sends-arbitrary-mail-as
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Restrict the send preview email endpoint to posts of registered email post types.
diff --git a/packages/php/email-editor/src/Engine/class-email-editor.php b/packages/php/email-editor/src/Engine/class-email-editor.php
index 0d962687666..b455ac28e0e 100644
--- a/packages/php/email-editor/src/Engine/class-email-editor.php
+++ b/packages/php/email-editor/src/Engine/class-email-editor.php
@@ -137,7 +137,7 @@ class Email_Editor {
 			$request_uri = sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) );
 		}
 		if ( strstr( $request_uri, 'site-editor.php' ) === false ) {
-			$post_types = array_column( $this->get_post_types(), 'name' );
+			$post_types = array_column( self::get_post_types(), 'name' );
 			$this->templates->initialize( $post_types );
 		}
 	}
@@ -158,7 +158,7 @@ class Email_Editor {
 	 * @return void
 	 */
 	private function register_email_post_types(): void {
-		foreach ( $this->get_post_types() as $post_type ) {
+		foreach ( self::get_post_types() as $post_type ) {
 			register_post_type(
 				$post_type['name'],
 				array_merge( $this->get_default_email_post_args(), $post_type['args'] )
@@ -182,9 +182,15 @@ class Email_Editor {
 	 * @return array
 	 * @phpstan-return EmailPostType[]
 	 */
-	private function get_post_types(): array {
-		$post_types = array();
-		return apply_filters( 'woocommerce_email_editor_post_types', $post_types );
+	private static function get_post_types(): array {
+		$post_types = apply_filters( 'woocommerce_email_editor_post_types', array() );
+		/**
+		 * Non-array filter results mean no email post types.
+		 *
+		 * @var EmailPostType[] $post_types
+		 */
+		$post_types = is_array( $post_types ) ? $post_types : array();
+		return $post_types;
 	}

 	/**
@@ -240,7 +246,7 @@ class Email_Editor {
 	 * @return void
 	 */
 	public function extend_email_post_api() {
-		$email_post_types = array_column( $this->get_post_types(), 'name' );
+		$email_post_types = array_column( self::get_post_types(), 'name' );
 		register_rest_field(
 			$email_post_types,
 			'email_data',
@@ -270,7 +276,7 @@ class Email_Editor {
 					}
 					$post_id = $request->get_param( 'postId' );
 					if ( is_numeric( $post_id ) && (int) $post_id > 0 ) {
-						return current_user_can( 'edit_post', (int) $post_id );
+						return self::is_email_post_type( get_post_type( (int) $post_id ) ) && current_user_can( 'edit_post', (int) $post_id );
 					}

 					/**
@@ -355,17 +361,17 @@ class Email_Editor {
 	}

 	/**
-	 * Check if the current post type is an email post type.
+	 * Check if the post type is registered for the email editor.
 	 *
-	 * @param string $current_post_type The current post type.
+	 * @param string|false $post_type The post type to check.
 	 * @return bool
 	 */
-	private function current_post_is_email_post_type( $current_post_type ): bool {
-		if ( ! $current_post_type ) {
+	public static function is_email_post_type( $post_type ): bool {
+		if ( ! $post_type ) {
 			return false;
 		}
-		$email_post_types = array_column( $this->get_post_types(), 'name' );
-		return in_array( $current_post_type, $email_post_types, true );
+		$email_post_types = array_column( self::get_post_types(), 'name' );
+		return in_array( $post_type, $email_post_types, true );
 	}

 	/**
@@ -381,7 +387,7 @@ class Email_Editor {
 			return $template;
 		}

-		if ( ! $this->current_post_is_email_post_type( $post->post_type ) ) {
+		if ( ! self::is_email_post_type( $post->post_type ) ) {
 			return $template;
 		}

@@ -413,7 +419,7 @@ class Email_Editor {
 			return $preview_link;
 		}

-		if ( ! $this->current_post_is_email_post_type( $post->post_type ) ) {
+		if ( ! self::is_email_post_type( $post->post_type ) ) {
 			return $preview_link;
 		}

diff --git a/packages/php/email-editor/src/Engine/class-send-preview-email.php b/packages/php/email-editor/src/Engine/class-send-preview-email.php
index 7fc43a45b02..2d277bab7ac 100644
--- a/packages/php/email-editor/src/Engine/class-send-preview-email.php
+++ b/packages/php/email-editor/src/Engine/class-send-preview-email.php
@@ -219,7 +219,7 @@ class Send_Preview_Email {
 	 */
 	private function fetch_post( $post_id ): \WP_Post {
 		$post = get_post( intval( $post_id ) );
-		if ( ! $post instanceof \WP_Post ) {
+		if ( ! $post instanceof \WP_Post || ! Email_Editor::is_email_post_type( $post->post_type ) ) {
 			throw new \Exception( esc_html__( 'Invalid post', 'woocommerce' ) );
 		}
 		return $post;
diff --git a/packages/php/email-editor/tests/integration/Engine/Send_Preview_Email_Permission_Test.php b/packages/php/email-editor/tests/integration/Engine/Send_Preview_Email_Permission_Test.php
index 076e101fe03..9ac53bbdefa 100644
--- a/packages/php/email-editor/tests/integration/Engine/Send_Preview_Email_Permission_Test.php
+++ b/packages/php/email-editor/tests/integration/Engine/Send_Preview_Email_Permission_Test.php
@@ -34,6 +34,20 @@ class Send_Preview_Email_Permission_Test extends \Email_Editor_Integration_Test_
 	 */
 	private const ROUTE = '/woocommerce-email-editor/v1/send_preview_email';

+	/**
+	 * Post type registered for the email editor in these tests.
+	 *
+	 * @var string
+	 */
+	private const EMAIL_POST_TYPE = 'custom_email_type';
+
+	/**
+	 * Callback to register the test email post type.
+	 *
+	 * @var callable
+	 */
+	private $post_register_callback;
+
 	/**
 	 * Creates a user and returns an integer ID.
 	 *
@@ -47,13 +61,13 @@ class Send_Preview_Email_Permission_Test extends \Email_Editor_Integration_Test_
 	}

 	/**
-	 * Creates a post and returns an integer ID.
+	 * Creates an email post and returns an integer ID.
 	 *
 	 * @param array $args Arguments for post creation.
 	 * @return int
 	 */
 	private function create_post( array $args = array() ): int {
-		$result = self::factory()->post->create( $args );
+		$result = self::factory()->post->create( array_merge( array( 'post_type' => self::EMAIL_POST_TYPE ), $args ) );
 		$this->assertIsInt( $result );
 		return $result;
 	}
@@ -63,7 +77,17 @@ class Send_Preview_Email_Permission_Test extends \Email_Editor_Integration_Test_
 	 */
 	public function setUp(): void {
 		parent::setUp();
-		$this->email_editor = $this->di_container->get( Email_Editor::class );
+		$this->email_editor           = $this->di_container->get( Email_Editor::class );
+		$this->post_register_callback = function ( $post_types ) {
+			$post_types[] = array(
+				'name' => self::EMAIL_POST_TYPE,
+				'args' => array(),
+				'meta' => array(),
+			);
+			return $post_types;
+		};
+		add_filter( 'woocommerce_email_editor_post_types', $this->post_register_callback );
+		$this->email_editor->initialize();

 		global $wp_rest_server;
 		$wp_rest_server = new \WP_REST_Server();
@@ -78,10 +102,87 @@ class Send_Preview_Email_Permission_Test extends \Email_Editor_Integration_Test_
 	 */
 	public function tearDown(): void {
 		parent::tearDown();
+		remove_filter( 'woocommerce_email_editor_post_types', $this->post_register_callback );
 		global $wp_rest_server;
 		$wp_rest_server = null;
 	}

+	/**
+	 * Test that an author can send a preview email for their own email post.
+	 */
+	public function testAuthorCanSendPreviewForOwnEmailPost(): void {
+		$author_id = $this->create_user( array( 'role' => 'author' ) );
+		$post_id   = $this->create_post( array( 'post_author' => $author_id ) );
+
+		wp_set_current_user( $author_id );
+
+		$request = new \WP_REST_Request( 'POST', self::ROUTE );
+		$request->set_body_params(
+			array(
+				'email'  => 'test@example.com',
+				'postId' => $post_id,
+			)
+		);
+
+		$response = $this->server->dispatch( $request );
+
+		$this->assertNotEquals( 403, $response->get_status(), 'Author should be allowed to send preview for own email post' );
+	}
+
+	/**
+	 * Test that an author cannot send a preview email for their own post of a non-email post type.
+	 */
+	public function testAuthorCannotSendPreviewForOwnNonEmailPost(): void {
+		$author_id = $this->create_user( array( 'role' => 'author' ) );
+		$post_id   = $this->create_post(
+			array(
+				'post_author' => $author_id,
+				'post_type'   => 'post',
+			)
+		);
+
+		wp_set_current_user( $author_id );
+
+		$request = new \WP_REST_Request( 'POST', self::ROUTE );
+		$request->set_body_params(
+			array(
+				'email'  => 'test@example.com',
+				'postId' => $post_id,
+			)
+		);
+
+		$response = $this->server->dispatch( $request );
+
+		$this->assertEquals( 403, $response->get_status(), 'Author should not be allowed to send preview for a non-email post' );
+	}
+
+	/**
+	 * Test that an admin cannot send a preview email for a post of a non-email post type.
+	 */
+	public function testAdminCannotSendPreviewForNonEmailPost(): void {
+		$admin_id = $this->create_user( array( 'role' => 'administrator' ) );
+		$post_id  = $this->create_post(
+			array(
+				'post_author' => $admin_id,
+				'post_type'   => 'post',
+			)
+		);
+
+		wp_set_current_user( $admin_id );
+
+		$request = new \WP_REST_Request( 'POST', self::ROUTE );
+		$request->set_body_params(
+			array(
+				'email'  => 'test@example.com',
+				'postId' => $post_id,
+			)
+		);
+
+		$response = $this->server->dispatch( $request );
+
+		$this->assertEquals( 403, $response->get_status(), 'Admin should not be allowed to send preview for a non-email post' );
+	}
+
 	/**
 	 * Test that an admin can send a preview email for their own post.
 	 */
diff --git a/packages/php/email-editor/tests/integration/Engine/Send_Preview_Email_Test.php b/packages/php/email-editor/tests/integration/Engine/Send_Preview_Email_Test.php
index d7031cdee48..6fe7c585f5e 100644
--- a/packages/php/email-editor/tests/integration/Engine/Send_Preview_Email_Test.php
+++ b/packages/php/email-editor/tests/integration/Engine/Send_Preview_Email_Test.php
@@ -32,12 +32,29 @@ class Send_Preview_Email_Test extends \Email_Editor_Integration_Test_Case {
 	 */
 	private $renderer_mock;

+	/**
+	 * Callback to register the test email post type.
+	 *
+	 * @var callable
+	 */
+	private $post_register_callback;
+
 	/**
 	 * Set up before each test
 	 */
 	public function setUp(): void {
 		parent::setUp();

+		$this->post_register_callback = function ( $post_types ) {
+			$post_types[] = array(
+				'name' => 'custom_email_type',
+				'args' => array(),
+				'meta' => array(),
+			);
+			return $post_types;
+		};
+		add_filter( 'woocommerce_email_editor_post_types', $this->post_register_callback );
+
 		$this->renderer_mock = $this->createMock( Renderer::class );
 		$this->renderer_mock->method( 'render' )->willReturn(
 			array(
@@ -70,6 +87,7 @@ class Send_Preview_Email_Test extends \Email_Editor_Integration_Test_Case {

 		$email_post_id = $this->factory->post->create(
 			array(
+				'post_type'    => 'custom_email_type',
 				'post_content' => '<!-- wp:button --><div class="wp-block-button"><a class="wp-block-button__link has-background wp-element-button">Button</a></div><!-- /wp:button -->',
 			)
 		);
@@ -104,6 +122,7 @@ class Send_Preview_Email_Test extends \Email_Editor_Integration_Test_Case {

 		$email_post_id = $this->factory->post->create(
 			array(
+				'post_type'    => 'custom_email_type',
 				'post_content' => '<!-- wp:button --><div class="wp-block-button"><a class="wp-block-button__link has-background wp-element-button">Button</a></div><!-- /wp:button -->',
 			)
 		);
@@ -188,6 +207,22 @@ class Send_Preview_Email_Test extends \Email_Editor_Integration_Test_Case {
 		$this->send_preview_email->send_preview_email( $post_data );
 	}

+	/**
+	 * Test it throws an exception when the post is not of an email post type
+	 */
+	public function testItThrowsAnExceptionWhenPostIsNotAnEmailPostType(): void {
+		$post_id = $this->factory->post->create( array( 'post_type' => 'post' ) );
+		$this->assertIsInt( $post_id );
+
+		$this->expectException( \Exception::class );
+		$this->expectExceptionMessage( 'Invalid post' );
+		$post_data = array(
+			'email'  => 'hello@example.com',
+			'postId' => $post_id,
+		);
+		$this->send_preview_email->send_preview_email( $post_data );
+	}
+
 	/**
 	 * Test it throws an exception when the post cannot be found
 	 */
@@ -201,4 +236,12 @@ class Send_Preview_Email_Test extends \Email_Editor_Integration_Test_Case {
 		);
 		$this->send_preview_email->send_preview_email( $post_data );
 	}
+
+	/**
+	 * Clean up after each test
+	 */
+	public function tearDown(): void {
+		remove_filter( 'woocommerce_email_editor_post_types', $this->post_register_callback );
+		parent::tearDown();
+	}
 }