CVE-2026-40796

WPPizza – A Restaurant Plugin <= 3.19.9 - Authenticated (Subscriber+) Information Exposure

mediumExposure of Sensitive Information to an Unauthorized Actor
4.3
CVSS Score
4.3
CVSS Score
medium
Severity
3.20
Patched in
6d
Time to patch

Description

The WPPizza – A Restaurant Plugin plugin for WordPress is vulnerable to Sensitive Information Exposure in all versions up to, and including, 3.19.9. This makes it possible for authenticated attackers, with Subscriber-level access and above, to extract sensitive user or configuration data.

CVSS Vector Breakdown

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

Technical Details

Affected versions<=3.19.9
PublishedApril 29, 2026
Last updatedMay 4, 2026
Affected pluginwppizza

What Changed in the Fix

Changes introduced in v3.20

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

This research plan outlines the exploitation of **CVE-2026-40796**, an Information Exposure vulnerability in the **WPPizza** plugin. The vulnerability stems from the lack of capability checks in the plugin's administrative AJAX handlers, allowing any authenticated user (including **Subscribers**) to…

Show full research plan

This research plan outlines the exploitation of CVE-2026-40796, an Information Exposure vulnerability in the WPPizza plugin. The vulnerability stems from the lack of capability checks in the plugin's administrative AJAX handlers, allowing any authenticated user (including Subscribers) to access sensitive sales data or order information by utilizing a nonce that is globally available in the WordPress admin footer.

1. Vulnerability Summary

The vulnerability exists in the handling of the wppizza_admin_ajax action. While the plugin implements a WordPress nonce check to prevent CSRF, it fails to perform a capability check (e.g., current_user_can('manage_options')) to verify that the user has administrative privileges.

Crucially, the nonce required for this check (wppizza_ajax_nonce) is rendered in the admin footer for all logged-in users who can access the dashboard, including those with the Subscriber role. Once a Subscriber obtains this nonce, they can trigger administrative AJAX functions that expose sensitive sales reports and configuration data.

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • AJAX Action: wppizza_admin_ajax (registered in classes/admin/class.wppizza.wp_admin.php)
  • Vulnerable File: ajax/admin.ajax.wppizza.php
  • HTTP Method: POST
  • Authentication: Authenticated (Subscriber+)
  • Required Parameters:
    • action: wppizza_admin_ajax
    • vars[nonce]: A valid wppizza_ajax_nonce.
    • vars[field]: update-dashboard-widget (triggers sales report exposure).

3. Code Flow

  1. Registration: In classes/admin/class.wppizza.wp_admin.php, the hook wp_ajax_wppizza_admin_ajax is registered to the method set_admin_ajax. This hook is only for logged-in users.
  2. Nonce Exposure: The same class registers wppizza_ajax_nonce on the admin_footer hook (Line 42), ensuring the nonce is present in the HTML for any user viewing the admin area.
  3. Entry Point: When a request is sent to admin-ajax.php?action=wppizza_admin_ajax, the function set_admin_ajax requires ajax/admin.ajax.wppizza.php.
  4. Verification (Insufficient): ajax/admin.ajax.wppizza.php checks the nonce (Lines 26-36) using wp_verify_nonce( $_POST['vars']['nonce'] , $wppizza_ajax_nonce ). It does not check user capabilities.
  5. Execution: After verification, it triggers do_action('wppizza_ajax_admin', $wppizza_options).
  6. Sink: The class WPPIZZA_WP_ADMIN listens to this action and executes admin_ajax (Line 35). Inside admin_ajax, if vars[field] is set to update-dashboard-widget, it instantiates WPPIZZA_DASHBOARD_WIDGETS and calls wppizza_do_dashboard_widget_sales(), which prints sales summaries to the output.

4. Nonce Acquisition Strategy

The nonce is rendered in the footer of any admin page (e.g., /wp-admin/profile.php or /wp-admin/index.php).

  1. Log in as a Subscriber.
  2. Navigate to /wp-admin/index.php.
  3. The plugin enqueues a script or prints the nonce in the footer. Use browser_eval to find it. Based on the source, it is likely inside a script tag or localized object.
  4. JS Search Strategy:
    • Check for wppizza_admin_vars or similar localization keys.
    • Check for the string wppizza_ajax_nonce in the page source.
    • Example: browser_eval("window.wppizza_admin_vars?.nonce") or inspecting the HTML for a hidden input/script.

5. Exploitation Strategy

Execute the following steps using the http_request tool:

Step 1: Obtain Nonce

  • Navigate to /wp-admin/index.php as a Subscriber.
  • Search the HTML response for the wppizza_ajax_nonce.
  • Note: The nonce action is wppizza_ajax_nonce.

Step 2: Request Sensitive Information

  • URL: http://localhost:8080/wp-admin/admin-ajax.php
  • Method: POST
  • Content-Type: application/x-www-form-urlencoded
  • Body:
    action=wppizza_admin_ajax&vars[field]=update-dashboard-widget&vars[nonce]=[EXTRACTED_NONCE]
    

