CVE-2026-3155

OneSignal – Web Push Notifications <= 3.8.0 - Missing Authorization to Authenticated (Subscriber+) Post Meta Deletion via 'post_id'

lowMissing Authorization
3.1
CVSS Score
3.1
CVSS Score
low
Severity
3.8.1
Patched in
0d
Time to patch

Description

The OneSignal – Web Push Notifications plugin for WordPress is vulnerable to authorization bypass in versions up to, and including, 3.8.0. This is due to the plugin not properly verifying that a user is authorized to perform an action. This makes it possible for authenticated attackers, with subscriber-level access and above, to delete OneSignal metadata for arbitrary posts.

CVSS Vector Breakdown

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

Technical Details

Affected versions<=3.8.0
PublishedApril 15, 2026
Last updatedApril 15, 2026

What Changed in the Fix

Changes introduced in v3.8.1

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Vulnerability Research Plan: CVE-2026-3155 ## 1. Vulnerability Summary The **OneSignal – Web Push Notifications** plugin (<= 3.8.0) contains a missing authorization vulnerability in its AJAX handling logic. Specifically, the function `has_metadata()` registered via the `wp_ajax_has_metadata` hook…

Show full research plan

Vulnerability Research Plan: CVE-2026-3155

1. Vulnerability Summary

The OneSignal – Web Push Notifications plugin (<= 3.8.0) contains a missing authorization vulnerability in its AJAX handling logic. Specifically, the function has_metadata() registered via the wp_ajax_has_metadata hook in v2/onesignal-admin.php fails to perform any capability checks (e.g., current_user_can()) or nonce verification (e.g., check_ajax_referer()).

This allows any authenticated user, including those with Subscriber permissions, to delete specific post metadata keys (status, recipients, response_body) for any arbitrary post by providing a post_id. This metadata is used to track notification delivery status.

2. Attack Vector Analysis

  • Endpoint: /wp-admin/admin-ajax.php
  • AJAX Action: has_metadata
  • HTTP Method: GET (The code uses $_GET['post_id'])
  • Vulnerable Parameter: post_id
  • Authentication: Required (Subscriber or higher)
  • Preconditions: The plugin must be operating in "V2" mode. Per onesignal.php, this occurs when the plugin is not a "new install" and has not been "migrated" to V3.

3. Code Flow

  1. Entry Point: An authenticated user sends a GET request to admin-ajax.php?action=has_metadata&post_id=[TARGET_ID].
  2. Hook Registration: In v2/onesignal-admin.php, the hook is registered:
    add_action('wp_ajax_has_metadata', 'has_metadata');
    
  3. Execution: The has_metadata() function is called:
    • It retrieves $post_id from $_GET['post_id'] (line 30).
    • It filters the ID using FILTER_SANITIZE_NUMBER_INT.
    • It retrieves post meta for recipients, status, and response_body (lines 37-49).
  4. Vulnerable Sink: The function then unconditionally calls delete_post_meta for these keys:
    // v2/onesignal-admin.php lines 52-54
    delete_post_meta($post_id, 'status');
    delete_post_meta($post_id, 'recipients');
    delete_post_meta($post_id, 'response_body');
    
  5. Response: The function returns the deleted values in a JSON-encoded array and exits.

4. Nonce Acquisition Strategy

Based on the analysis of v2/onesignal-admin.php, the has_metadata function does not implement a nonce check.

  • Line 27 begins the function definition.
  • There is no call to check_ajax_referer() or wp_verify_nonce().
  • Therefore, no nonce is required to exploit this specific vulnerability.

5. Exploitation Strategy

The goal is to delete the OneSignal notification metadata for a specific post using a Subscriber-level account.

Step-by-Step Plan:

  1. Target Identification: Identify a post_id of a published post.
  2. Authentication: Log in to WordPress as a Subscriber.
  3. Trigger Deletion: Send a GET request to the AJAX endpoint.
    • URL: http://localhost:8080/wp-admin/admin-ajax.php?action=has_metadata&post_id=[POST_ID]
    • Headers: Provide the Subscriber's session cookies.
  4. Verify Deletion: Check if the metadata keys are removed from the database for that post_id.

6. Test Data Setup

To successfully test this, the environment must be forced into the vulnerable V2 code path and have existing metadata:

  1. Force V2 Mode:
    wp option update OneSignalWPSetting '{"app_id":"12345"}' --format=json
    wp option update onesignal_plugin_migrated 0
    
  2. Create Target Post:
    POST_ID=$(wp post create --post_title="Target Post" --post_status=publish --porcelain)
    
  3. Inject Metadata: Add the specific keys targeted by the function:
    wp post meta add $POST_ID status "sent"
    wp post meta add $POST_ID recipients 100
    wp post meta add $POST_ID response_body "success"
    
  4. Create Subscriber:
    wp user create attacker attacker@example.com --role=subscriber --user_pass=password
    

