WPPizza – A Restaurant Plugin <= 3.19.9 - Authenticated (Subscriber+) Information Exposure
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:NTechnical Details
What Changed in the Fix
Changes introduced in v3.20
Source Code
WordPress.org SVNThis 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 inclasses/admin/class.wppizza.wp_admin.php) - Vulnerable File:
ajax/admin.ajax.wppizza.php - HTTP Method: POST
- Authentication: Authenticated (Subscriber+)
- Required Parameters:
action:wppizza_admin_ajaxvars[nonce]: A validwppizza_ajax_nonce.vars[field]:update-dashboard-widget(triggers sales report exposure).
3. Code Flow
- Registration: In
classes/admin/class.wppizza.wp_admin.php, the hookwp_ajax_wppizza_admin_ajaxis registered to the methodset_admin_ajax. This hook is only for logged-in users. - Nonce Exposure: The same class registers
wppizza_ajax_nonceon theadmin_footerhook (Line 42), ensuring the nonce is present in the HTML for any user viewing the admin area. - Entry Point: When a request is sent to
admin-ajax.php?action=wppizza_admin_ajax, the functionset_admin_ajaxrequiresajax/admin.ajax.wppizza.php. - Verification (Insufficient):
ajax/admin.ajax.wppizza.phpchecks the nonce (Lines 26-36) usingwp_verify_nonce( $_POST['vars']['nonce'] , $wppizza_ajax_nonce ). It does not check user capabilities. - Execution: After verification, it triggers
do_action('wppizza_ajax_admin', $wppizza_options). - Sink: The class
WPPIZZA_WP_ADMINlistens to this action and executesadmin_ajax(Line 35). Insideadmin_ajax, ifvars[field]is set toupdate-dashboard-widget, it instantiatesWPPIZZA_DASHBOARD_WIDGETSand callswppizza_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).
- Log in as a Subscriber.
- Navigate to
/wp-admin/index.php. - The plugin enqueues a script or prints the nonce in the footer. Use
browser_evalto find it. Based on the source, it is likely inside a script tag or localized object. - JS Search Strategy:
- Check for
wppizza_admin_varsor similar localization keys. - Check for the string
wppizza_ajax_noncein the page source. - Example:
browser_eval("window.wppizza_admin_vars?.nonce")or inspecting the HTML for a hidden input/script.
- Check for
5. Exploitation Strategy
Execute the following steps using the http_request tool:
Step 1: Obtain Nonce
- Navigate to
/wp-admin/index.phpas 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
- Plugin Configuration: Ensure WPPizza is installed and activated.
- 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.
- 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 listsadmin-view-orderandadmin-order-historyas types that check thewppizza_ajax_nonce. If this nonce is shared, a Subscriber can view full details of any order by iterating IDs.
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
@@ -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 ***********************/ @@ -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.