The Exploit
Authenticated Subscriber-level users can abuse the plugin's AJAX callback to overwrite arbitrary string user meta on their own account.
curl 'https://TARGET/wp-admin/admin-ajax.php' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-H 'Cookie: wordpress_logged_in_<hash>=<session>' \
--data 'action=aiovg_store_user_meta&user_id=13&key=first_name&value=owned'
The request succeeds even without a high privilege account; the AJAX handler returns a normal WordPress response and the selected user meta key on the authenticated account is updated in the database. The attacker can repeat this for any key and value pair, including standard profile meta like description or custom string meta.
What the Patch Did
Before:
$key = isset( $_POST['key'] ) ? sanitize_text_field( $_POST['key'] ) : '';
$value = isset( $_POST['value'] ) ? sanitize_text_field( $_POST['value'] ) : '';
if ( ! empty( $user_id ) && ! empty( $key ) ) {
update_user_meta( $user_id, $key, $value );
}
wp_die();
After:
if ( ! $user_id ) {
wp_die();
}
if ( ! current_user_can( 'manage_aiovg_options' ) ) {
wp_die();
}
$key = isset( $_POST['key'] ) ? sanitize_key( $_POST['key'] ) : '';
$allowed_keys = array( 'aiovg_video_form_tour', 'aiovg_automation_form_tour' );
if ( ! in_array( $key, $allowed_keys ) ) {
wp_die();
}
$value = isset( $_POST['value'] ) ? trim( $_POST['value'] ) : 0;
if ( 'completed' !== $value ) {
$value = (int) $value;
}
update_user_meta( $user_id, $key, $value );
The patch adds a WordPress capability check via current_user_can('manage_aiovg_options'), plus stricter input handling: sanitize_key() for the metadata key, a whitelist of allowed keys, and normalized value coercion.
Root Cause
This is CWE-863: Incorrect Authorization. The AJAX callback ajax_callback_store_user_meta() accepted attacker-controlled $_POST['user_id'], $_POST['key'], and $_POST['value'], then passed them directly into update_user_meta() without first verifying that the requester was authorized. The trust boundary crossed was the HTTP POST request into a database write operation, and there was no capability check guarding that state-changing path.
Why It Works
The load-bearing defense is the current_user_can( 'manage_aiovg_options' ) check. Without that line, any authenticated user can still reach update_user_meta() and modify their own metadata. The other added lines are defense-in-depth: sanitize_key() ensures the meta key is a valid slug, the whitelist prevents arbitrary meta keys, and value normalization limits the payload shape. But the exploit is fundamentally possible because the prior code had no authorization gate.
Hardening Checklist
- Use
current_user_can()in every admin AJAX action that changes data. - Protect state-changing AJAX requests with
check_ajax_referer()and nonces. - Sanitize metadata keys with
sanitize_key()and do not accept arbitrary strings. - Restrict metadata updates to a fixed allowlist using
in_array($key, $allowed_keys, true). - When updating profile data, verify the target user is the current user or that the caller has
edit_usercapability for that user.
References
- https://nvd.nist.gov/vuln/detail/CVE-2025-15516