Skip to content

Commit 04afd90

Browse files
committed
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 #60651. git-svn-id: https://develop.svn.wordpress.org/trunk@57754 602fd350-edb4-49c9-b593-d223f7449a82
1 parent 5bf25d8 commit 04afd90

3 files changed

Lines changed: 318 additions & 0 deletions

File tree

src/wp-includes/block-bindings/post-meta.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,19 @@ function _block_bindings_post_meta_get_value( array $source_args, $block_instanc
3434
return null;
3535
}
3636

37+
// Check if the meta field is protected.
38+
if ( is_protected_meta( $source_args['key'], 'post' ) ) {
39+
return null;
40+
}
41+
42+
// Check if the meta field is registered to be shown in REST.
43+
$meta_keys = get_registered_meta_keys( 'post', $block_instance->context['postType'] );
44+
// Add fields registered for all subtypes.
45+
$meta_keys = array_merge( $meta_keys, get_registered_meta_keys( 'post', '' ) );
46+
if ( empty( $meta_keys[ $source_args['key'] ]['show_in_rest'] ) ) {
47+
return null;
48+
}
49+
3750
return get_post_meta( $post_id, $source_args['key'], true );
3851
}
3952

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
<?php
2+
/**
3+
* Tests for Block Bindings API "core/post-meta" source.
4+
*
5+
* @package WordPress
6+
* @subpackage Blocks
7+
* @since 6.5.0
8+
*
9+
* @group blocks
10+
* @group block-bindings
11+
*/
12+
class Tests_Block_Bindings_Post_Meta_Source extends WP_UnitTestCase {
13+
protected static $post;
14+
protected static $wp_meta_keys_saved;
15+
16+
/**
17+
* Modify the post content.
18+
*
19+
* @param string $content The new content.
20+
*/
21+
private function get_modified_post_content( $content ) {
22+
$GLOBALS['post']->post_content = $content;
23+
return apply_filters( 'the_content', $GLOBALS['post']->post_content );
24+
}
25+
26+
/**
27+
* Sets up shared fixtures.
28+
*/
29+
public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) {
30+
self::$post = $factory->post->create_and_get();
31+
self::$wp_meta_keys_saved = isset( $GLOBALS['wp_meta_keys'] ) ? $GLOBALS['wp_meta_keys'] : array();
32+
}
33+
34+
/**
35+
* Tear down after class.
36+
*/
37+
public static function wpTearDownAfterClass() {
38+
$GLOBALS['wp_meta_keys'] = self::$wp_meta_keys_saved;
39+
}
40+
41+
/**
42+
* Set up before each test.
43+
*
44+
* @since 6.5.0
45+
*/
46+
public function set_up() {
47+
parent::set_up();
48+
// Needed because tear_down() will reset it between tests.
49+
$GLOBALS['post'] = self::$post;
50+
}
51+
52+
/**
53+
* Tests that a block connected to a custom field renders its value.
54+
*
55+
* @ticket 60651
56+
*/
57+
public function test_custom_field_value_is_rendered() {
58+
register_meta(
59+
'post',
60+
'tests_custom_field',
61+
array(
62+
'show_in_rest' => true,
63+
'single' => true,
64+
'type' => 'string',
65+
'default' => 'Custom field value',
66+
)
67+
);
68+
69+
$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 -->' );
70+
$this->assertSame(
71+
'<p>Custom field value</p>',
72+
$content,
73+
'The post content should show the value of the custom field . '
74+
);
75+
}
76+
77+
/**
78+
* Tests that an html attribute connected to a custom field renders its value.
79+
*
80+
* @ticket 60651
81+
*/
82+
public function test_html_attribute_connected_to_custom_field_value_is_rendered() {
83+
register_meta(
84+
'post',
85+
'tests_url_custom_field',
86+
array(
87+
'show_in_rest' => true,
88+
'single' => true,
89+
'type' => 'string',
90+
'default' => 'https://example.com/foo.png',
91+
)
92+
);
93+
94+
$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 -->' );
95+
$this->assertSame(
96+
'<figure class="wp-block-image"><img decoding="async" src="https://example.com/foo.png" alt=""/></figure>',
97+
$content,
98+
'The image src should point to the value of the custom field . '
99+
);
100+
}
101+
102+
/**
103+
* Tests that a blocks connected in a password protected post don't render the value.
104+
*
105+
* @ticket 60651
106+
*/
107+
public function test_custom_field_value_is_not_shown_in_password_protected_posts() {
108+
register_meta(
109+
'post',
110+
'tests_custom_field',
111+
array(
112+
'show_in_rest' => true,
113+
'single' => true,
114+
'type' => 'string',
115+
'default' => 'Custom field value',
116+
)
117+
);
118+
119+
add_filter( 'post_password_required', '__return_true' );
120+
121+
$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 -->' );
122+
123+
remove_filter( 'post_password_required', '__return_true' );
124+
125+
$this->assertSame(
126+
'<p>Fallback value</p>',
127+
$content,
128+
'The post content should show the fallback value instead of the custom field value.'
129+
);
130+
}
131+
132+
/**
133+
* Tests that a blocks connected in a post that is not publicly viewable don't render the value.
134+
*
135+
* @ticket 60651
136+
*/
137+
public function test_custom_field_value_is_not_shown_in_non_viewable_posts() {
138+
register_meta(
139+
'post',
140+
'tests_custom_field',
141+
array(
142+
'show_in_rest' => true,
143+
'single' => true,
144+
'type' => 'string',
145+
'default' => 'Custom field value',
146+
)
147+
);
148+
149+
add_filter( 'is_post_status_viewable', '__return_false' );
150+
151+
$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 -->' );
152+
153+
remove_filter( 'is_post_status_viewable', '__return_false' );
154+
155+
$this->assertSame(
156+
'<p>Fallback value</p>',
157+
$content,
158+
'The post content should show the fallback value instead of the custom field value.'
159+
);
160+
}
161+
162+
/**
163+
* Tests that a block connected to a meta key that doesn't exist renders the fallback.
164+
*
165+
* @ticket 60651
166+
*/
167+
public function test_binding_to_non_existing_meta_key() {
168+
$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 -->' );
169+
170+
$this->assertSame(
171+
'<p>Fallback value</p>',
172+
$content,
173+
'The post content should show the fallback value.'
174+
);
175+
}
176+
177+
/**
178+
* Tests that a block connected without specifying the custom field renders the fallback.
179+
*
180+
* @ticket 60651
181+
*/
182+
public function test_binding_without_key_renders_the_fallback() {
183+
$content = $this->get_modified_post_content( '<!-- wp:paragraph {"metadata":{"bindings":{"content":{"source":"core/post-meta"}}}} --><p>Fallback value</p><!-- /wp:paragraph -->' );
184+
185+
$this->assertSame(
186+
'<p>Fallback value</p>',
187+
$content,
188+
'The post content should show the fallback value.'
189+
);
190+
}
191+
192+
/**
193+
* Tests that a block connected to a protected field doesn't show the value.
194+
*
195+
* @ticket 60651
196+
*/
197+
public function test_protected_field_value_is_not_shown() {
198+
register_meta(
199+
'post',
200+
'_tests_protected_field',
201+
array(
202+
'show_in_rest' => true,
203+
'single' => true,
204+
'type' => 'string',
205+
'default' => 'Protected value',
206+
)
207+
);
208+
209+
$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 -->' );
210+
211+
$this->assertSame(
212+
'<p>Fallback value</p>',
213+
$content,
214+
'The post content should show the fallback value instead of the protected value.'
215+
);
216+
}
217+
218+
/**
219+
* Tests that a block connected to a field not exposed in the REST API doesn't show the value.
220+
*
221+
* @ticket 60651
222+
*/
223+
public function test_custom_field_not_exposed_in_rest_api_is_not_shown() {
224+
register_meta(
225+
'post',
226+
'tests_show_in_rest_false_field',
227+
array(
228+
'show_in_rest' => false,
229+
'single' => true,
230+
'type' => 'string',
231+
'default' => 'Protected value',
232+
)
233+
);
234+
235+
$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 -->' );
236+
237+
$this->assertSame(
238+
'<p>Fallback value</p>',
239+
$content,
240+
'The post content should show the fallback value instead of the protected value.'
241+
);
242+
}
243+
244+
/**
245+
* Tests that meta key with unsafe HTML is sanitized.
246+
*
247+
* @ticket 60651
248+
*/
249+
public function test_custom_field_with_unsafe_html_is_sanitized() {
250+
register_meta(
251+
'post',
252+
'tests_unsafe_html_field',
253+
array(
254+
'show_in_rest' => true,
255+
'single' => true,
256+
'type' => 'string',
257+
'default' => '<script>alert("Unsafe HTML")</script>',
258+
)
259+
);
260+
261+
$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 -->' );
262+
263+
$this->assertSame(
264+
'<p>alert(&#8220;Unsafe HTML&#8221;)</p>',
265+
$content,
266+
'The post content should not include the script tag.'
267+
);
268+
}
269+
}

