CVE-2026-42668

Omnisend for WooCommerce <= 1.18.0 - Unauthenticated Omnisend Account Takeover via Predictable Connect Token

highUse of Insufficiently Random Values
7.5
CVSS Score
7.5
CVSS Score
high
Severity
1.18.1
Patched in
1d
Time to patch

Description

The Omnisend for WooCommerce plugin for WordPress is vulnerable to an unauthenticated account takeover via insufficiently random values in versions up to, and including, 1.18.0. This is due to the generate_install_url() function deriving the OAuth connect token solely from the Unix timestamp at page-load time (hash('sha256', time())), which yields only ~86,400 distinct values per day and is trivially brute-forceable. This makes it possible for unauthenticated attackers to predict the connect token, authenticate to the POST /wp-json/omnisend-api/v1/connect REST endpoint, and replace the store's omnisend_api_key and brand_id with attacker-supplied values, redirecting all customer PII synchronization, order webhooks, and marketing communications to the attacker's Omnisend account.

CVSS Vector Breakdown

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

Technical Details

Affected versions<=1.18.0
PublishedMay 13, 2026
Last updatedMay 13, 2026
Affected pluginomnisend-connect

What Changed in the Fix

Changes introduced in v1.18.1

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Vulnerability Research Plan: CVE-2026-42668 - Omnisend Account Takeover ## 1. Vulnerability Summary The **Omnisend for WooCommerce** plugin (up to and including version 1.18.0) contains a critical vulnerability where the OAuth connection token is generated using a predictable value. Specifically,…

Show full research plan

Vulnerability Research Plan: CVE-2026-42668 - Omnisend Account Takeover

1. Vulnerability Summary

The Omnisend for WooCommerce plugin (up to and including version 1.18.0) contains a critical vulnerability where the OAuth connection token is generated using a predictable value. Specifically, the Omnisend_Install::generate_install_url() function creates a connect_token using hash('sha256', time()). Since time() is a Unix timestamp with second-level precision, the resulting token space is extremely small (~86,400 values per day), allowing an unauthenticated attacker to brute-force the token and gain access to the POST /wp-json/omnisend-api/v1/connect REST endpoint.

Successful exploitation allows an attacker to overwrite the omnisend_api_key and omnisend_account_id (brand ID) settings, effectively hijacking the data synchronization between the WordPress store and Omnisend. This results in sensitive customer PII and order data being sent to the attacker's Omnisend account.

2. Attack Vector Analysis

  • REST Endpoint: POST /wp-json/omnisend-api/v1/connect
  • Permissions: Restricted by the validate_connect_token permission callback, which requires a valid connect_token.
  • Vulnerability: The connect_token is generated using a predictable time() value and persists in the omnisend_connect_token option until used or explicitly cleared.
  • Precondition: The omnisend_connect_token must have been generated (typically by an administrator visiting the Omnisend settings page) but not yet used to complete a connection.

