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();
+ }
}