CVE-2026-32398

TeraWallet – For WooCommerce <= 1.5.15 - Authenticated (Customer+) Race Condition

mediumConcurrent Execution using Shared Resource with Improper Synchronization ('Race Condition')
4.3
CVSS Score
4.3
CVSS Score
medium
Severity
1.5.16
Patched in
75d
Time to patch

Description

The Wallet for WooCommerce plugin for WordPress is vulnerable to a race condition in all versions up to, and including, 1.5.15. This makes it possible for authenticated attackers, with Custom-level access and above, to perform an unauthorized action.

CVSS Vector Breakdown

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

Technical Details

Affected versions<=1.5.15
PublishedFebruary 20, 2026
Last updatedMay 5, 2026
Affected pluginwoo-wallet

What Changed in the Fix

Changes introduced in v1.5.16

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan: TeraWallet Race Condition (CVE-2026-32398) ## 1. Vulnerability Summary The **Wallet for WooCommerce (TeraWallet)** plugin is vulnerable to a race condition in versions up to 1.5.15. The flaw exists because sensitive operations—specifically **Partial Payments** and **Wa…

Show full research plan

Exploitation Research Plan: TeraWallet Race Condition (CVE-2026-32398)

1. Vulnerability Summary

The Wallet for WooCommerce (TeraWallet) plugin is vulnerable to a race condition in versions up to 1.5.15. The flaw exists because sensitive operations—specifically Partial Payments and Wallet Transfers—do not implement proper concurrency controls (like database-level row locking or atomic transactions) when checking and updating the user's wallet balance. An authenticated attacker can send multiple concurrent requests to these endpoints, allowing them to "double-spend" their balance or apply a total discount to their cart that exceeds their actual wallet balance.

2. Attack Vector Analysis

  • Endpoint: /wp-json/wc/store/v1/cart/extensions (WooCommerce Store API)
  • Namespace: apply-partial-payment
  • Action: POST
  • Authentication: Authenticated (Customer or above).
  • Preconditions:
    1. TeraWallet "Partial Payment" must be enabled (default: 'on').
    2. User must have a non-zero wallet balance (e.g., $10).
    3. User must have an item in the WooCommerce cart.
    4. The cart total must be greater than the user's current wallet balance (to ensure the "Partial Payment" UI is active).

3. Code Flow

  1. Frontend: The React component in build/partial-payment/index.js uses the extensionCartUpdate function from the WooCommerce Blocks package.
  2. JS Execution: When the "Apply" button is clicked, it calls s({namespace:"apply-partial-payment",data:{amount:e}}).
  3. API Route: This triggers a POST request to the WooCommerce Store API endpoint /wp-json/wc/store/v1/cart/extensions.
  4. Backend Sink: The plugin's extension handler (triggered by the apply-partial-payment namespace) reads the current balance using woo_wallet()->wallet->get_wallet_balance().
  5. Race Condition: Because multiple concurrent requests can all execute the "check balance" step before any single request completes the "deduct/apply fee" step, the plugin may apply the discount multiple times or allow multiple deductions from the same initial balance state.

4. Nonce Acquisition Strategy

The WooCommerce Store API requires a specific nonce for state-changing requests (POST).

  1. Navigate: Use browser_navigate to the checkout page (/checkout/).
  2. Extract: The nonce is stored in the WooCommerce Blocks settings object.
  3. JS Variable: window.wc.wcSettings.
  4. Extraction Command:
    browser_eval("window.wc.wcSettings.getSetting('wc-store-api-nonce')")
    
  5. Context: Also verify the partial-payment_data localized variable:
    browser_eval("window.wc.wcSettings.getSetting('partial-payment_data')")
    

5. Exploitation Strategy

Step 1: Prepare Environment

  • Create a product and a customer user.
  • Credit the user's wallet with a small amount (e.g., $5).
  • Add a product costing $100 to the cart.

Step 2: Concurrent API Attacks