7. Expected Results

  • The AJAX request should return a JSON response containing the values of the metadata before they were deleted.
    • Example: {"recipients":"100","status_code":"sent","response_body":"success"}
  • The HTTP status code should be 200 OK.

8. Verification Steps

After the HTTP request, use WP-CLI to confirm the metadata is gone:

# These should all return empty strings
wp post meta get [POST_ID] status
wp post meta get [POST_ID] recipients
wp post meta get [POST_ID] response_body

9. Alternative Approaches

If the plugin automatically migrates to V3 upon admin access (preventing V2 code execution), the exploit can be attempted by:

  1. Directly calling the AJAX action immediately after setting the options via CLI, before any admin user navigates the site.
  2. Checking if v3/onesignal-admin/onesignal-admin.php (not provided in source but part of the plugin) contains a similar unauthenticated or under-privileged meta-deletion logic. Given the CVSS and description, it is highly likely that this logic was either replicated or left reachable in the affected versions.
Research Findings
Static analysis — not yet PoC-verified

Summary

The OneSignal plugin for WordPress (versions <= 3.8.0) is vulnerable to an authorization bypass due to a missing capability check and nonce verification in its 'has_metadata' AJAX handler. This allows authenticated users, including those with subscriber-level permissions, to delete OneSignal notification metadata (status, recipients, and response body) for any arbitrary post by providing its ID.

Vulnerable Code

// v2/onesignal-admin.php line 26
add_action('wp_ajax_has_metadata', 'has_metadata');
function has_metadata()
{
    $post_id = isset($_GET['post_id']) ?
            (filter_var($_GET['post_id'], FILTER_SANITIZE_NUMBER_INT))
            : '';

    if (is_null($post_id)) {
        $data = array('error' => 'could not get post id');
    } else {
        $recipients = get_post_meta($post_id, 'recipients');
        if ($recipients && is_array($recipients)) {
            $recipients = $recipients[0];
        }

        $status = get_post_meta($post_id, 'status');
        if ($status && is_array($status)) {
            $status = $status[0];
        }

        $response_body = get_post_meta($post_id, 'response_body');
        if ($response_body && is_array($response_body)) {
            $response_body = $response_body[0];
        }

        // reset meta
        // v2/onesignal-admin.php lines 53-55
        delete_post_meta($post_id, 'status');
        delete_post_meta($post_id, 'recipients');
        delete_post_meta($post_id, 'response_body');

        $data = array('recipients' => $recipients, 'status_code' => $status, 'response_body' => $response_body);

    }

    echo wp_json_encode($data);

    exit;
}

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/onesignal-free-web-push-notifications/3.8.0/onesignal.php /home/deploy/wp-safety.org/data/plugin-versions/onesignal-free-web-push-notifications/3.8.1/onesignal.php
--- /home/deploy/wp-safety.org/data/plugin-versions/onesignal-free-web-push-notifications/3.8.0/onesignal.php	2026-01-22 23:02:16.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/onesignal-free-web-push-notifications/3.8.1/onesignal.php	2026-04-07 23:49:20.000000000 +0000
@@ -6,7 +6,7 @@
  * Plugin Name: OneSignal Push Notifications
  * Plugin URI: https://onesignal.com/
  * Description: Free web push notifications.
- * Version: 3.8.0
+ * Version: 3.8.1
  * Author: OneSignal
  * Author URI: https://onesignal.com
  * License: MIT
@@ -18,7 +18,7 @@
 define('ONESIGNAL_URI_REVEAL_PROJECT_NUMBER', 'reveal_project_number=true');
 
 // Plugin version - must match Version in plugin header
-define('ONESIGNAL_PLUGIN_VERSION', '030800');
+define('ONESIGNAL_PLUGIN_VERSION', '030801');
 
 // Constants for plugin versions
 define('ONESIGNAL_VERSION_V2', 'v2');
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/onesignal-free-web-push-notifications/3.8.0/readme.txt /home/deploy/wp-safety.org/data/plugin-versions/onesignal-free-web-push-notifications/3.8.1/readme.txt
--- /home/deploy/wp-safety.org/data/plugin-versions/onesignal-free-web-push-notifications/3.8.0/readme.txt	2026-01-22 23:02:16.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/onesignal-free-web-push-notifications/3.8.1/readme.txt	2026-04-07 23:49:20.000000000 +0000
@@ -4,7 +4,7 @@
 Tags: push notification, push notifications, desktop notifications, mobile notifications, chrome push, android, android notification, android notifications, android push, desktop notification, firefox, firefox push, mobile, mobile notification, notification, notifications, notify, onesignal, push, push messages, safari, safari push, web push, chrome
 Requires at least: 3.8
 Tested up to: 6.9