6. Test Data Setup

  1. Plugin Configuration: Ensure WPPizza is installed and activated.
  2. Order Data: Use WP-CLI to create at least one dummy order so the sales widget has data to return:
    • wp wppizza order create ... (if available) or manually through the UI as admin.
  3. Attacker User: Create a Subscriber user:
    • wp user create attacker attacker@example.com --role=subscriber --user_pass=password

7. Expected Results

  • The response should be an HTTP 200 OK.
  • The body should contain HTML markup generated by wppizza_do_dashboard_widget_sales(), including sales statistics, revenue totals, or order counts which should be restricted to administrators.

8. Verification Steps

  • Log Check: Check the response for keywords like "Sales", "Total", or currency symbols formatted by wppizza_format_price.
  • Access Comparison: Confirm that a Subscriber normally cannot see the "WPPizza Sales" widget on the dashboard.

9. Alternative Approaches

If update-dashboard-widget does not return enough data, attempt to access order history via the frontend AJAX file ajax/ajax.wppizza.php:

  • Action: wppizza_ajax
  • Parameters: vars[type]=admin-view-order&vars[id]=1&vars[nonce]=[EXTRACTED_NONCE]
  • Note: ajax/ajax.wppizza.php (Line 68) explicitly lists admin-view-order and admin-order-history as types that check the wppizza_ajax_nonce. If this nonce is shared, a Subscriber can view full details of any order by iterating IDs.
Research Findings
Static analysis — not yet PoC-verified

Summary

The WPPizza plugin is vulnerable to information exposure because its AJAX handlers for administrative functions check only for a valid nonce but fail to verify user capabilities. Since the required nonce is globally exposed in the WordPress admin footer to all logged-in users, including Subscribers, an attacker can extract sensitive sales data, revenue statistics, and full order details.

Vulnerable Code

/* ajax/admin.ajax.wppizza.php Lines 26-36 */
$wppizza_ajax_nonce = '' . WPPIZZA_PREFIX . '_ajax_nonce';
/* --- skip nonce check for all '...nag_dismiss' notices --- */
if( isset( $_POST['vars']['type'] ) && stristr($_POST['vars']['type'], 'nag_dismiss') !== false ){
	//skip nonce check
}else{
	if (! isset( $_POST['vars']['nonce'] ) || !wp_verify_nonce(  $_POST['vars']['nonce'] , $wppizza_ajax_nonce ) ) {
		header('HTTP/1.0 403 Forbidden [A]', true, 403);
		print"Forbidden [A]. Invalid Nonce.";
		exit; //just for good measure
	}
}

---

/* ajax/ajax.wppizza.php Lines 65-73 */
if(isset($_POST['vars']['type']) && in_array( $_POST['vars']['type'], array('admin-delete-order', 'admin-change-status', 'admin-view-order', 'admin-order-history') ) ){
	$wppizza_ajax_nonce = '' . WPPIZZA_PREFIX . '_ajax_nonce';
	if (! isset( $_POST['vars']['nonce'] ) || !wp_verify_nonce(  $_POST['vars']['nonce'] , $wppizza_ajax_nonce ) ) {
		header('HTTP/1.0 403 Forbidden [F]', true, 403);
		print"Forbidden [F]. Invalid Nonce.";
		exit; //just for good measure
	}
}

---

/* classes/admin/class.wppizza.wp_admin.php Lines 42-50 */
	    /******************
	    	ajax nonce in footer for all admin pages
	    	
	    	Note: also needed for non-wppizza admin pages for: 
	    	-	dashboard widgets, 
	    	-	order notifications on non-wppizza pages, 
	    	-	dismissal of install notices
	    	getc
	 	******************/ 
		add_action('admin_footer', array($this, 'wppizza_ajax_nonce'));

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/wppizza/3.19.9/ajax/admin.ajax.wppizza.php /home/deploy/wp-safety.org/data/plugin-versions/wppizza/3.20/ajax/admin.ajax.wppizza.php
--- /home/deploy/wp-safety.org/data/plugin-versions/wppizza/3.19.9/ajax/admin.ajax.wppizza.php	2026-02-16 12:01:54.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wppizza/3.20/ajax/admin.ajax.wppizza.php	2026-04-14 17:39:04.000000000 +0000
@@ -1,7 +1,7 @@
 <?php
