The Exploit
An unauthenticated attacker can modify or delete arbitrary user and term metadata by sending a REST API request without any authentication header.
POST /wp-json/rank-math/v1/updateMeta HTTP/1.1
Host: target.wordpress.local
Content-Type: application/json
{
"objectID": "1",
"objectType": "user",
"metaKey": "wp_user_level",
"metaValue": ""
}
The attacker observes a 200 OK response with {"success": true}, and the targeted administrator user's wp_user_level metadata is deleted. On the administrator's next login attempt, they are locked out of the dashboard because WordPress cannot verify their role.
What the Patch Did
Before:
public static function get_user_permissions_check( $request ) {
return Helper::get_settings( 'titles.author_add_meta_box' );
}
After:
public static function get_user_permissions_check( $request ) {
$user_id = $request->get_param( 'objectID' );
return current_user_can( 'edit_user', $user_id ) && Helper::get_settings( 'titles.author_add_meta_box' );
}
The patch added a current_user_can( 'edit_user', $user_id ) capability check. This WordPress capability gate prevents unauthorized users from modifying user metadata by verifying that the caller holds the edit_user permission for the specific target user ID extracted from the objectID request parameter. Without this check, the endpoint only consulted a plugin setting and performed no authentication or authorization, making metadata modification available to any caller.
A parallel fix was applied to term metadata:
Before:
public static function get_term_permissions_check( $request ) {
$term = self::get_term( $request->get_param( 'objectID' ) );
if ( is_wp_error( $term ) ) {
return $term;
}
if ( ! in_array( $term->taxonomy, array_keys( Helper::get_accessible_taxonomies() ), true ) ) {
return new WP_Error( /* ... */ );
After:
public static function get_term_permissions_check( $request ) {
$term_id = $request->get_param( 'objectID' );
$term = self::get_term( $term_id );
if ( is_wp_error( $term ) ) {
return $term;
}
if (
! in_array( $term->taxonomy, array_keys( Helper::get_accessible_taxonomies() ), true ) ||
! current_user_can( get_taxonomy( $term->taxonomy )->cap->edit_terms, $term_id )
) {
return new WP_Error( /* ... */ );
The term metadata endpoint was strengthened by adding a dynamic capability check using get_taxonomy()->cap->edit_terms with the term ID, ensuring the caller holds the appropriate taxonomy-specific permission before allowing edits.
Root Cause
CWE-862: Missing Authorization. The vulnerability exists because the REST API endpoints that handle user and term metadata updates (get_user_permissions_check and get_term_permissions_check) perform no authorization verification. The user metadata endpoint checks only a plugin setting (titles.author_add_meta_box), which does not authenticate or authorize the request. The objectID parameter, extracted via $request->get_param( 'objectID' ), flows directly into metadata operations without passing through WordPress capability checks. This allows an unauthenticated attacker to specify any user ID or term ID and modify or delete associated metadata. The trust boundary between the unauthenticated HTTP layer and the privileged metadata store is crossed without validation.
Why It Works
The load-bearing line is the current_user_can() call paired with the specific capability and object ID: current_user_can( 'edit_user', $user_id ). If this single line were removed, the vulnerability would remain fully exploitable—an attacker could still delete administrator metadata by only checking the plugin setting. The engineer also extracted the objectID parameter into a named variable ($user_id) before passing it to current_user_can(), which clarifies intent and ensures the ID is available for the capability check. For term metadata, the addition of the taxonomy-specific capability gate (get_taxonomy( $term->taxonomy )->cap->edit_terms) is equally critical because it respects the role-based access model WordPress enforces: different users have different permissions on different taxonomies. These layers—extracting the ID, dereferencing the taxonomy capability, and calling current_user_can() at the boundary—together form a defense-in-depth control that prevents unauthorized access.
Hardening Checklist
- Audit all REST endpoint callbacks: Search for custom
permission_callbackhandlers that omitcurrent_user_can()checks. Every endpoint that modifies state must verify capabilities before processing the request. - Use
WP_REST_Server::permission_callback(): Implement permission checks as separate methods tied to route registration via'permission_callback'parameter inregister_rest_route(), not inside the main handler. This ensures checks run before the endpoint logic executes. - Pass object IDs to capability checks: When authorizing operations on specific users, posts, or terms, always pass the object ID as the second argument to
current_user_can()(e.g.,current_user_can( 'edit_user', $user_id )). This enables WordPress to evaluate context-aware permissions. - Validate and sanitize REST parameters: Extract request parameters with
$request->get_param()and validate type and range before use. For object IDs, cast to(int)and confirm the object exists viaget_user_by(),get_term(), or similar before authorization. - Test unauthenticated access: Verify that all REST endpoints reject requests from
wp_get_current_user() === 0unless explicitly public. Write unit tests that call endpoints aswp_set_current_user( 0 )to confirm they fail gracefully.
References
- https://nvd.nist.gov/vuln/detail/CVE-2024-9161