PushEngage – Web Push Notifications, WooCommerce Automation & Chat Widget <= 4.2.3 - Authenticated (Subscriber+) Information Exposure
Description
The PushEngage – Web Push Notifications, WooCommerce Automation & Chat Widget plugin for WordPress is vulnerable to Sensitive Information Exposure in all versions up to, and including, 4.2.3. 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:L/I:N/A:NTechnical Details
What Changed in the Fix
Changes introduced in v4.2.4
Source Code
WordPress.org SVNtrue` is set, the Abilities API might bypass the `permission_callback` for MCP-enabled clients (which might include a Subscriber-level user role). * `pushengage/get-plugin-info` * `pushengage/get-analytics` * `pushengage/get-segments` * `pushengage/get-audience-groups` *…
Show full research plan
trueis set, the Abilities API might bypass thepermission_callback` for MCP-enabled clients (which might include a Subscriber-level user role).
* `pushengage/get-plugin-info`
* `pushengage/get-analytics`
* `pushengage/get-segments`
* `pushengage/get-audience-groups`
* `pushengage/get-auto-push-settings`
* I'll try `pushengage/get-plugin-info` first.
* *Pre-conditions*:
1. WordPress Abilities API (feature plugin) must be active (or we simulate the environment where PushEngage registers these).
2. PushEngage must be configured with an API key (to have a `user` envelope to leak).
3. Subscriber user must be created.
* *Steps*:
1. Login as Subscriber.
2. Fetch `/wp-json/wp-abilities/v1/call/pushengage/get-plugin-info` via POST.
3. Check if response contains the `user` object.
* *Nonce*: Use `X-WP-Nonce` header with a nonce for the `wp_rest` action.
* *How to get the nonce?*
Log in as Subscriber, navigate to `/wp-admin/`, extract `wpApiSettings
Summary
The PushEngage plugin registers multiple 'abilities' via the WordPress Abilities API and admin-facing dashboard components that fail to properly enforce high-level permissions. This allows authenticated users with Subscriber-level roles to access sensitive account metadata, including owner emails, plan details, and billing-related IDs (like Stripe IDs) by querying REST API endpoints or viewing admin widgets.
Vulnerable Code
// app/DashboardWidget.php:41 public function dashboard_widget() { // Get options from settings $pushengage_settings = Options::get_site_settings(); $hide_widget = $pushengage_settings['misc']['hideDashboardWidget']; // Return if widget is disabled. if ( $hide_widget ) { return; } wp_add_dashboard_widget( 'pe-dashboard-widget', esc_html__( 'PushEngage', 'pushengage' ), array( $this, 'dashboard_widget_callback' ), null, null, 'normal', 'high' ); --- // app/WhatsNew.php:52 public static function is_showing() { global $pagenow; $pushengage_settings = Options::get_site_settings(); $disabled = ArrayHelper::get( $pushengage_settings, 'dismissed_whats_new_notice', false ); return 'plugins.php' === $pagenow && ! $disabled; } --- // app/Integrations/Abilities/AbstractRegistrar.php:115 protected static function unwrap_envelope( $response, $shape = 'object' ) { $data = ( is_array( $response ) && isset( $response['data'] ) && is_array( $response['data'] ) ) ? $response['data'] : array(); if ( 'rows' !== $shape ) { return $data; }
Security Fix
@@ -266,14 +266,13 @@ * @return void */ public function init_admin_options() { + if ( ! current_user_can( 'manage_options' ) || false === $this->is_pushengage_active() ) { + return; + } // Add Meta box when PushEngage site is not connected. add_action( 'add_meta_boxes', array( $this, 'add_pushengage_site_not_connected_metabox' ) ); - if ( ! is_user_logged_in() || false === $this->is_pushengage_active() ) { - return; - } - add_action( 'add_meta_boxes', array( $this, 'add_pushengage_settings_metabox' ) ); // action hook, to handle the case of draft and schedule post to send notification. @@ -747,6 +746,9 @@ * @return void */ public function load_block_editor_scripts() { + if ( ! current_user_can( 'manage_options' ) ) { + return; + } $screen = get_current_screen(); if ( ! $screen || ! in_array( $screen->post_type, Options::get_allowed_post_types_for_auto_push(), true ) ) { return; @@ -41,6 +41,9 @@ * @return void */ public function dashboard_widget() { + if ( ! current_user_can( 'manage_options' ) ) { + return; + } // Get options from settings $pushengage_settings = Options::get_site_settings(); $hide_widget = $pushengage_settings['misc']['hideDashboardWidget']; @@ -52,7 +52,7 @@ $pushengage_settings = Options::get_site_settings(); $disabled = ArrayHelper::get( $pushengage_settings, 'dismissed_whats_new_notice', false ); - return 'plugins.php' === $pagenow && ! $disabled; + return current_user_can( 'manage_options' ) && 'plugins.php' === $pagenow && ! $disabled; }
Exploit Outline
The vulnerability is exploitable by an authenticated user with Subscriber-level permissions or higher. An attacker can use a valid REST API nonce (extractable from the WordPress dashboard) to send POST requests to endpoints registered by the Abilities API, such as `/wp-json/wp-abilities/v1/call/pushengage/get-plugin-info`. Because the `unwrap_envelope` logic in `AbstractRegistrar.php` was incomplete for certain data shapes, and capability checks were insufficient in `Core.php`, the API response would leak the internal PushEngage 'user' envelope. This envelope contains sensitive information including the account owner's email address, active sites, and external payment provider identifiers (e.g., Stripe IDs).
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.