-if(!defined('DOING_AJAX') || !DOING_AJAX){
+if( !defined('DOING_AJAX') || !DOING_AJAX || !defined('ABSPATH') ){
 	header('HTTP/1.0 400 Bad Request', true, 400);
-	print"you cannot call this script directly";
+	print"You cannot call this script directly.";
   exit; //just for good measure
 }
 /**testing variables ***********************/
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/wppizza/3.19.9/ajax/ajax.wppizza.php /home/deploy/wp-safety.org/data/plugin-versions/wppizza/3.20/ajax/ajax.wppizza.php
--- /home/deploy/wp-safety.org/data/plugin-versions/wppizza/3.19.9/ajax/ajax.wppizza.php	2025-08-18 17:16:36.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wppizza/3.20/ajax/ajax.wppizza.php	2026-04-14 17:39:04.000000000 +0000
@@ -2,9 +2,9 @@
 /**************************************************
 	[ajax only]
 **************************************************/
-if(!defined('DOING_AJAX') || !DOING_AJAX){
+if( !defined('DOING_AJAX') || !DOING_AJAX || !defined('ABSPATH') ){
 	header('HTTP/1.0 400 Bad Request', true, 400);
-	print"you cannot call this script directly";
+	print"You cannot call this script directly.";
   exit; //just for good measure
 }
 /**************************************************
@@ -40,23 +40,69 @@
 /**************************************************
 	[add globals to use]
 **************************************************/
-global $wppizza_options, $blog_id;
+global $wppizza_options, $blog_id, $current_user;
 
 
-/**************************************************
+/*\/*\/*\/*\/*\/*\/*\/*\/*\/*\/*\/*\/*\/*\/*\/*\/*\/*\/*\/*\/*\/*\/*\/*\/*\/*\/*\/*\/*\/*\/*\/*\/*\/*\/*\/*\/*\/*\/*\/*\/*\/*\/*\/*\/*\/*\/*
+*
+*
+*
+*	Nonce/Auth/Credentials/Caps checks 
+*
+*
+*
+*\/*\/*\/*\/*\/*\/*\/*\/*\/*\/*\/*\/*\/*\/*\/*\/*\/*\/*\/*\/*\/*\/*\/*\/*\/*\/*\**\/*\/*\/*\/*\/*\/*\/*\/*\/*\/*\/*\/*\/*\**\/*\/*\/*\/*\/*/
+
+/*-------------------------------------------------
 	[some frontend ajax calls should check the nonce too]
 	to be expanded if needs be.....
-**************************************************/
+-------------------------------------------------*/
 if(isset($_POST['vars']['type']) && in_array( $_POST['vars']['type'], array('admin-delete-order', 'admin-change-status', 'admin-view-order', 'admin-order-history') ) ){
 	$wppizza_ajax_nonce = '' . WPPIZZA_PREFIX . '_ajax_nonce';
 	if (! isset( $_POST['vars']['nonce'] ) || !wp_verify_nonce(  $_POST['vars']['nonce'] , $wppizza_ajax_nonce ) ) {
 		header('HTTP/1.0 403 Forbidden [F]', true, 403);
 		print"Forbidden [F]. Invalid Nonce.";
-		exit; //just for good measure
+		exit() ; //just for good measure
 	}
 }
 
+/*-------------------------------------------------
+	additional auth/capability checks 
+	for certain order (history) related ajax calls
+-------------------------------------------------*/
+if(isset($_POST['vars']['type']) && in_array( $_POST['vars']['type'], array('admin-delete-order', 'admin-order-history', 'admin-view-order', 'admin-change-status') ) ){
+	//logged in user only with wppizza_cap_orderhistory privileges
+	if (!is_user_logged_in() || empty($current_user->allcaps['wppizza_cap_orderhistory'])){		
+		$obj = array();
+		$obj['access_prohibited'] = __('Sorry, you are not allowed to access this page.', 'default' );
+		print"".json_encode($obj)."";
+		exit();
+	}
+}
+/*-------------------------------------------------
+	Delete order needs additional credentials 
+-------------------------------------------------*/
+if( isset($_POST['vars']['type']) && $_POST['vars']['type']=='admin-delete-order' && !empty($_POST['vars']['uoKey']) ){
+
+	/* missing wppizza_cap_delete_order capabilities */
+	if(!current_user_can('wppizza_cap_delete_order')){
+		$obj['update_prohibited'] = __('Error: You need order delete permissions to perform this action.', 'wppizza-admin');
+		print"".json_encode($obj)."";
+	exit();
+	}
+}
+
+/*-------------------------------------------------
+	saving/update disabled by constant
+	for selected actions
+-------------------------------------------------*/
+if(isset($_POST['vars']['type']) && in_array( $_POST['vars']['type'], array('admin-delete-order', 'admin-change-status') ) && !empty($_POST['vars']['uoKey']) ){
+	if(WPPIZZA_DEV_ADMIN_NO_SAVE){
+		$obj['update_prohibited'] = __('Update Prohibited', 'wppizza-admin');
+		print"".json_encode($obj)."";
+	exit();
+	}
+}
+

Exploit Outline

1. Log in to the WordPress site as a user with Subscriber privileges. 2. Access the dashboard (e.g., `/wp-admin/index.php`) and locate the `wppizza_ajax_nonce` within the page source or localized JS variables (it is automatically added to the footer for all logged-in users). 3. To extract sales information: Send a POST request to `/wp-admin/admin-ajax.php` with the parameters `action=wppizza_admin_ajax`, `vars[nonce]=[EXTRACTED_NONCE]`, and `vars[field]=update-dashboard-widget`. The response will contain the HTML for the sales widget, including revenue and order totals. 4. To extract specific order details: Send a POST request to `/wp-admin/admin-ajax.php` with the parameters `action=wppizza_ajax`, `vars[nonce]=[EXTRACTED_NONCE]`, `vars[type]=admin-view-order`, and `vars[id]=[TARGET_ORDER_ID]`. The response will contain detailed PII and order content for the specified ID.

Check if your site is affected.

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