tests/phpunit/tests/block-bindings/render.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,4 +198,40 @@ public function test_update_block_with_value_from_source_image_placeholder() {
198198
'The block content should be updated with the value returned by the source.'
199199
);
200200
}
201+
202+
/**
203+
* Tests if the block content is sanitized when unsafe HTML is passed.
204+
*
205+
* @ticket 60651
206+
*
207+
* @covers ::register_block_bindings_source
208+
*/
209+
public function test_source_value_with_unsafe_html_is_sanitized() {
210+
$get_value_callback = function () {
211+
return '<script>alert("Unsafe HTML")</script>';
212+
};
213+
214+
register_block_bindings_source(
215+
self::SOURCE_NAME,
216+
array(
217+
'label' => self::SOURCE_LABEL,
218+
'get_value_callback' => $get_value_callback,
219+
)
220+
);
221+
222+
$block_content = <<<HTML
223+
<!-- wp:paragraph {"metadata":{"bindings":{"content":{"source":"test/source"}}}} -->
224+
<p>This should not appear</p>
225+
<!-- /wp:paragraph -->
226+
HTML;
227+
$parsed_blocks = parse_blocks( $block_content );
228+
$block = new WP_Block( $parsed_blocks[0] );
229+
$result = $block->render();
230+
231+
$this->assertSame(
232+
'<p>alert("Unsafe HTML")</p>',
233+
trim( $result ),
234+
'The block content should be updated with the value returned by the source.'
235+
);
236+
}
201237
}

0 commit comments

Comments
 (0)