CVE-2026-1558

WP Recipe Maker <= 10.3.2 - Insecure Direct Object Reference to Unauthenticated Arbitrary Post Metadata Modification via 'recipeId' Parameter

mediumAuthorization Bypass Through User-Controlled Key
5.3
CVSS Score
5.3
CVSS Score
medium
Severity
10.3.3
Patched in
1d
Time to patch

Description

The WP Recipe Maker plugin for WordPress is vulnerable to an Insecure Direct Object Reference (IDOR) in versions up to, and including, 10.3.2. This is due to the /wp-json/wp-recipe-maker/v1/integrations/instacart REST API endpoint's permission_callback being set to __return_true and a lack of subsequent authorization or ownership checks on the user-supplied recipeId. This makes it possible for unauthenticated attackers to overwrite arbitrary post metadata (wprm_instacart_combinations) for any post ID on the site via the recipeId parameter.

CVSS Vector Breakdown

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
None
Scope
Unchanged
None
Confidentiality
Low
Integrity
None
Availability

Technical Details

Affected versions<=10.3.2
PublishedFebruary 26, 2026
Last updatedFebruary 27, 2026
Affected pluginwp-recipe-maker

What Changed in the Fix

Changes introduced in v10.3.3

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan - CVE-2026-1558 ## 1. Vulnerability Summary The **WP Recipe Maker** plugin (<= 10.3.2) is vulnerable to an **Insecure Direct Object Reference (IDOR)** in its Instacart integration REST API endpoint. The endpoint `/wp-json/wp-recipe-maker/v1/integrations/instacart` uses …

Show full research plan

Exploitation Research Plan - CVE-2026-1558

1. Vulnerability Summary

The WP Recipe Maker plugin (<= 10.3.2) is vulnerable to an Insecure Direct Object Reference (IDOR) in its Instacart integration REST API endpoint. The endpoint /wp-json/wp-recipe-maker/v1/integrations/instacart uses __return_true for its permission_callback, allowing unauthenticated access. Furthermore, the plugin fails to verify if the current user has permission to edit the post specified by the recipeId parameter before updating its metadata (wprm_instacart_combinations). This allows an unauthenticated attacker to modify post metadata for any post, page, or recipe ID on the site.

2. Attack Vector Analysis

  • Endpoint: POST /wp-json/wp-recipe-maker/v1/integrations/instacart
  • Method: POST
  • Authentication: Unauthenticated (None required).
  • Vulnerable Parameter: data[recipeId] inside the JSON body.
  • Payload Parameters:
    • data[recipeId]: The ID of the target post to modify.
    • data[servingsSystemCombination]: A unique key to identify the specific combination.
    • data[title]: A string used in the Instacart API call.
    • data[image_url]: A URL used in the Instacart API call.
    • data[ingredients]: An array of ingredient objects (required to trigger the code path that calls update_post_meta).

3. Code Flow

  1. Entry Point: The REST route is registered in includes/public/api/class-wprm-api-integrations.php.
    • Line 47: permission_callback is set to __return_true.
    • Line 45: The route is /integrations/instacart.
  2. Controller: WPRM_Api_Integrations::api_instacart (Line 56) receives the request.
    • Line 57: $params = $request->get_params();
    • Line 62: Calls WPRM_Instacart::get_link_for_recipe( $params['data'] ).
  3. Vulnerable Logic: WPRM_Instacart::get_link_for_recipe in includes/public/class-wprm-instacart.php.
    • Line 54: $recipe_id = intval( $data['recipeId'] ); (The IDOR occurs here; no ownership check follows).
    • Line 55: $servings_system_combination = sanitize_key( $data['servingsSystemCombination'] );
    • Line 58-72: Checks for existing cached combinations in metadata wprm_instacart_combinations. If the provided servingsSystemCombination is new, it proceeds.
    • Line 75-108: Prepares $api_data and loops through $data['ingredients'].
    • Line 111: Calls self::call_instacart_api( 'recipe', $api_data ).
    • Line 119: The Sink: update_post_meta( $recipe_id, 'wprm_instacart_combinations', $existing_combinations ); updates the metadata of the arbitrary $recipe_id.

4. Nonce Acquisition Strategy

This vulnerability does not require a WordPress nonce for exploitation.

  • The permission_callback in includes/public/api/class-wprm-api-integrations.php (Line 47) is explicitly set to __return_true.
  • Unauthenticated REST API requests (those without authentication cookies) in WordPress do not require the X-WP-Nonce header unless the endpoint's callback specifically checks for it.
  • Review of api_instacart and get_link_for_recipe confirms no nonce verification is performed.

5. Exploitation Strategy

The goal is to trigger an unauthorized update_post_meta call on a target post.