3. Code Flow

  1. Token Generation:
    • When an administrator visits the settings page, Omnisend_Install::generate_install_url() (in includes/class-omnisend-install.php) is called.
    • It checks if omnisend_connect_token is empty:
      $token = get_option( 'omnisend_connect_token', '' );
      if ( $token === '' ) {
          $token = hash( 'sha256', time() );
          update_option( 'omnisend_connect_token', $token );
      }
      
  2. REST Authentication:
    • The /connect route uses validate_connect_token as its permission_callback (includes/omnisend-api.php).
    • validate_connect_token extracts connect_token from the JSON body and compares it to the stored option:
      $token = get_option( 'omnisend_connect_token', '' );
      if ( $token !== $request['connect_token'] ) { // 401 Unauthorized if mismatch
      
  3. Account Takeover:
    • If the token matches, omnisend_connect_account is executed.
    • It updates the API keys with the attacker-provided values:
      update_option( 'omnisend_connect_token', null );
      Omnisend_Settings::set_brand_id( $body['brand_id'] );
      update_option( 'omnisend_api_key', $body['omnisend_api_key'] );
      

4. Nonce Acquisition Strategy

This vulnerability does not require a WordPress nonce for the final exploitation of the REST endpoint. The permission_callback only validates the connect_token.

However, to trigger the generation of the token if it doesn't exist, an administrator must visit the settings page. For the purpose of a PoC in a test environment, the agent should simulate this by navigating to the Omnisend settings page or manually calling the function via WP-CLI.

5. Exploitation Strategy

Step 1: Synchronize Time

Obtain the server's current time to establish a baseline for the brute-force attack.

  • Request: GET /
  • Action: Extract the Date header from the response. Convert this to a Unix timestamp.

Step 2: Brute Force Token

Iterate through timestamps backwards from the current time (e.g., covering the last 24 hours).

  • Target Endpoint: POST /wp-json/omnisend-api/v1/connect
  • Method: POST
  • Headers: Content-Type: application/json
  • Payload:
    {
      "connect_token": "HASH_OF_TIMESTAMP",
      "brand_id": "attacker_brand_123",
      "omnisend_api_key": "attacker_api_key_456"
    }
    
  • Logic:
    1. Calculate token = hash('sha256', (string)$timestamp).
    2. Send request.
    3. If response is 401 (omnisend_incorrect_connect_token), decrement timestamp and retry.
    4. If response is 403 (omnisend_connect_denied), the token is empty or already used (abort/reset).
    5. If response is 200 (success: true), the takeover is successful.

6. Test Data Setup

  1. Install and activate the plugin: wp plugin install omnisend-connect --version=1.18.0 --activate.
  2. Ensure the plugin is in an "unconnected" state.
  3. Critical Step: Simulate an admin visit to generate the token:
    • Use wp eval "Omnisend_Install::get_registration_url();" OR
    • Login as admin and navigate to wp-admin/admin.php?page=omnisend-woocommerce.

7. Expected Results

  • The brute-force script should eventually identify the timestamp used during the "admin visit" simulation.
  • The server will return a 200 OK response with {"success": true}.
  • The omnisend_api_key option will be updated to attacker_api_key_456.

8. Verification Steps

After the HTTP exploit succeeds, verify the database state using WP-CLI:

  • Check Brand ID: wp option get omnisend_account_id
  • Check API Key: wp option get omnisend_api_key
  • Check Token cleared: wp option get omnisend_connect_token (should be empty/null)

9. Alternative Approaches

If the plugin is already connected, the validate_connect_token function returns a 403. In a real-world scenario, the attacker would wait for a "disconnect" event (or trigger one if another vulnerability exists) to force the site back into the connectable state. For this PoC, focus on the initial connection phase or a site where the admin has navigated to the setup page but hasn't finalized the connection.

Research Findings
Static analysis — not yet PoC-verified

Summary

The Omnisend for WooCommerce plugin is vulnerable to an unauthenticated account takeover due to the use of a predictable OAuth connection token generated using only the current Unix timestamp. Attackers can brute-force the approximately 86,400 possible tokens for a given day to authenticate to a sensitive REST endpoint and overwrite the store's API keys. This allows the attacker to hijack the synchronization of sensitive customer PII and order data to their own Omnisend account.

Vulnerable Code

// includes/class-omnisend-install.php (around line 234 in version 1.18.0)
	private static function generate_install_url() {
		$token = get_option( 'omnisend_connect_token', '' );

		if ( $token === '' ) {
			$token = hash( 'sha256', time() );
			update_option( 'omnisend_connect_token', $token );
		}

---

// includes/omnisend-api.php (around line 170 in version 1.18.0)
function validate_connect_token( WP_REST_Request $request ) {
	$body = json_decode( $request->get_body(), true );

	if ( ! isset( $body['connect_token'] ) ) {
		return new WP_Error(
			'omnisend_missing_connect_token',
			'Missing connect token in request.',
			array( 'status' => 400 )
		);
	}

	$token = get_option( 'omnisend_connect_token', '' );

	if ( $token === '' ) {
		return new WP_Error(
			'omnisend_connect_denied',
			'Connect token is already used.',
			array( 'status' => 403 )
		);
	}

	if ( $token !== $request['connect_token'] ) {
		return new WP_Error(
			'omnisend_incorrect_connect_token',
			'Connect token is incorrect.',
			array( 'status' => 401 )
		);
	}

	return true;
}

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/omnisend-connect/1.18.0/includes/class-omnisend-install.php /home/deploy/wp-safety.org/data/plugin-versions/omnisend-connect/1.18.1/includes/class-omnisend-install.php
--- /home/deploy/wp-safety.org/data/plugin-versions/omnisend-connect/1.18.0/includes/class-omnisend-install.php	2025-10-09 11:58:08.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/omnisend-connect/1.18.1/includes/class-omnisend-install.php	2026-04-15 12:40:58.000000000 +0000
@@ -231,7 +231,7 @@
 		$token = get_option( 'omnisend_connect_token', '' );
 
 		if ( $token === '' ) {
-			$token = hash( 'sha256', time() );
+			$token = bin2hex( random_bytes( 32 ) );
 			update_option( 'omnisend_connect_token', $token );
 		}
 
--- /home/deploy/wp-safety.org/data/plugin-versions/omnisend-connect/1.18.0/includes/omnisend-api.php	2025-10-09 11:58:08.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/omnisend-connect/1.18.1/includes/omnisend-api.php	2026-04-15 12:40:58.000000000 +0000
@@ -147,34 +147,30 @@
 	$body = json_decode( $request->get_body(), true );
 
 	if ( ! isset( $body['connect_token'] ) ) {
-		return new WP_Error(
-			'omnisend_missing_connect_token',
-			'Missing connect token in request.',
-			array( 'status' => 400 )
-		);
+		return omnisend_connect_token_invalid_error();
 	}
 
 	$token = get_option( 'omnisend_connect_token', '' );
 
 	if ( $token === '' ) {
-		return new WP_Error(
-			'omnisend_connect_denied',
-			'Connect token is already used.',
-			array( 'status' => 403 )
-		);
+		return omnisend_connect_token_invalid_error();
 	}
 
-	if ( $token !== $request['connect_token'] ) {
-		return new WP_Error(
-			'omnisend_incorrect_connect_token',
-			'Connect token is incorrect.',
-			array( 'status' => 401 )
-		);
+	if ( ! hash_equals( $token, (string) $request['connect_token'] ) ) {
+		return omnisend_connect_token_invalid_error();
 	}
 
 	return true;
 }
 
+function omnisend_connect_token_invalid_error() {
+	return new WP_Error(
+		'omnisend_invalid_connect_token',
+		'Connect token is invalid.',
+		array( 'status' => 403 )
+	);
+}

Exploit Outline

The exploit targets the vulnerable OAuth connection process of the Omnisend plugin. 1. **Preparation**: The attacker identifies a target site where the administrator has initiated but not completed the Omnisend connection process (or triggers the process if possible). This populates the `omnisend_connect_token` option with a predictable value: `hash('sha256', time())`. 2. **Time Synchronization**: The attacker sends a request to the target site (e.g., `GET /`) and captures the `Date` header to synchronize their local clock with the server's Unix timestamp. 3. **Brute-Force**: The attacker iterates through potential timestamps (typically backwards from the current time) and calculates the SHA256 hash for each second. 4. **Endpoint Hit**: For each candidate token, the attacker sends an unauthenticated `POST` request to `/wp-json/omnisend-api/v1/connect` with a payload containing the candidate `connect_token`, an attacker-controlled `brand_id`, and an `omnisend_api_key`. 5. **Validation**: A response code of `401` indicates an incorrect token, while a `200 OK` response indicates a successful guess and account takeover. 6. **Takeover**: Once successful, the plugin replaces the legitimate connection settings with the attacker's credentials, rerouting all subsequent WooCommerce webhooks and customer data syncs to the attacker's infrastructure.

Check if your site is affected.

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