-Stable tag: 3.8.0
+Stable tag: 3.8.1
 License: GPLv2 or later
 License URI: http://www.gnu.org/licenses/gpl-2.0.html
 
@@ -64,6 +64,9 @@
 
 == Changelog ==
 
+= 3.8.1 =
+- Improve AJAX handler validation
+
 = 3.8.0 =
 - add HTML meta tag for improved debugging
 
--- /home/deploy/wp-safety.org/data/plugin-versions/onesignal-free-web-push-notifications/3.8.0/v2/onesignal-admin.php	2024-12-20 03:36:24.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/onesignal-free-web-push-notifications/3.8.1/v2/onesignal-admin.php	2026-04-07 23:49:20.000000000 +0000
@@ -18,47 +18,45 @@
     if ($post) {
         wp_register_script('notice_script', plugins_url('notice.js', __FILE__), array('jquery'), '1.1', true);
         wp_enqueue_script('notice_script');
-        wp_localize_script('notice_script', 'ajax_object', array('ajax_url' => admin_url('admin-ajax.php'), 'post_id' => $post->ID));
+        wp_localize_script('notice_script', 'ajax_object', array(
+            'ajax_url' => admin_url('admin-ajax.php'),
+            'post_id' => $post->ID,
+            'nonce' => wp_create_nonce('onesignal_has_metadata'),
+        ));
     }
 }
 
 add_action('wp_ajax_has_metadata', 'has_metadata');
 function has_metadata()
 {
-    $post_id = isset($_GET['post_id']) ?
-            (filter_var($_GET['post_id'], FILTER_SANITIZE_NUMBER_INT))
-            : '';
-
-    if (is_null($post_id)) {
-        $data = array('error' => 'could not get post id');
-    } else {
-        $recipients = get_post_meta($post_id, 'recipients');
-        if ($recipients && is_array($recipients)) {
-            $recipients = $recipients[0];
-        }
-
-        $status = get_post_meta($post_id, 'status');
-        if ($status && is_array($status)) {
-            $status = $status[0];
-        }
-
-        $response_body = get_post_meta($post_id, 'response_body');
-        if ($response_body && is_array($response_body)) {
-            $response_body = $response_body[0];
-        }
-
-        // reset meta
-        delete_post_meta($post_id, 'status');
-        delete_post_meta($post_id, 'recipients');
-        delete_post_meta($post_id, 'response_body');
+    check_ajax_referer('onesignal_has_metadata', 'nonce');
 
-        $data = array('recipients' => $recipients, 'status_code' => $status, 'response_body' => $response_body);
+    $post_id = isset($_GET['post_id']) ? intval($_GET['post_id']) : 0;
 
+    if (!$post_id || !current_user_can('edit_post', $post_id)) {
+        wp_send_json_error(array('error' => 'Unauthorized'), 403);
     }
 
-    echo wp_json_encode($data);
+    $recipients = get_post_meta($post_id, 'recipients');
+    if ($recipients && is_array($recipients)) {
+        $recipients = $recipients[0];
+    }
+
+    $status = get_post_meta($post_id, 'status');
+    if ($status && is_array($status)) {
+        $status = $status[0];
+    }
+
+    $response_body = get_post_meta($post_id, 'response_body');
+    if ($response_body && is_array($response_body)) {
+        $response_body = $response_body[0];
+    }
+
+    delete_post_meta($post_id, 'status');
+    delete_post_meta($post_id, 'recipients');
+    delete_post_meta($post_id, 'response_body');
 
-    exit;
+    wp_send_json(array('recipients' => $recipients, 'status_code' => $status, 'response_body' => $response_body));
 }

Exploit Outline

To exploit this vulnerability, an attacker needs a Subscriber-level account on the WordPress site. The attacker must then identify the ID of a post they wish to target. By sending a GET request to the `/wp-admin/admin-ajax.php` endpoint with the parameters `action=has_metadata` and `post_id=[target_id]`, the plugin's `has_metadata` function will execute without any permission or nonce checks. The server will respond with the current values of the `recipients`, `status`, and `response_body` metadata for that post, and subsequently delete those metadata records from the database.

Check if your site is affected.

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