Skip to content

Commit d6cb024

Browse files
vraja-proclaude
andcommitted
fix(capabilities): prevent authors from writing advanced/schema meta fields
allow_custom_field_edits() was granting edit_post_meta for every _yoast_wpseo_* key to any user who can edit the post, including advanced and schema fields. When map_meta_cap() added 'edit_post_meta' to the required caps because the auth_callback returned false, this function immediately satisfied that requirement, bypassing the restriction. The fix withholds the grant for advanced/schema fields when disableadvanced_meta is on and the user lacks wpseo_edit_advanced_metadata, closing the vector for both REST API and XML-RPC access paths. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 282de5c commit d6cb024

2 files changed

Lines changed: 234 additions & 3 deletions

File tree

inc/wpseo-non-ajax-functions.php

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,21 @@ function allow_custom_field_edits( $required_capabilities, $capabilities, $args
4646
return $required_capabilities;
4747
}
4848

49-
// If the meta key is part of the plugin, grant capabilities accordingly.
50-
if ( strpos( $args[3], WPSEO_Meta::$meta_prefix ) === 0 && current_user_can( 'edit_post', $args[2] ) ) {
51-
$required_capabilities[ $args[0] ] = true;
49+
// If the meta key is not part of the plugin, or the user cannot edit the post, bail.
50+
if ( strpos( $args[3], WPSEO_Meta::$meta_prefix ) !== 0 || ! current_user_can( 'edit_post', $args[2] ) ) {
51+
return $required_capabilities;
52+
}
53+
54+
// When advanced/schema fields are restricted, do not grant write access to them for users lacking the capability.
55+
if ( WPSEO_Options::get( 'disableadvanced_meta' ) && ! WPSEO_Capability_Utils::current_user_can( 'wpseo_edit_advanced_metadata' ) ) {
56+
$field_info = WPSEO_Meta::$fields_index[ $args[3] ] ?? null;
57+
if ( $field_info && in_array( $field_info['subset'], [ 'advanced', 'schema' ], true ) ) {
58+
return $required_capabilities;
59+
}
5260
}
5361

62+
$required_capabilities[ $args[0] ] = true;
63+
5464
return $required_capabilities;
5565
}
5666

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
<?php
2+
3+
namespace Yoast\WP\SEO\Tests\WP\Inc;
4+
5+
use WPSEO_Meta;
6+
use WPSEO_Options;
7+
use Yoast\WP\SEO\Tests\WP\TestCase;
8+
9+
/**
10+
* Integration tests for allow_custom_field_edits() in inc/wpseo-non-ajax-functions.php.
11+
*
12+
* @coversNothing
13+
*
14+
* @group inc
15+
*/
16+
final class Non_Ajax_Functions_Test extends TestCase {
17+
18+
/**
19+
* The author user ID.
20+
*
21+
* @var int
22+
*/
23+
private $author_id;
24+
25+
/**
26+
* The post ID owned by the author.
27+
*
28+
* @var int
29+
*/
30+
private $post_id;
31+
32+
/**
33+
* Sets up test fixtures.
34+
*
35+
* @return void
36+
*/
37+
public function set_up() {
38+
parent::set_up();
39+
40+
$this->author_id = $this->factory->user->create( [ 'role' => 'author' ] );
41+
$this->post_id = $this->factory->post->create( [ 'post_author' => $this->author_id ] );
42+
43+
$this->set_disable_advanced_meta( true );
44+
}
45+
46+
/**
47+
* Restores the disableadvanced_meta option to its default after each test.
48+
*
49+
* @return void
50+
*/
51+
public function tear_down() {
52+
$this->set_disable_advanced_meta( true );
53+
parent::tear_down();
54+
}
55+
56+
// -------------------------------------------------------------------------
57+
// Advanced fields — restricted
58+
// -------------------------------------------------------------------------
59+
60+
/**
61+
* Tests that an author cannot get edit_post_meta for a noindex field when
62+
* disableadvanced_meta is on (the default).
63+
*
64+
* This is the core regression: allow_custom_field_edits must not grant
65+
* edit_post_meta for advanced/schema fields to users who lack
66+
* wpseo_edit_advanced_metadata.
67+
*
68+
* @return void
69+
*/
70+
public function test_author_cannot_edit_advanced_field_when_advanced_meta_restricted() {
71+
\wp_set_current_user( $this->author_id );
72+
73+
$result = \current_user_can(
74+
'edit_post_meta',
75+
$this->post_id,
76+
WPSEO_Meta::$meta_prefix . 'meta-robots-noindex',
77+
);
78+
79+
$this->assertFalse( $result );
80+
}
81+
82+
/**
83+
* Tests that an author cannot get edit_post_meta for a canonical URL field
84+
* when disableadvanced_meta is on.
85+
*
86+
* @return void
87+
*/
88+
public function test_author_cannot_edit_canonical_field_when_advanced_meta_restricted() {
89+
\wp_set_current_user( $this->author_id );
90+
91+
$result = \current_user_can(
92+
'edit_post_meta',
93+
$this->post_id,
94+
WPSEO_Meta::$meta_prefix . 'canonical',
95+
);
96+
97+
$this->assertFalse( $result );
98+
}
99+
100+
/**
101+
* Tests that an author cannot get edit_post_meta for a schema field when
102+
* disableadvanced_meta is on.
103+
*
104+
* @return void
105+
*/
106+
public function test_author_cannot_edit_schema_field_when_advanced_meta_restricted() {
107+
\wp_set_current_user( $this->author_id );
108+
109+
$result = \current_user_can(
110+
'edit_post_meta',
111+
$this->post_id,
112+
WPSEO_Meta::$meta_prefix . 'schema_page_type',
113+
);
114+
115+
$this->assertFalse( $result );
116+
}
117+
118+
// -------------------------------------------------------------------------
119+
// General fields — always writable by authors who can edit the post
120+
// -------------------------------------------------------------------------
121+
122+
/**
123+
* Tests that an author can still get edit_post_meta for a general-subset field
124+
* (title) even when disableadvanced_meta is on.
125+
*
126+
* @return void
127+
*/
128+
public function test_author_can_edit_general_field_when_advanced_meta_restricted() {
129+
\wp_set_current_user( $this->author_id );
130+
131+
$result = \current_user_can(
132+
'edit_post_meta',
133+
$this->post_id,
134+
WPSEO_Meta::$meta_prefix . 'title',
135+
);
136+
137+
$this->assertTrue( $result );
138+
}
139+
140+
/**
141+
* Tests that an author can still get edit_post_meta for a meta description field
142+
* even when disableadvanced_meta is on.
143+
*
144+
* @return void
145+
*/
146+
public function test_author_can_edit_metadesc_field_when_advanced_meta_restricted() {
147+
\wp_set_current_user( $this->author_id );
148+
149+
$result = \current_user_can(
150+
'edit_post_meta',
151+
$this->post_id,
152+
WPSEO_Meta::$meta_prefix . 'metadesc',
153+
);
154+
155+
$this->assertTrue( $result );
156+
}
157+
158+
// -------------------------------------------------------------------------
159+
// Advanced fields — unrestricted
160+
// -------------------------------------------------------------------------
161+
162+
/**
163+
* Tests that an author can get edit_post_meta for a noindex field when
164+
* disableadvanced_meta is off.
165+
*
166+
* @return void
167+
*/
168+
public function test_author_can_edit_advanced_field_when_advanced_meta_not_restricted() {
169+
$this->set_disable_advanced_meta( false );
170+
\wp_set_current_user( $this->author_id );
171+
172+
$result = \current_user_can(
173+
'edit_post_meta',
174+
$this->post_id,
175+
WPSEO_Meta::$meta_prefix . 'meta-robots-noindex',
176+
);
177+
178+
$this->assertTrue( $result );
179+
}
180+
181+
// -------------------------------------------------------------------------
182+
// Editor — always has access to advanced fields
183+
// -------------------------------------------------------------------------
184+
185+
/**
186+
* Tests that an editor can get edit_post_meta for a noindex field even when
187+
* disableadvanced_meta is on, because editors have wpseo_edit_advanced_metadata.
188+
*
189+
* @return void
190+
*/
191+
public function test_editor_can_edit_advanced_field_when_advanced_meta_restricted() {
192+
$editor_id = $this->factory->user->create( [ 'role' => 'editor' ] );
193+
\wp_set_current_user( $editor_id );
194+
195+
$result = \current_user_can(
196+
'edit_post_meta',
197+
$this->post_id,
198+
WPSEO_Meta::$meta_prefix . 'meta-robots-noindex',
199+
);
200+
201+
$this->assertTrue( $result );
202+
}
203+
204+
// -------------------------------------------------------------------------
205+
// Helpers
206+
// -------------------------------------------------------------------------
207+
208+
/**
209+
* Sets the disableadvanced_meta option and clears the WPSEO_Options cache.
210+
*
211+
* @param bool $value The value to set.
212+
*
213+
* @return void
214+
*/
215+
private function set_disable_advanced_meta( bool $value ): void {
216+
$wpseo = \get_option( 'wpseo', [] );
217+
$wpseo['disableadvanced_meta'] = $value;
218+
\update_option( 'wpseo', $wpseo );
219+
WPSEO_Options::clear_cache();
220+
}
221+
}

0 commit comments

Comments
 (0)