WP Recipe Maker <= 10.3.2 - Insecure Direct Object Reference to Unauthenticated Arbitrary Post Metadata Modification via 'recipeId' Parameter
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:NTechnical Details
<=10.3.2What Changed in the Fix
Changes introduced in v10.3.3
Source Code
WordPress.org SVN# 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 callsupdate_post_meta).
3. Code Flow
- Entry Point: The REST route is registered in
includes/public/api/class-wprm-api-integrations.php.- Line 47:
permission_callbackis set to__return_true. - Line 45: The route is
/integrations/instacart.
- Line 47:
- 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'] ).
- Line 57:
- Vulnerable Logic:
WPRM_Instacart::get_link_for_recipeinincludes/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 providedservingsSystemCombinationis new, it proceeds. - Line 75-108: Prepares
$api_dataand 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.
- Line 54:
4. Nonce Acquisition Strategy
This vulnerability does not require a WordPress nonce for exploitation.
- The
permission_callbackinincludes/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-Nonceheader unless the endpoint's callback specifically checks for it. - Review of
api_instacartandget_link_for_recipeconfirms 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:
- Identify Target: Choose a target post ID (e.g., ID 1).
- Construct Payload: Create a JSON object matching the structure expected by
WPRM_Instacart::get_link_for_recipe. - Send Request: Use the
http_requesttool to send aPOSTrequest 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
- Target Post: Create a standard WordPress post (if one doesn't exist).
wp post create --post_title="Target Post" --post_status=publish
- Identify ID: Note the ID returned (e.g.,
123). - Verify Initial State: Check that the metadata
wprm_instacart_combinationsis either empty or does not contain the keypwn_check_2026.wp post meta get 123 wprm_instacart_combinations
7. Expected Results
- The server should return a
200 OKresponse. - The response body will likely be
falseor a URL (depending on whether theWPRM_Proxy::callsucceeds 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 usesWPRMPRC_Shopping_List::save_meta(Line 216). Check ifwprm-api-manage-lists.phpor 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 justwprm_recipe, by targeting different IDs. Sinceupdate_post_metais agnostic to post type, this should succeed.
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
@@ -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 ); } @@ -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.