Montonio for WooCommerce <= 10.1.2 - Missing Authorization
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:NTechnical Details
<=10.1.2What Changed in the Fix
Changes introduced in v10.1.3
Source Code
WordPress.org SVNThis 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_settingsoption with arbitrary API keys (linked to an attacker's Montonio account). - Preconditions: The plugin must be active. The attacker needs to generate a valid
codeby initiating a connection flow through the official Montonio Partner System for their own account.
3. Code Flow
- Registration: In
includes/admin/class-montonio-connection.php, theinit()method registers the hook:add_action( 'woocommerce_api_montonio_connection', array( __CLASS__, 'handle_callback' ) ); - Entry Point: An unauthenticated HTTP request hits
https://example.com/?wc-api=montonio_connection. - Callback Processing: WooCommerce's API dispatcher calls
Montonio_Connection::handle_callback. - Exchange (Inferred): The function (located after line 192 in the full source) likely retrieves a
codeparameter from$_GETor$_POST. - Telemetry Request: It sends this code to
self::TELEMETRY_API_URL(https://plugin-telemetry.montonio.com/api) to retrieve API keys. - Sink: Upon receiving the keys, it calls
update_option( self::SETTINGS_OPTION, $new_settings ), whereself::SETTINGS_OPTIONis'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
stateparameter 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.
- Identify Parameters (Inferred): Based on the plugin's purpose, the callback expects a
codeparameter. - Initiate Request: Send a GET or POST request to the WooCommerce API endpoint.
- Target URL:
http://localhost:8888/?wc-api=montonio_connection - Payload:
code: A placeholder string (e.g.,poc_test_code_123).
- 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
- Install and activate WooCommerce.
- Install and activate Montonio for WooCommerce version 10.1.2.
- No specific configuration is required, as the endpoint is exposed globally upon plugin activation.
7. Expected Results
- Response: The server should return a
302 Redirectback to the settings page with a query parameter likemontonio_connect=activation_failed(or similar, depending on the response from the unreachable telemetry API). - Authorization Bypass: The request should not return a
403 Forbiddenorwp_diemessage, 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:
- Check for Notice State:
wp option get woocommerce_wc_montonio_api_settings - Verify Access: If the function was reached, the plugin may have logged an error or updated a "connection status" flag in the options.
- Log Inspection: Check the WordPress debug log (
wp-content/debug.log) for anyWP_Errorobjects or failedwp_remote_postcalls toplugin-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.
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
@@ -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.