CVE-2026-48873

Montonio for WooCommerce <= 10.1.2 - Missing Authorization

mediumMissing Authorization
5.3
CVSS Score
5.3
CVSS Score
medium
Severity
10.1.3
Patched in
7d
Time to patch

Description

The Montonio for WooCommerce plugin for WordPress is vulnerable to unauthorized access due to a missing capability check on a function in versions up to, and including, 10.1.2. This makes it possible for unauthenticated attackers to perform an unauthorized action.

CVSS Vector Breakdown

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

Technical Details

Affected versions<=10.1.2
PublishedJune 2, 2026
Last updatedJune 8, 2026

What Changed in the Fix

Changes introduced in v10.1.3

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

This research plan focuses on **CVE-2026-48873**, a missing authorization vulnerability in the **Montonio for WooCommerce** plugin. The vulnerability allows an unauthenticated attacker to trigger the account connection callback, potentially overwriting the store's API credentials. ### 1. Vulnerabil…

Show full research plan

This research plan focuses on CVE-2026-48873, a missing authorization vulnerability in the Montonio for WooCommerce plugin. The vulnerability allows an unauthenticated attacker to trigger the account connection callback, potentially overwriting the store's API credentials.

1. Vulnerability Summary

The vulnerability exists in the Montonio_Connection::handle_callback method, which is registered as a handler for the woocommerce_api_montonio_connection hook. Unlike handle_disconnect, which correctly implements a capability check (current_user_can( 'manage_woocommerce' )), the callback handler lacks any authorization or authentication checks. This endpoint is designed to receive an authorization code from Montonio's Partner System and exchange it for API keys, which are then saved to the site's database.

