The Exploit
An authenticated subscriber-level user can POST to an AJAX endpoint to modify shop settings without administrative approval, because the plugin validates CSRF tokens but skips capability checks.
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: target.local
Content-Type: application/x-www-form-urlencoded
Cookie: wordpress_logged_in=subscriber_session_token
action=wcrb_main_setting_form_submit&wcrb_main_setting_nonce_field=valid_nonce_here&wcrb_shop_name=Pwned%20Shop&[email protected]&wcrb_shop_phone=1234567890&wcrb_shop_address=attacker_address
The attacker is a subscriber (role ID 4, minimum permission level on the install). The server responds with HTTP 200 and updates wp_options table rows for wcrb_shop_name, wcrb_shop_email, and wcrb_shop_phone to attacker-supplied values. A shop manager opening the settings page now sees the shop branded with attacker contact details, and any outbound customer emails carry the attacker's email as a reply-to field.
What the Patch Did
Before
if ( ! isset( $_POST['wcrb_main_setting_nonce_field'] ) || ! wp_verify_nonce( $_POST['wcrb_main_setting_nonce_field'], 'wcrb_main_setting_nonce' ) ) {
$message = esc_html__("Something is wrong with your submission!", "computer-repair-shop");
} else {
// Process setting updates directly
After
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'message' => 'Unauthorized' ) );
wp_die();
}
if ( ! isset( $_POST['wcrb_main_setting_nonce_field'] ) || ! wp_verify_nonce( $_POST['wcrb_main_setting_nonce_field'], 'wcrb_main_setting_nonce' ) ) {
$message = esc_html__("Something is wrong with your submission!", "computer-repair-shop");
} else {
// Process setting updates
The patch added a current_user_can( 'manage_options' ) capability check before nonce validation. In WordPress, the manage_options capability belongs exclusively to administrators on single-site installs; on multisite, it can be delegated to super-admins. The check gates entry to the entire settings handler, and if the user lacks the capability, execution terminates with wp_die() rather than proceeding to the vulnerable update logic.
Root Cause
CWE-862: Missing Authorization — The handler wcrb_main_setting_form_submit() in lib/includes/main_page.php (line 1086) receives attacker-controlled POST parameters wcrb_main_setting_nonce_field, wcrb_shop_name, wcrb_shop_email, etc. The nonce is verified to prevent CSRF, but WordPress capability checks (current_user_can()) are never consulted before the parameters reach the database update sink at lines 1095+. The trust boundary between subscriber and administrator roles is crossed without enforcement. An identical issue exists in wcrb_currency_setting_form_submit() at line 998, and a third in the AJAX handler wcrb_reload_customer_data() in theme_functions.php line 5435, which lacks both nonce and role verification.
Why It Works
The single load-bearing line is if ( ! current_user_can( 'manage_options' ) ) wp_die(); — without it, subscribers proceed directly to the nonce check and option update. The engineer added the secondary lines (wp_send_json_error() and explicit wp_die()) for user-facing clarity and graceful error handling: they ensure that rejected requests return a JSON response explaining the denial rather than a silent 200 OK. However, the capability check itself is the gatekeeper. Nonce validation alone does not restrict who can POST; it restricts which requests are legitimate from any authenticated user. A nonce is valid whether you are a subscriber or an admin, because it is keyed only to the session and action name. The patch correctly places the role check before nonce verification: nonces are expensive to compute (they read the database), so rejecting unauthorized users first saves cycles.
Hardening Checklist
- Audit every AJAX handler registered via
add_action( 'wp_ajax_...', callback )oradd_action( 'wp_ajax_nopriv_...', callback )and prepend acurrent_user_can( 'required_capability' )check; usewp_ajax_noprivonly for public-facing actions (e.g., cart add). - Validate nonces after capability checks, not before. Structure as: capability check → nonce check → input validation → database operation. This mirrors WordPress core patterns in
wp-admin/options.php. - Use
wp_verify_nonce()on every state-altering POST, even if capability-gated. Nonce + capability is defense in depth; either alone is insufficient. - Prohibit direct
$_POSTor$_GETaccess in handlers intended for admin use; wrap parameters in sanitization routines such assanitize_text_field()and escaped output functions likeesc_attr()to prevent secondary injection. - Document required roles in callback comments, e.g.,
// Requires: manage_options (admin only), so future maintainers understand the threat model.
References
- https://nvd.nist.gov/vuln/detail/CVE-2026-39584