Send 20 concurrent POST requests to the cart extensions endpoint.

  • URL: https://TARGET/wp-json/wc/store/v1/cart/extensions
  • Method: POST
  • Headers:
    • Content-Type: application/json
    • X-WC-Store-API-Nonce: [EXTRACTED_NONCE]
  • Body:
    {
      "namespace": "apply-partial-payment",
      "data": {
        "amount": "5"
      }
    }
    

Step 3: Verification

Check the cart totals to see if multiple fees were applied.

  • URL: https://TARGET/wp-json/wc/store/v1/cart
  • Method: GET
  • Check: Look at the fees array and the total_price. If the total discount exceeds $5, the race condition is successful.

6. Test Data Setup

  1. Create Product:
    wp product create --name="Premium Item" --regular_price=100 --status=publish
    
  2. Create User:
    wp user create attacker attacker@example.com --role=customer --user_pass=password
    
  3. Enable Partial Payment:
    wp option patch update _wallet_settings_general is_enable_partial_payment on
    
  4. Add Wallet Balance:
    wp eval "woo_wallet()->wallet->credit(get_user_by('login', 'attacker')->ID, 5, 'Testing Race');"
    
  5. Add to Cart: Log in as attacker and add the product to the cart via /?add-to-cart=[ID].

7. Expected Results

  • Success: The cart total will show a discount (fee) significantly higher than the $5 balance (e.g., $25 discount if 5 requests succeeded).
  • Database State: The user's wallet transactions (wp_woowallet_transactions) might show multiple deductions that, when summed, exceed the original balance, or the session-based fee in the cart will simply be multiplied.

8. Verification Steps

  1. Check Cart via CLI:
    # Hard to check cart session via CLI, use HTTP GET to Store API
    
  2. Check Transactions:
    wp db query "SELECT * FROM wp_woowallet_transactions WHERE user_id = [ID] ORDER BY transaction_id DESC"
    
  3. Check User Meta:
    wp user meta get [ID] _wallet_balance
    

9. Alternative Approaches

If the Store API endpoint is not additive (i.e., it overwrites the amount), target the Wallet Transfer functionality:

  • Endpoint: admin-ajax.php
  • Action: woo_wallet_transfer (inferred)
  • Payload: target_user, amount, _wpnonce.
  • Strategy: Send concurrent transfers to a second "mule" account. If the attacker has $10 and successfully transfers $10 to the mule 5 times simultaneously, the mule's balance will increase by $50, while the attacker's balance likely only drops to $0 or -$40.

Alternatively, target Sell Content in includes/actions/class-woo-wallet-action-sell-content.php:

  • Endpoint: GET /?purchase_content=[POST_ID]
  • Strategy: Repeatedly hit this URL concurrently for an expensive post. Check if the balance deduction is only performed once or multiple times (leading to negative balance/profit share inflation).
Research Findings
Static analysis — not yet PoC-verified

Summary

The TeraWallet plugin for WooCommerce is vulnerable to a race condition in features like Partial Payments and Sell Content purchases. Because the plugin checks a user's wallet balance and subsequently performs a deduction without atomic transactions or database row locking, multiple concurrent requests can all pass the balance check before any single deduction is finalized, allowing for 'double-spending' or applying discounts that exceed the available balance.

Vulnerable Code

