<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[57754] trunk: Editor: do not expose protected post meta fields in block bindings.</title>
</head>
<body>
<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; }
#msg dl a { font-weight: bold}
#msg dl a:link { color:#fc3; }
#msg dl a:active { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { white-space: pre-line; overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="https://core.trac.wordpress.org/changeset/57754">57754</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"https://core.trac.wordpress.org/changeset/57754","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>swissspidy</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2024-03-02 14:11:53 +0000 (Sat, 02 Mar 2024)</dd>
</dl>
<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>Editor: do not expose protected post meta fields in block bindings.
Ignores meta keys which are considered protected or not registered to be shown in the REST API. Adds tests.
Props santosguillamot, swissspidy, gziolo, xknown, peterwilsoncc.
Fixes <a href="https://core.trac.wordpress.org/ticket/60651">#60651</a>.</pre>
<h3>Modified Paths</h3>
<ul>
<li><a href="#trunksrcwpincludesblockbindingspostmetaphp">trunk/src/wp-includes/block-bindings/post-meta.php</a></li>
<li><a href="#trunktestsphpunittestsblockbindingsrenderphp">trunk/tests/phpunit/tests/block-bindings/render.php</a></li>
</ul>
<h3>Added Paths</h3>
<ul>
<li><a href="#trunktestsphpunittestsblockbindingspostMetaSourcephp">trunk/tests/phpunit/tests/block-bindings/postMetaSource.php</a></li>
</ul>
</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunksrcwpincludesblockbindingspostmetaphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/block-bindings/post-meta.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/block-bindings/post-meta.php 2024-03-02 14:05:38 UTC (rev 57753)
+++ trunk/src/wp-includes/block-bindings/post-meta.php 2024-03-02 14:11:53 UTC (rev 57754)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -34,6 +34,19 @@
</span><span class="cx" style="display: block; padding: 0 10px"> return null;
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ // Check if the meta field is protected.
+ if ( is_protected_meta( $source_args['key'], 'post' ) ) {
+ return null;
+ }
+
+ // Check if the meta field is registered to be shown in REST.
+ $meta_keys = get_registered_meta_keys( 'post', $block_instance->context['postType'] );
+ // Add fields registered for all subtypes.
+ $meta_keys = array_merge( $meta_keys, get_registered_meta_keys( 'post', '' ) );
+ if ( empty( $meta_keys[ $source_args['key'] ]['show_in_rest'] ) ) {
+ return null;
+ }
+
</ins><span class="cx" style="display: block; padding: 0 10px"> return get_post_meta( $post_id, $source_args['key'], true );
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px">
</span></span></pre></div>
<a id="trunktestsphpunittestsblockbindingspostMetaSourcephp"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: trunk/tests/phpunit/tests/block-bindings/postMetaSource.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/block-bindings/postMetaSource.php (rev 0)
+++ trunk/tests/phpunit/tests/block-bindings/postMetaSource.php 2024-03-02 14:11:53 UTC (rev 57754)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -0,0 +1,269 @@
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+<?php
+/**
+ * Tests for Block Bindings API "core/post-meta" source.
+ *
+ * @package WordPress
+ * @subpackage Blocks
+ * @since 6.5.0
+ *
+ * @group blocks
+ * @group block-bindings
+ */
+class Tests_Block_Bindings_Post_Meta_Source extends WP_UnitTestCase {
+ protected static $post;
+ protected static $wp_meta_keys_saved;
+
+ /**
+ * Modify the post content.
+ *
+ * @param string $content The new content.
+ */
+ private function get_modified_post_content( $content ) {
+ $GLOBALS['post']->post_content = $content;
+ return apply_filters( 'the_content', $GLOBALS['post']->post_content );
+ }
+
+ /**
+ * Sets up shared fixtures.
+ */
+ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) {
+ self::$post = $factory->post->create_and_get();
+ self::$wp_meta_keys_saved = isset( $GLOBALS['wp_meta_keys'] ) ? $GLOBALS['wp_meta_keys'] : array();
+ }
+
+ /**
+ * Tear down after class.
+ */
+ public static function wpTearDownAfterClass() {
+ $GLOBALS['wp_meta_keys'] = self::$wp_meta_keys_saved;
+ }
+
+ /**
+ * Set up before each test.
+ *
+ * @since 6.5.0
+ */
+ public function set_up() {
+ parent::set_up();
+ // Needed because tear_down() will reset it between tests.
+ $GLOBALS['post'] = self::$post;
+ }
+
+ /**
+ * Tests that a block connected to a custom field renders its value.
+ *
+ * @ticket 60651
+ */
+ public function test_custom_field_value_is_rendered() {
+ register_meta(
+ 'post',
+ 'tests_custom_field',
+ array(
+ 'show_in_rest' => true,
+ 'single' => true,
+ 'type' => 'string',
+ 'default' => 'Custom field value',
+ )
+ );
+
+ $content = $this->get_modified_post_content( '<!-- wp:paragraph {"metadata":{"bindings":{"content":{"source":"core/post-meta","args":{"key":"tests_custom_field"}}}}} --><p>Fallback value</p><!-- /wp:paragraph -->' );
+ $this->assertSame(
+ '<p>Custom field value</p>',
+ $content,
+ 'The post content should show the value of the custom field . '
+ );
+ }
+
+ /**
+ * Tests that an html attribute connected to a custom field renders its value.
+ *
+ * @ticket 60651
+ */
+ public function test_html_attribute_connected_to_custom_field_value_is_rendered() {
+ register_meta(
+ 'post',
+ 'tests_url_custom_field',
+ array(
+ 'show_in_rest' => true,
+ 'single' => true,
+ 'type' => 'string',
+ 'default' => 'https://example.com/foo.png',
+ )
+ );
+
+ $content = $this->get_modified_post_content( '<!-- wp:image {"metadata":{"bindings":{"url":{"source":"core/post-meta","args":{"key":"tests_url_custom_field"}}}}} --><figure class="wp-block-image"><img alt=""/></figure><!-- /wp:image -->' );
+ $this->assertSame(
+ '<figure class="wp-block-image"><img decoding="async" src="https://example.com/foo.png" alt=""/></figure>',
+ $content,
+ 'The image src should point to the value of the custom field . '
+ );
+ }
+
+ /**
+ * Tests that a blocks connected in a password protected post don't render the value.
+ *
+ * @ticket 60651
+ */
+ public function test_custom_field_value_is_not_shown_in_password_protected_posts() {
+ register_meta(
+ 'post',
+ 'tests_custom_field',
+ array(
+ 'show_in_rest' => true,
+ 'single' => true,
+ 'type' => 'string',
+ 'default' => 'Custom field value',
+ )
+ );
+
+ add_filter( 'post_password_required', '__return_true' );
+
+ $content = $this->get_modified_post_content( '<!-- wp:paragraph {"metadata":{"bindings":{"content":{"source":"core/post-meta","args":{"key":"tests_custom_field"}}}}} --><p>Fallback value</p><!-- /wp:paragraph -->' );
+
+ remove_filter( 'post_password_required', '__return_true' );
+
+ $this->assertSame(
+ '<p>Fallback value</p>',
+ $content,
+ 'The post content should show the fallback value instead of the custom field value.'
+ );
+ }
+
+ /**
+ * Tests that a blocks connected in a post that is not publicly viewable don't render the value.
+ *
+ * @ticket 60651
+ */
+ public function test_custom_field_value_is_not_shown_in_non_viewable_posts() {
+ register_meta(
+ 'post',
+ 'tests_custom_field',
+ array(
+ 'show_in_rest' => true,
+ 'single' => true,
+ 'type' => 'string',
+ 'default' => 'Custom field value',
+ )
+ );
+
+ add_filter( 'is_post_status_viewable', '__return_false' );
+
+ $content = $this->get_modified_post_content( '<!-- wp:paragraph {"metadata":{"bindings":{"content":{"source":"core/post-meta","args":{"key":"tests_custom_field"}}}}} --><p>Fallback value</p><!-- /wp:paragraph -->' );
+
+ remove_filter( 'is_post_status_viewable', '__return_false' );
+
+ $this->assertSame(
+ '<p>Fallback value</p>',
+ $content,
+ 'The post content should show the fallback value instead of the custom field value.'
+ );
+ }
+
+ /**
+ * Tests that a block connected to a meta key that doesn't exist renders the fallback.
+ *
+ * @ticket 60651
+ */
+ public function test_binding_to_non_existing_meta_key() {
+ $content = $this->get_modified_post_content( '<!-- wp:paragraph {"metadata":{"bindings":{"content":{"source":"core/post-meta","args":{"key":"tests_non_existing_field"}}}}} --><p>Fallback value</p><!-- /wp:paragraph -->' );
+
+ $this->assertSame(
+ '<p>Fallback value</p>',
+ $content,
+ 'The post content should show the fallback value.'
+ );
+ }
+
+ /**
+ * Tests that a block connected without specifying the custom field renders the fallback.
+ *
+ * @ticket 60651
+ */
+ public function test_binding_without_key_renders_the_fallback() {
+ $content = $this->get_modified_post_content( '<!-- wp:paragraph {"metadata":{"bindings":{"content":{"source":"core/post-meta"}}}} --><p>Fallback value</p><!-- /wp:paragraph -->' );
+
+ $this->assertSame(
+ '<p>Fallback value</p>',
+ $content,
+ 'The post content should show the fallback value.'
+ );
+ }
+
+ /**
+ * Tests that a block connected to a protected field doesn't show the value.
+ *
+ * @ticket 60651
+ */
+ public function test_protected_field_value_is_not_shown() {
+ register_meta(
+ 'post',
+ '_tests_protected_field',
+ array(
+ 'show_in_rest' => true,
+ 'single' => true,
+ 'type' => 'string',
+ 'default' => 'Protected value',
+ )
+ );
+
+ $content = $this->get_modified_post_content( '<!-- wp:paragraph {"metadata":{"bindings":{"content":{"source":"core/post-meta","args":{"key":"_tests_protected_field"}}}}} --><p>Fallback value</p><!-- /wp:paragraph -->' );
+
+ $this->assertSame(
+ '<p>Fallback value</p>',
+ $content,
+ 'The post content should show the fallback value instead of the protected value.'
+ );
+ }
+
+ /**
+ * Tests that a block connected to a field not exposed in the REST API doesn't show the value.
+ *
+ * @ticket 60651
+ */
+ public function test_custom_field_not_exposed_in_rest_api_is_not_shown() {
+ register_meta(
+ 'post',
+ 'tests_show_in_rest_false_field',
+ array(
+ 'show_in_rest' => false,
+ 'single' => true,
+ 'type' => 'string',
+ 'default' => 'Protected value',
+ )
+ );
+
+ $content = $this->get_modified_post_content( '<!-- wp:paragraph {"metadata":{"bindings":{"content":{"source":"core/post-meta","args":{"key":"tests_show_in_rest_false_field"}}}}} --><p>Fallback value</p><!-- /wp:paragraph -->' );
+
+ $this->assertSame(
+ '<p>Fallback value</p>',
+ $content,
+ 'The post content should show the fallback value instead of the protected value.'
+ );
+ }
+
+ /**
+ * Tests that meta key with unsafe HTML is sanitized.
+ *
+ * @ticket 60651
+ */
+ public function test_custom_field_with_unsafe_html_is_sanitized() {
+ register_meta(
+ 'post',
+ 'tests_unsafe_html_field',
+ array(
+ 'show_in_rest' => true,
+ 'single' => true,
+ 'type' => 'string',
+ 'default' => '<script>alert("Unsafe HTML")</script>',
+ )
+ );
+
+ $content = $this->get_modified_post_content( '<!-- wp:paragraph {"metadata":{"bindings":{"content":{"source":"core/post-meta","args":{"key":"tests_unsafe_html_field"}}}}} --><p>Fallback value</p><!-- /wp:paragraph -->' );
+
+ $this->assertSame(
+ '<p>alert(“Unsafe HTML”)</p>',
+ $content,
+ 'The post content should not include the script tag.'
+ );
+ }
+}
</ins><span class="cx" style="display: block; padding: 0 10px">Property changes on: trunk/tests/phpunit/tests/block-bindings/postMetaSource.php
</span><span class="cx" style="display: block; padding: 0 10px">___________________________________________________________________
</span></span></pre></div>
<a id="svneolstyle"></a>
<div class="addfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Added: svn:eol-style</h4></div>
<ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+native
</ins><span class="cx" style="display: block; padding: 0 10px">\ No newline at end of property
</span><a id="trunktestsphpunittestsblockbindingsrenderphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/phpunit/tests/block-bindings/render.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/block-bindings/render.php 2024-03-02 14:05:38 UTC (rev 57753)
+++ trunk/tests/phpunit/tests/block-bindings/render.php 2024-03-02 14:11:53 UTC (rev 57754)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -198,4 +198,40 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 'The block content should be updated with the value returned by the source.'
</span><span class="cx" style="display: block; padding: 0 10px"> );
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+ /**
+ * Tests if the block content is sanitized when unsafe HTML is passed.
+ *
+ * @ticket 60651
+ *
+ * @covers ::register_block_bindings_source
+ */
+ public function test_source_value_with_unsafe_html_is_sanitized() {
+ $get_value_callback = function () {
+ return '<script>alert("Unsafe HTML")</script>';
+ };
+
+ register_block_bindings_source(
+ self::SOURCE_NAME,
+ array(
+ 'label' => self::SOURCE_LABEL,
+ 'get_value_callback' => $get_value_callback,
+ )
+ );
+
+ $block_content = <<<HTML
+<!-- wp:paragraph {"metadata":{"bindings":{"content":{"source":"test/source"}}}} -->
+<p>This should not appear</p>
+<!-- /wp:paragraph -->
+HTML;
+ $parsed_blocks = parse_blocks( $block_content );
+ $block = new WP_Block( $parsed_blocks[0] );
+ $result = $block->render();
+
+ $this->assertSame(
+ '<p>alert("Unsafe HTML")</p>',
+ trim( $result ),
+ 'The block content should be updated with the value returned by the source.'
+ );
+ }
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span></span></pre>
</div>
</div>
</body>
</html>