2. Attack Vector Analysis

  • Endpoint: /?wc-api=montonio_connection (The WooCommerce API endpoint for this action).
  • Authentication: None (Unauthenticated).
  • Vulnerable Action: Overwriting the woocommerce_wc_montonio_api_settings option with arbitrary API keys (linked to an attacker's Montonio account).
  • Preconditions: The plugin must be active. The attacker needs to generate a valid code by initiating a connection flow through the official Montonio Partner System for their own account.

3. Code Flow

  1. Registration: In includes/admin/class-montonio-connection.php, the init() method registers the hook:
    add_action( 'woocommerce_api_montonio_connection', array( __CLASS__, 'handle_callback' ) );
  2. Entry Point: An unauthenticated HTTP request hits https://example.com/?wc-api=montonio_connection.
  3. Callback Processing: WooCommerce's API dispatcher calls Montonio_Connection::handle_callback.
  4. Exchange (Inferred): The function (located after line 192 in the full source) likely retrieves a code parameter from $_GET or $_POST.
  5. Telemetry Request: It sends this code to self::TELEMETRY_API_URL (https://plugin-telemetry.montonio.com/api) to retrieve API keys.
  6. Sink: Upon receiving the keys, it calls update_option( self::SETTINGS_OPTION, $new_settings ), where self::SETTINGS_OPTION is 'woocommerce_wc_montonio_api_settings'.

4. Nonce Acquisition Strategy

This specific vulnerability does not require a WordPress nonce.

  • The woocommerce_api_ hooks are designed for external service callbacks (webhooks/OAuth-style redirects). Since the external service (Montonio) cannot know a WordPress user's session-specific nonce, these endpoints are intentionally public.
  • The security of such endpoints usually relies on a state parameter or checking a cryptographic signature, which appears to be missing or bypassed here.

5. Exploitation Strategy

To verify the missing authorization, we will simulate the callback that occurs after a merchant authorizes the plugin.

  1. Identify Parameters (Inferred): Based on the plugin's purpose, the callback expects a code parameter.
  2. Initiate Request: Send a GET or POST request to the WooCommerce API endpoint.
  3. Target URL: http://localhost:8888/?wc-api=montonio_connection
  4. Payload:
    • code: A placeholder string (e.g., poc_test_code_123).
  5. Execution via http_request:
    // Example request to trigger the vulnerable code path
    await http_request({
        url: 'http://localhost:8888/?wc-api=montonio_connection&code=poc_test_code_123',
        method: 'GET'
    });
    

Note: Since the actual key exchange requires a valid code from Montonio's live servers, a successful PoC in an isolated environment will manifest as an "activation_failed" or "invalid_request" error message in the logs or a redirect with a specific status, proving the function was reached and processed without a 403 Forbidden error.

6. Test Data Setup

  1. Install and activate WooCommerce.
  2. Install and activate Montonio for WooCommerce version 10.1.2.
  3. No specific configuration is required, as the endpoint is exposed globally upon plugin activation.

7. Expected Results

  • Response: The server should return a 302 Redirect back to the settings page with a query parameter like montonio_connect=activation_failed (or similar, depending on the response from the unreachable telemetry API).
  • Authorization Bypass: The request should not return a 403 Forbidden or wp_die message, despite the attacker being unauthenticated.

8. Verification Steps

After sending the request, use WP-CLI to check if the plugin attempted to process the request:

  1. Check for Notice State:
    wp option get woocommerce_wc_montonio_api_settings
  2. Verify Access: If the function was reached, the plugin may have logged an error or updated a "connection status" flag in the options.
  3. Log Inspection: Check the WordPress debug log (wp-content/debug.log) for any WP_Error objects or failed wp_remote_post calls to plugin-telemetry.montonio.com, which confirms the logic was executed.

9. Alternative Approaches

If a simple GET request is ignored, the callback might strictly require a POST request (as mentioned in the docblock: "Receives the auto-submitted form POST"):

await http_request({
    url: 'http://localhost:8888/?wc-api=montonio_connection',
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: 'code=poc_test_code_123'
});

Additionally, check if the handle_callback method requires a state parameter to match an existing session; if so, the vulnerability might only be exploitable via CSRF or if the state check is also missing.

Research Findings
Static analysis — not yet PoC-verified

Summary

The Montonio for WooCommerce plugin fails to perform authorization or state validation on its account connection callback endpoint. Unauthenticated attackers can exploit this by sending a crafted request to the `woocommerce_api_montonio_connection` hook, which overwrites the store's API credentials with keys linked to the attacker's own Montonio account.

Vulnerable Code

// includes/admin/class-montonio-connection.php (lines 192-205 in v10.1.2)

    public static function handle_callback() {
        if ( ! isset( $_POST['connectionUuid'], $_POST['oneTimeCode'] ) && ! isset( $_POST['cancelled'] ) ) {
            return;
        }

        $cancelled       = sanitize_text_field( wp_unslash( $_POST['cancelled'] ?? '' ) );
        $connection_uuid = sanitize_text_field( wp_unslash( $_POST['connectionUuid'] ?? '' ) );
        $one_time_code   = sanitize_text_field( wp_unslash( $_POST['oneTimeCode'] ?? '' ) );

        if ( 'true' === $cancelled ) {
            self::redirect_to_settings( 'cancelled' );
            return;
        }

Security Fix

--- /home/deploy/wp-safety.org/data/plugin-versions/montonio-for-woocommerce/10.1.2/includes/admin/class-montonio-connection.php	2026-05-15 06:27:40.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/montonio-for-woocommerce/10.1.3/includes/admin/class-montonio-connection.php	2026-05-22 07:25:20.000000000 +0000
@@ -125,11 +125,19 @@
             trailingslashit( get_home_url() )
         );
 
+        $state = bin2hex( random_bytes( 16 ) );
+        update_option(
+            self::state_option_key(),
+            $state,
+            false
+        );
+
         $query = http_build_query( array(
             'action'      => 'connect-plugin',
             'platform'    => 'woocommerce',
             'storeUrl'    => wp_parse_url( get_home_url(), PHP_URL_HOST ),
-            'callbackUrl' => $redirect_uri
+            'callbackUrl' => $redirect_uri,
+            'state'       => $state,
         ) );
 
         return 'https://partner.montonio.com/?' . $query;
@@ -200,9 +208,31 @@
             return;
         }
 
+        if ( ! current_user_can( 'manage_woocommerce' ) ) {
+            WC_Montonio_Logger::log( 'Connection callback rejected: insufficient capability.' );
+            wp_die(
+                esc_html__( 'Sorry, you are not allowed to access this page.' ),
+                '',
+                array( 'response' => 403 )
+            );
+        }
+
         $cancelled       = sanitize_text_field( wp_unslash( $_POST['cancelled'] ?? '' ) );
         $connection_uuid = sanitize_text_field( wp_unslash( $_POST['connectionUuid'] ?? '' ) );
         $one_time_code   = sanitize_text_field( wp_unslash( $_POST['oneTimeCode'] ?? '' ) );
+        $state           = sanitize_text_field( wp_unslash( $_POST['state'] ?? '' ) );
+
+        $stored_state = get_option( self::state_option_key() );
+        if ( false === $stored_state || ! is_string( $stored_state ) || '' === $state || ! hash_equals( $stored_state, $state ) ) {
+            WC_Montonio_Logger::log( 'Connection callback rejected: state mismatch or missing stored state.' );
+            // Clean up whatever may be there so a fresh flow starts cleanly.
+            delete_option( self::state_option_key() );
+            self::redirect_to_settings( 'invalid_request' );
+            return;
+        }
+
+        // State validated; consume it.
+        delete_option( self::state_option_key() );
 
         if ( 'true' === $cancelled ) {
             self::redirect_to_settings( 'cancelled' );
@@ -223,7 +253,7 @@
             return;
         }
 
-        $activation = self::activate_connection( $connection_uuid, $one_time_code );
+        $activation = self::activate_connection( $connection_uuid, $one_time_code, $state );
 
         if ( is_wp_error( $activation ) ) {
             WC_Montonio_Logger::log( 'Connection activation failed: ' . $activation->get_error_message() );
@@ -244,14 +274,27 @@
     }
 
     /**
+     * Build the per-user wp_options key that stores the OAuth state value
+     * for an in-flight connect flow. Single source of truth for the four
+     * call sites that read / write / consume it.
+     *
+     * @since 10.1.3
+     * @return string
+     */
+    private static function state_option_key() {
+        return 'montonio_connection_state_' . get_current_user_id();
+    }
+
+    /**
      * Exchange the one-time code with PTS for API keys.
      *
      * @since 10.1.0
      * @param string $connection_uuid
      * @param string $one_time_code
+     * @param string $state OAuth state value validated against wp_options, forwarded to PTS.
      * @return array|WP_Error Decoded PTS response on success.
      */
-    private static function activate_connection( $connection_uuid, $one_time_code ) {
+    private static function activate_connection( $connection_uuid, $one_time_code, $state ) {
         $response = wp_remote_post(
             trailingslashit( self::TELEMETRY_API_URL ) . 'connections/activate',
             array(
@@ -263,7 +306,8 @@
                 'body'    => wp_json_encode(
                     array(
                         'connectionUuid' => $connection_uuid,
-                        'oneTimeCode'    => $one_time_code
+                        'oneTimeCode'    => $one_time_code,
+                        'state'          => $state,
                     )
                 )
             )

Exploit Outline

The exploit targets the public WooCommerce API endpoint registered by the plugin. An attacker first obtains a valid `connectionUuid` and `oneTimeCode` by initiating a connection flow through the official Montonio Partner System for their own merchant account. They then send an unauthenticated POST request to `/?wc-api=montonio_connection` on the target site, providing their own credentials in the POST body. Because the plugin lacks capability checks and does not verify an OAuth-style 'state' parameter, it accepts the data, exchanges the code with Montonio's telemetry service, and updates the site's database settings (`woocommerce_wc_montonio_api_settings`) with the attacker's API keys.

Check if your site is affected.

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