Step-by-Step Plan:

  1. Identify Target: Choose a target post ID (e.g., ID 1).
  2. Construct Payload: Create a JSON object matching the structure expected by WPRM_Instacart::get_link_for_recipe.
  3. Send Request: Use the http_request tool to send a POST request to the target endpoint.

HTTP Request Details:

  • URL: http://<target-site>/wp-json/wp-recipe-maker/v1/integrations/instacart
  • Method: POST
  • Headers:
    • Content-Type: application/json
  • Body:
{
  "data": {
    "recipeId": 1,
    "servingsSystemCombination": "pwn_check_2026",
    "title": "Malicious Metadata Inject",
    "image_url": "http://example.com/exploit.jpg",
    "ingredients": [
      {
        "name": "Exploit Ingredient",
        "quantity": "1",
        "unit": "packet"
      }
    ]
  }
}

6. Test Data Setup

  1. Target Post: Create a standard WordPress post (if one doesn't exist).
    • wp post create --post_title="Target Post" --post_status=publish
  2. Identify ID: Note the ID returned (e.g., 123).
  3. Verify Initial State: Check that the metadata wprm_instacart_combinations is either empty or does not contain the key pwn_check_2026.
    • wp post meta get 123 wprm_instacart_combinations

7. Expected Results

  • The server should return a 200 OK response.
  • The response body will likely be false or a URL (depending on whether the WPRM_Proxy::call succeeds in the test environment).
  • Regardless of the response body, the database will be updated.

8. Verification Steps

After sending the HTTP request, use WP-CLI to confirm the metadata modification:

wp post meta get <TARGET_ID> wprm_instacart_combinations

Success Criteria: The output should show a serialized array containing the key pwn_check_2026 and a timestamp corresponding to the time of the attack.

9. Alternative Approaches

If the plugin is configured in a way that blocks the REST API, or if the WPRM_Proxy call causes an early exit (unlikely given the code flow):

  • List Metadata Modification: The plugin also contains get_link_for_list (Line 129), but it is triggered differently and uses WPRMPRC_Shopping_List::save_meta (Line 216). Check if wprm-api-manage-lists.php or similar files expose this via a similarly insecure REST endpoint.
  • Varying Post Types: Confirm the IDOR works on page, attachment, and custom post types, not just wprm_recipe, by targeting different IDs. Since update_post_meta is agnostic to post type, this should succeed.
Research Findings
Static analysis — not yet PoC-verified

Summary

The WP Recipe Maker plugin for WordPress is vulnerable to an unauthenticated Insecure Direct Object Reference (IDOR) via its Instacart integration REST API endpoint. Due to a missing authorization check and lack of input validation on the user-supplied 'recipeId' parameter, attackers can modify the 'wprm_instacart_combinations' metadata for any arbitrary post ID on the site.

Vulnerable Code

// includes/public/api/class-wprm-api-integrations.php lines 42-46
register_rest_route( 'wp-recipe-maker/v1', '/integrations/instacart', array(
    'callback' => array( __CLASS__, 'api_instacart' ),
    'methods' => 'POST',
    'permission_callback' => '__return_true',
));

---

// includes/public/class-wprm-instacart.php lines 53-119
public static function get_link_for_recipe( $data ) {
    $recipe_id = intval( $data['recipeId'] );

    // ... (lines 55-111 fetch cached combinations or call Instacart API)

    // Store result for future use.
    $existing_combinations[ $servings_system_combination ] = array(
        'response' => $instacart_response,
        'timestamp' => time(),
    );
    update_post_meta( $recipe_id, 'wprm_instacart_combinations', $existing_combinations );

    // Return Instacart URL.
    return self::get_link_from_response( $instacart_response );
}

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/wp-recipe-maker/10.3.2/includes/public/api/class-wprm-api-integrations.php /home/deploy/wp-safety.org/data/plugin-versions/wp-recipe-maker/10.3.3/includes/public/api/class-wprm-api-integrations.php
--- /home/deploy/wp-safety.org/data/plugin-versions/wp-recipe-maker/10.3.2/includes/public/api/class-wprm-api-integrations.php	2025-01-22 13:05:30.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wp-recipe-maker/10.3.3/includes/public/api/class-wprm-api-integrations.php	2026-02-18 10:27:34.000000000 +0000
@@ -56,6 +56,9 @@
 
 		if ( $data ) {
 			$link = WPRM_Instacart::get_link_for_recipe( $data );
+			if ( is_wp_error( $link ) ) {
+				return $link;
+			}
 			return rest_ensure_response( $link );
 		}
 
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/wp-recipe-maker/10.3.2/includes/public/class-wprm-instacart.php /home/deploy/wp-safety.org/data/plugin-versions/wp-recipe-maker/10.3.3/includes/public/class-wprm-instacart.php
--- /home/deploy/wp-safety.org/data/plugin-versions/wp-recipe-maker/10.3.2/includes/public/class-wprm-instacart.php	2025-09-17 09:42:32.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wp-recipe-maker/10.3.3/includes/public/class-wprm-instacart.php	2026-02-18 10:27:34.000000000 +0000
@@ -47,12 +47,46 @@
 	 *
 	 * @since	9.8.0
 	 * @param	array $data Recipe data.
+	 * @return	string|WP_Error Instacart URL or error on validation failure.
 	 */
 	public static function get_link_for_recipe( $data ) {
 		$recipe_id = intval( $data['recipeId'] );
+
+		// Validate recipeId: post must exist, be a published recipe.
+		$post = get_post( $recipe_id );
+		if ( ! $post ) {
+			return new WP_Error( 'invalid_recipe', __( 'The specified recipe does not exist.', 'wp-recipe-maker' ), array( 'status' => 404 ) );
+		}
+		if ( WPRM_POST_TYPE !== get_post_type( $recipe_id ) ) {
+			return new WP_Error( 'invalid_recipe', __( 'The specified recipe does not exist.', 'wp-recipe-maker' ), array( 'status' => 404 ) );
+		}
+		if ( 'publish' !== get_post_status( $recipe_id ) ) {
+			return new WP_Error( 'invalid_recipe', __( 'The specified recipe does not exist.', 'wp-recipe-maker' ), array( 'status' => 404 ) );
+		}
+
 		$servings_system_combination = sanitize_key( $data['servingsSystemCombination'] );
 
-		// Look for existing combination first.
+		// Validate servingsSystemCombination: format must be servings-system, system 1 or 2, servings integer in range.
+		$parts = explode( '-', $servings_system_combination, 2 );
+		if ( 2 !== count( $parts ) ) {
+			return new WP_Error( 'invalid_combination', __( 'Invalid servings and system combination.', 'wp-recipe-maker' ), array( 'status' => 400 ) );
+		}
+		$system = isset( $parts[1] ) ? $parts[1] : '';
+		if ( ! in_array( $system, array( '1', '2' ), true ) ) {
+			return new WP_Error( 'invalid_combination', __( 'Invalid servings and system combination.', 'wp-recipe-maker' ), array( 'status' => 400 ) );
+		}
+		$servings_raw = isset( $parts[0] ) ? $parts[0] : '';
+		$requested_servings = WPRM_Recipe_Parser::parse_quantity( $servings_raw );
+		if ( ! is_numeric( $requested_servings ) || $requested_servings !== (int) $requested_servings || 1 > $requested_servings ) {
+			return new WP_Error( 'invalid_combination', __( 'Invalid servings and system combination.', 'wp-recipe-maker' ), array( 'status' => 400 ) );
+		}
+		$requested_servings = (int) $requested_servings;
+		$original_parsed = WPRM_Recipe_Parser::parse_quantity( get_post_meta( $recipe_id, 'wprm_servings', true ) );
+		$original_parsed = is_numeric( $original_parsed ) && 0 < $original_parsed ? (int) $original_parsed : 1;
+		$max_servings = max( 100, $original_parsed * 4 );
+		$within_cache_range = ( $requested_servings <= $max_servings );
+
+		// Look for existing combination first (only in-range combinations are cached).
 		$existing_combinations = get_post_meta( $recipe_id, 'wprm_instacart_combinations', true );
 		$existing_combinations = $existing_combinations ? maybe_unserialize( $existing_combinations ) : array();
 
@@ -102,12 +136,14 @@
 		// Call Instacart API.
 		$instacart_response = self::call_instacart_api( 'recipe', $api_data );
 
-		// Store result for future use.
-		$existing_combinations[ $servings_system_combination ] = array(
-			'response' => $instacart_response,
-			'timestamp' => time(),
-		);
-		update_post_meta( $recipe_id, 'wprm_instacart_combinations', $existing_combinations );
+		// Store result for future use only when within the logical range (out-of-range requests still get the link but are not cached).
+		if ( $within_cache_range ) {
+			$existing_combinations[ $servings_system_combination ] = array(
+				'response' => $instacart_response,
+				'timestamp' => time(),
+			);
+			update_post_meta( $recipe_id, 'wprm_instacart_combinations', $existing_combinations );
+		}
 
 		// Return Instacart URL.
 		return self::get_link_from_response( $instacart_response );

Exploit Outline

The exploit uses an unauthenticated POST request to the `/wp-json/wp-recipe-maker/v1/integrations/instacart` REST endpoint. The attacker sends a JSON payload containing `data[recipeId]` set to the target post ID and a non-empty `data[ingredients]` array. Because the endpoint's `permission_callback` is set to `__return_true` and the backend `get_link_for_recipe` function lacks ownership checks, the plugin will proceed to call the Instacart API and eventually execute `update_post_meta` on the target ID. This allows an attacker to inject or overwrite the `wprm_instacart_combinations` metadata on any post, page, or recipe.

Check if your site is affected.

Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.