// includes/actions/class-woo-wallet-action-sell-content.php around line 439
public function handle_purchase_content() : void {
	global $post;
	if ( isset( $_POST['tw_buy_content_nonce'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		$amount = isset( $_POST['amount'] ) ? sanitize_text_field( wp_unslash( $_POST['amount'] ) ) : 0;
		if ( wp_verify_nonce( wp_unslash( $_POST['tw_buy_content_nonce'] ), 'tw_buy_content_nonce_' . $amount ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
			$user_id                = get_current_user_id();
			$transient              = isset( $_POST['transient'] ) ? sanitize_text_field( wp_unslash( $_POST['transient'] ) ) : md5( 'tw-sell-content' . $post->ID . $user_id . $amount );
			$tw_sell_content_amount = floatval( $amount );
			// ... balance check and deduction logic follows ...

---

// build/partial-payment/index.js (simplified trigger logic)
onClick:t=>{t.preventDefault(),w(!0),s({namespace:"apply-partial-payment",data:{amount:e}}).then(()=>{w(!1)})}

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/woo-wallet/1.5.15/includes/actions/class-woo-wallet-action-sell-content.php /home/deploy/wp-safety.org/data/plugin-versions/woo-wallet/1.5.16/includes/actions/class-woo-wallet-action-sell-content.php
--- /home/deploy/wp-safety.org/data/plugin-versions/woo-wallet/1.5.15/includes/actions/class-woo-wallet-action-sell-content.php	2025-12-10 03:05:50.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/woo-wallet/1.5.16/includes/actions/class-woo-wallet-action-sell-content.php	2026-02-12 05:48:14.000000000 +0000
@@ -409,7 +409,7 @@
 		$user_id = get_current_user_id();
 		ob_start();
 		?>
 		<form method="post">
-			<?php wp_nonce_field( 'tw_buy_content_nonce_' . $amount, 'tw_buy_content_nonce' ); ?>
+			<?php wp_nonce_field( 'tw_buy_content_nonce_' . $post->ID . '_' . $user_id . '_' . $amount, 'tw_buy_content_nonce' ); ?>
 			<input type="hidden" name="amount" value="<?php echo esc_attr( $amount ); ?>">
 			<input type="hidden" name="transient" value="<?php echo esc_attr( md5( 'tw-sell-content' . $post->ID . $user_id . $amount ) ); ?>">
 			<button type="submit" class="<?php echo esc_attr( $this->settings['button_css_class'] ); ?>"><?php echo esc_html( $this->settings['button_lable'] ); ?></button>
@@ -436,13 +441,17 @@
 	 *
 	 * @return void
 	 */
-	public function handle_purchase_content() : void {
+	public function handle_purchase_content() {
 		global $post;
 		if ( isset( $_POST['tw_buy_content_nonce'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
-			$amount = isset( $_POST['amount'] ) ? sanitize_text_field( wp_unslash( $_POST['amount'] ) ) : 0;
-			if ( wp_verify_nonce( wp_unslash( $_POST['tw_buy_content_nonce'] ), 'tw_buy_content_nonce_' . $amount ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
-				$user_id                = get_current_user_id();
-				$transient              = isset( $_POST['transient'] ) ? sanitize_text_field( wp_unslash( $_POST['transient'] ) ) : md5( 'tw-sell-content' . $post->ID . $user_id . $amount );
+			if ( ! is_user_logged_in() ) {
+				return;
+			}
+			$amount  = isset( $_POST['amount'] ) ? sanitize_text_field( wp_unslash( $_POST['amount'] ) ) : 0;
+			$user_id = get_current_user_id();
+			// Verify nonce bound to post ID, user ID and amount.
+			if ( wp_verify_nonce( wp_unslash( $_POST['tw_buy_content_nonce'] ), 'tw_buy_content_nonce_' . $post->ID . '_' . $user_id . '_' . $amount ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
+				$transient              = md5( 'tw-sell-content' . $post->ID . $user_id . $amount );
 				$tw_sell_content_amount = floatval( $amount );

Exploit Outline

The exploit requires a Customer-level account with a small amount of wallet credit. 1. Authenticate as the customer and add an item to the WooCommerce cart. 2. Capture the 'X-WC-Store-API-Nonce' from the checkout page (available in window.wc.wcSettings). 3. Send a flood of concurrent POST requests to /wp-json/wc/store/v1/cart/extensions using the 'apply-partial-payment' namespace, specifying an amount equal to the user's total balance in the payload. 4. Due to the race condition, multiple requests will pass the initial balance check before the first deduction is recorded, resulting in multiple applications of the balance as a discount, effectively exceeding the user's actual credit.

Check if your site is affected.

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