Contents

CVE-2025-58604 Analysis & POC

The vulnerability occurs in the Mail Mint plugin for WordPress prior to version 1.18.6. This flaw allows attackers to directly interact with the database — potentially leading to information disclosure or data theft.

  • Local WordPress & Debugging: Local WordPress and Debugging.
  • Mail Mint: v1.18.5 (vulnerable) and v1.18.6 (patched)
  • Diff tool: meld or any comparison tool to visualize the code difference between versions

The vulnerable function directly inserts user input into an SQL query without using proper sanitization or query preparation, leading to an SQL Injection vulnerability.

Use any diff tool to compare the vulnerable and patched versions. There is a clear difference in the file /app/Utilities/Helper/Import.php.

public static function get_wp_users_by_learndash_with_limit_offset($courses, $number = 5, $offset = 0)
{
    // other logic

    global $wpdb;
    $total_query = "SELECT COUNT(DISTINCT user_id) as total
                    FROM {$wpdb->prefix}usermeta
                    WHERE meta_key IN ('" . implode("', '", $keys) . "')";

    $total = $wpdb->get_var($total_query); //phpcs:ignore

    // Final query to retrieve user IDs with limit and offset.
    $final_query = "SELECT user_id
                    FROM {$wpdb->prefix}usermeta
                    WHERE meta_key IN ('" . implode("', '", $keys) . "')
                    GROUP BY user_id
                    LIMIT $number OFFSET $offset";

    $users = $wpdb->get_results($final_query, ARRAY_A); //phpcs:ignore

    // other logic
}

The patch replaces direct string concatenation with the safer $wpdb->prepare() method.

public static function get_wp_users_by_learndash_with_limit_offset($courses, $number = 5, $offset = 0)
{
    // other logic

    global $wpdb;
    // Total query (safe with prepare)
    $total_query = $wpdb->prepare(
        "SELECT COUNT(DISTINCT user_id) as total
        FROM {$wpdb->usermeta}
        WHERE meta_key IN ($placeholders)",
        $keys
    );

    $total = $wpdb->get_var($total_query); //phpcs:ignore

    // Final query with LIMIT & OFFSET (safe with prepare)
    $final_query = $wpdb->prepare(
        "SELECT user_id
        FROM {$wpdb->usermeta}
        WHERE meta_key IN ($placeholders)
        GROUP BY user_id
        LIMIT %d OFFSET %d",
        array_merge($keys, array($number, $offset))
    );

    $users = $wpdb->get_results($final_query, ARRAY_A); //phpcs:ignore

    // other logic
}
Patch Diff

Patch Diff

The vulnerability resides in the function get_wp_users_by_learndash_with_limit_offset($courses, $number = 5, $offset = 0) inside the Import class. To trace its usage, search for the keyword get_wp_users_by_learndash_with_limit_offset in the plugin directory.

Search Import

Search Import

The function is called inside:

  • retrieve_contacts_associated_with_learndash()
  • perform_learndash_user_import()

These functions belong to the ContactImportAction class in /app/API/Actions/Admin/Contact/ContactImportAction.php.

Next, searching for retrieve_contacts_associated_with_learndash reveals it is used inside another method:

Search 1

Search 1

This function is invoked by map_contacts_with_learndash() in the ContactImportController class located in /app/API/Controllers/Admin/Contact/ContactImportController.php.

Further tracing map_contacts_with_learndash shows it is registered as a REST API callback:

Search 2

Search 2

class ContactImportRoute extends AdminRoute {
    public function register_routes() {
        register_rest_route(
            $this->namespace, // mrm/v1
            $this->rest_base . '/learndash/map', // contacts/import/learndash/map/
            array(
                array(
                    'methods'             => WP_REST_Server::CREATABLE, // POST
                    'callback'            => array( $this->controller, 'map_contacts_with_learndash' ),
                    'permission_callback' => PermissionManager::current_user_can('mint_manage_contacts'), // Admin
                    'args'                => array(
                        'selectedCourses' => array(
                            'description' 		=> __( 'The selected courses from which to import contacts.', 'mrm' ),
                            'required'    		=> true,
                            'type'              => 'array',
                            'sanitize_callback' => 'rest_sanitize_array',
                            )
                        ), // array + required
                ),
            )
        );
        // other logic
    }
    // other logic
}

Call flow:

  1. A POST request is made to /wp-json/mrm/v1/contacts/import/learndash/map with the required parameter selectedCourses.

  2. The callback map_contacts_with_learndash(WP_REST_Request $request) receives the request.

    • The $request object is converted to an array $params.
  3. The callback then calls retrieve_contacts_associated_with_learndash($params).

  4. That function finally calls get_wp_users_by_learndash_with_limit_offset($courses, $number = 5, $offset = 0) where:

    • $courses = $params['selectedCourses']
    • $keys is an array built from all value fields in $courses, concatenated like course_{COURSE_ID}_access_from.
    • Before being inserted into the SQL query, the $keys array is merged into a string using implode().
  5. The query is executed and returns a JSON response with formatted_users and total_users.

After analyzing the code, it’s clear that the selectedCourses parameter undergoes several transformations before being injected into the vulnerable SQL query — but the attacker still controls the value key.

POST request using BurpSuite:

A single key can be used to easily control the final SQL query.

Get all user info

Get all user info

The resulting query looks like this:

SELECT user_id 
FROM wp_usermeta 
WHERE meta_key IN ('course_abc') OR 1=1 -- _access_from') GROUP BY user_id LIMIT 5 OFFSET 0
  • ') escapes the IN clause
  • 1=1 is always true → returns all user IDs from wp_usermeta

The rest of the function retrieves metadata for all returned users:

$formatted_users = array_map(
    function ($user) {
        $user->usermeta = array_map(
            function ($user_data) {
                return reset($user_data);
            },
            get_user_meta($user->ID)
        );
        return $user;
    },
    $contacts
);

The CVE-2025-58604 vulnerability in the WordPress Mail Mint plugin (prior to version 1.18.6) originates from not using $wpdb->prepare() when executing SQL queries. Instead, user input was directly concatenated into the SQL statement — leading to a classic SQL Injection vulnerability.

The patch now uses $wpdb->prepare() to safely construct SQL queries and mitigate this issue.

Key takeaways:

  • Always use $wpdb->prepare() when interacting with the WordPress database to prevent SQL Injection.
  • Regularly update your plugins and perform security reviews to avoid exploitation.

SQL Injection Cheat Sheet - PortSwigger

WordPress Mail Mint Plugin <= 1.18.5 is Vulnerable to SQL Injection

Related Content