--- a/mail-mint/app/API/Actions/Admin/Contact/ContactProfileAction.php
+++ b/mail-mint/app/API/Actions/Admin/Contact/ContactProfileAction.php
@@ -83,24 +83,50 @@
* @since 1.7.0
*/
public function create_or_update_note( $params) {
- $contact_id = isset( $params['contact_id'] ) ? $params['contact_id'] : '';
- $note_id = isset( $params['note_id'] ) ? $params['note_id'] : '';
+ // Verify nonce for CSRF protection
+ $nonce = isset( $_SERVER['HTTP_X_WP_NONCE'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_WP_NONCE'] ) ) : '';
+ if ( ! wp_verify_nonce( $nonce, 'wp_rest' ) ) {
+ return array(
+ 'status' => 'failed',
+ 'message' => __( 'Invalid security token.', 'mrm' ),
+ );
+ }
+
+ $contact_id = isset( $params['contact_id'] ) ? absint( $params['contact_id'] ) : 0;
+ $note_id = isset( $params['note_id'] ) ? absint( $params['note_id'] ) : 0;
$note = isset( $params['note'] ) ? $params['note'] : array();
- // Note description validation.
- $description = isset( $note['description'] ) ? sanitize_text_field( $note['description'] ) : '';
- if ( empty( $description ) ) {
+ // Sanitize all note fields to prevent XSS and injection attacks
+ $sanitized_note = array(
+ 'description' => isset( $note['description'] ) ? sanitize_textarea_field( $note['description'] ) : '',
+ 'title' => isset( $note['title'] ) ? sanitize_text_field( $note['title'] ) : '',
+ 'type' => isset( $note['type'] ) ? sanitize_text_field( $note['type'] ) : '',
+ 'created_by' => isset( $note['created_by'] ) ? absint( $note['created_by'] ) : get_current_user_id(),
+ 'status' => isset( $note['status'] ) ? absint( $note['status'] ) : 1,
+ 'is_public' => isset( $note['is_public'] ) ? absint( $note['is_public'] ) : 1,
+ );
+
+ // Validate contact ID
+ if ( empty( $contact_id ) ) {
+ return array(
+ 'status' => 'failed',
+ 'message' => __( 'Invalid contact ID.', 'mrm' ),
+ );
+ }
+
+ // Note description validation
+ if ( empty( $sanitized_note['description'] ) ) {
return array(
'status' => 'failed',
'message' => __( 'Note description is required.', 'mrm' ),
);
}
- // Note object create and insert or update to database.
+ // Note object create and insert or update to database with sanitized data
if ( $note_id ) {
- $success = NoteModel::update( $note, $contact_id, $note_id );
+ $success = NoteModel::update( $sanitized_note, $contact_id, $note_id );
} else {
- $success = NoteModel::insert( $note, $contact_id );
+ $success = NoteModel::insert( $sanitized_note, $contact_id );
}
if ( $success ) {
@@ -159,6 +185,16 @@
* @since 1.7.0
*/
public function delete_contact_profile_note( $params ) {
+ // Verify nonce for CSRF protection
+ $nonce = isset( $_SERVER['HTTP_X_WP_NONCE'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_WP_NONCE'] ) ) : '';
+ if ( ! wp_verify_nonce( $nonce, 'wp_rest' ) ) {
+ return array(
+ 'status' => 'failed',
+ 'message' => __( 'Invalid security token.', 'mrm' ),
+ 'code' => 'rest_forbidden'
+ );
+ }
+
$note_id = isset( $params['note_id'] ) ? $params['note_id'] : '';
$success = NoteModel::destroy( $params['note_id'] );
--- a/mail-mint/app/API/Actions/Admin/Email/TemplateAction.php
+++ b/mail-mint/app/API/Actions/Admin/Email/TemplateAction.php
@@ -48,20 +48,24 @@
'title' => 'title',
);
- // Get 'order-by' and 'order-type' parameters or use default values.
+ // Validate 'order-by' parameter against whitelist.
$order_by = isset( $params['order-by'] ) && isset( $order_by_map[ $params['order-by'] ] ) ? $order_by_map[ $params['order-by'] ] : 'ID';
- $order_type = isset( $params['order-type'] ) ? strtoupper( $params['order-type'] ) : 'DESC';
+
+ // Validate 'order-type' parameter against whitelist (ASC or DESC only).
+ $allowed_order_types = array( 'ASC', 'DESC' );
+ $order_type_param = isset( $params['order-type'] ) ? strtoupper( sanitize_text_field( $params['order-type'] ) ) : 'DESC';
+ $order_type = in_array( $order_type_param, $allowed_order_types, true ) ? $order_type_param : 'DESC';
// Get 'search' parameter or use default value.
$search = isset( $params['search'] ) ? $params['search'] : '';
- // Define the query.
+ // Define the query with proper ORDER BY clause construction.
$query = "
SELECT id, title, thumbnail, thumbnail_data, json_content, editor_type, email_type, customizable, author_id, status, newsletter_type, newsletter_id, created_at, updated_at
FROM $table_name
WHERE (email_type = %s OR email_type IS NULL OR email_type = '')
AND title LIKE %s
- ORDER BY $order_by $order_type
+ ORDER BY {$order_by} {$order_type}
LIMIT %d OFFSET %d
";
--- a/mail-mint/app/Database/models/CampaignModel.php
+++ b/mail-mint/app/Database/models/CampaignModel.php
@@ -386,6 +386,15 @@
public static function get_all( $wpdb, $offset = 0, $limit = 10, $search = '', $order_by = 'id', $order_type = 'desc', $filter = '', $filter_type = '', $status = '' ) {
$campaign_table = $wpdb->prefix . CampaignSchema::$campaign_table;
+ // Validate order_by against whitelist
+ $allowed_order_by = array( 'id', 'title', 'created_at', 'status', 'type' );
+ $order_by = in_array( $order_by, $allowed_order_by, true ) ? $order_by : 'id';
+
+ // Validate order_type against whitelist (ASC or DESC only)
+ $allowed_order_types = array( 'asc', 'desc', 'ASC', 'DESC' );
+ $order_type_param = strtolower( $order_type );
+ $order_type = in_array( $order_type_param, array( 'asc', 'desc' ), true ) ? strtoupper( $order_type_param ) : 'DESC';
+
// Prepare search terms for query.
$search_terms = array();
if ( ! empty( $search ) ) {
@@ -409,8 +418,8 @@
$where = 'WHERE ' . implode( ' AND ', array_merge( $search_terms, $filter_terms, $status_terms ) );
}
- // Prepare sql results for list view.
- $results = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $campaign_table $where ORDER BY $order_by $order_type LIMIT %d, %d", $offset, $limit ), ARRAY_A ); // db call ok. ; no-cache ok.
+ // Prepare sql results for list view with validated ORDER BY clause.
+ $results = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $campaign_table $where ORDER BY {$order_by} {$order_type} LIMIT %d, %d", $offset, $limit ), ARRAY_A ); // db call ok. ; no-cache ok.
$count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) as total FROM $campaign_table $where" ) ); // db call ok. ; no-cache ok.
$total_pages = ceil( $count / $limit );
--- a/mail-mint/app/Database/models/CustomFieldModel.php
+++ b/mail-mint/app/Database/models/CustomFieldModel.php
@@ -104,14 +104,23 @@
global $wpdb;
$fields_table = $wpdb->prefix . CustomFieldSchema::$table_name;
+ // Validate order_by against whitelist
+ $allowed_order_by = array( 'id', 'title', 'slug', 'type' );
+ $order_by = in_array( $order_by, $allowed_order_by, true ) ? $order_by : 'id';
+
+ // Validate order_type against whitelist (ASC or DESC only)
+ $allowed_order_types = array( 'asc', 'desc', 'ASC', 'DESC' );
+ $order_type_param = strtoupper( $order_type );
+ $order_type = in_array( $order_type_param, $allowed_order_types, true ) ? $order_type_param : 'DESC';
+
$search_terms = null;
if ( ! empty( $search ) ) {
$search = $wpdb->esc_like( $search );
$search_terms = "WHERE `title` LIKE '%%$search%%'";
}
- // Return field froups for list view.
+ // Return field froups for list view with validated ORDER BY clause.
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
- $select_query = $wpdb->prepare( "SELECT * FROM $fields_table {$search_terms} ORDER BY %s %s LIMIT %d, %d", $order_by, $order_type, $offset, $limit );
+ $select_query = $wpdb->prepare( "SELECT * FROM $fields_table {$search_terms} ORDER BY {$order_by} {$order_type} LIMIT %d, %d", $offset, $limit );
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
$results = $wpdb->get_results( $select_query, ARRAY_A ); // db call ok. ; no-cache ok.
--- a/mail-mint/app/Database/models/FormModel.php
+++ b/mail-mint/app/Database/models/FormModel.php
@@ -207,6 +207,14 @@
$form_table = $wpdb->prefix . FormSchema::$table_name;
$meta_table = $wpdb->prefix . FormMetaSchema::$table_name;
+ // Validate order_by against whitelist
+ $allowed_order_by = array( 'id', 'title', 'created_at', 'status' );
+ $order_by = in_array( $order_by, $allowed_order_by, true ) ? $order_by : 'id';
+
+ // Validate order_type against whitelist (ASC or DESC only)
+ $allowed_order_types = array( 'asc', 'desc', 'ASC', 'DESC' );
+ $order_type = in_array( $order_type, $allowed_order_types, true ) ? strtoupper( $order_type ) : 'DESC';
+
// Prepare search terms for query.
$search_terms = array();
if ( ! empty( $search ) ) {
@@ -224,9 +232,9 @@
$where = 'WHERE ' . implode( ' AND ', array_merge( $search_terms, $status_terms ) );
}
- // Prepare sql results for list view.
+ // Prepare sql results for list view with validated ORDER BY clause.
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
- $results = $wpdb->get_results( $wpdb->prepare( "SELECT f.id, f.title, f.group_ids, f.status, f.created_at, IFNULL(m.meta_value, 0) AS entries FROM $form_table AS f LEFT JOIN $meta_table AS m ON f.id = m.form_id AND m.meta_key = 'entries' {$where} ORDER BY $order_by $order_type LIMIT %d, %d", array( $offset, $limit ) ), ARRAY_A ); // db call ok. ; no-cache ok.
+ $results = $wpdb->get_results( $wpdb->prepare( "SELECT f.id, f.title, f.group_ids, f.status, f.created_at, IFNULL(m.meta_value, 0) AS entries FROM $form_table AS f LEFT JOIN $meta_table AS m ON f.id = m.form_id AND m.meta_key = 'entries' {$where} ORDER BY {$order_by} {$order_type} LIMIT %d, %d", array( $offset, $limit ) ), ARRAY_A ); // db call ok. ; no-cache ok.
$count_query = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) as total FROM $form_table $where" ) ); // db call ok. ; no-cache ok.
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$count = (int) $count_query;
--- a/mail-mint/app/Internal/Automation/Core/DataStore/AutomationStore.php
+++ b/mail-mint/app/Internal/Automation/Core/DataStore/AutomationStore.php
@@ -441,6 +441,15 @@
$search_terms = null;
$condition = 'WHERE';
+ // Validate order_by against whitelist
+ $allowed_order_by = array( 'id', 'name', 'created_at', 'status' );
+ $order_by = in_array( $order_by, $allowed_order_by, true ) ? $order_by : 'created_at';
+
+ // Validate order_type against whitelist (ASC or DESC only)
+ $allowed_order_types = array( 'asc', 'desc', 'ASC', 'DESC' );
+ $order_type_param = strtolower( $order_type );
+ $order_type = in_array( $order_type_param, array( 'asc', 'desc' ), true ) ? strtoupper( $order_type_param ) : 'DESC';
+
// Search automation by name.
if ( ! empty( $search ) ) {
$search = $wpdb->esc_like( $search );
@@ -453,10 +462,10 @@
// Return automations in list view.
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
if ( 'all' === $status ) {
- $select_query = $wpdb->get_results( $wpdb->prepare( "SELECT automation.id,automation.name,automation.status,automation.created_at FROM $automation_table as automation LEFT JOIN $automation_meta_table AS meta ON automation.id = meta.automation_id {$search_terms} {$condition} meta.meta_key = %s AND meta.meta_value = %s ORDER BY automation.$order_by $order_type LIMIT %d, %d", array( 'source', 'mint', $offset, $limit ) ), ARRAY_A ); // db call ok. ; no-cache ok.
+ $select_query = $wpdb->get_results( $wpdb->prepare( "SELECT automation.id,automation.name,automation.status,automation.created_at FROM $automation_table as automation LEFT JOIN $automation_meta_table AS meta ON automation.id = meta.automation_id {$search_terms} {$condition} meta.meta_key = %s AND meta.meta_value = %s ORDER BY automation.{$order_by} {$order_type} LIMIT %d, %d", array( 'source', 'mint', $offset, $limit ) ), ARRAY_A ); // db call ok. ; no-cache ok.
$count_query = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $automation_table as automation LEFT JOIN $automation_meta_table AS meta ON automation.id = meta.automation_id {$search_terms} {$condition} meta.meta_key = %s AND meta.meta_value = %s", array( 'source', 'mint' ) ) ); // db call ok. ; no-cache ok.
} else {
- $select_query = $wpdb->get_results( $wpdb->prepare( "SELECT automation.id,automation.name,automation.status,automation.created_at FROM $automation_table as automation LEFT JOIN $automation_meta_table AS meta ON automation.id = meta.automation_id {$search_terms} {$condition} meta.meta_key = %s AND meta.meta_value = %s AND automation.status = %s ORDER BY automation.$order_by $order_type LIMIT %d, %d", array( 'source', 'mint', $status, $offset, $limit ) ), ARRAY_A ); // db call ok. ; no-cache ok.
+ $select_query = $wpdb->get_results( $wpdb->prepare( "SELECT automation.id,automation.name,automation.status,automation.created_at FROM $automation_table as automation LEFT JOIN $automation_meta_table AS meta ON automation.id = meta.automation_id {$search_terms} {$condition} meta.meta_key = %s AND meta.meta_value = %s AND automation.status = %s ORDER BY automation.{$order_by} {$order_type} LIMIT %d, %d", array( 'source', 'mint', $status, $offset, $limit ) ), ARRAY_A ); // db call ok. ; no-cache ok.
$count_query = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $automation_table as automation LEFT JOIN $automation_meta_table AS meta ON automation.id = meta.automation_id {$search_terms} {$condition} meta.meta_key = %s AND meta.meta_value = %s AND automation.status = %s", array( 'source', 'mint', $status ) ) ); // db call ok. ; no-cache ok.
}
--- a/mail-mint/app/Utilities/Helper/Import.php
+++ b/mail-mint/app/Utilities/Helper/Import.php
@@ -858,24 +858,43 @@
// Extract course IDs from the provided courses.
$course_ids = array_column($courses, 'value');
- // If no course IDs are provided, get all LearnDash courses.
+ // If no course IDs are provided, get all Tutor LMS courses.
if (!$course_ids) {
$all_courses = HelperFunctions::get_tutor_lms_courses();
$course_ids = array_column($all_courses, 'value');
}
+ // Sanitize course IDs to ensure they are integers
+ $course_ids = array_map('intval', $course_ids);
+
+ if (empty($course_ids)) {
+ return array(
+ 'formatted_users' => array(),
+ 'total_users' => 0,
+ );
+ }
+
global $wpdb;
$table_name = $wpdb->prefix . 'posts';
- $enrollments_query = $wpdb->prepare("SELECT post_author FROM $table_name WHERE post_type = 'tutor_enrolled' AND post_parent IN ('" . implode("', '", $course_ids) . "')"); //phpcs:ignore
+ // Create placeholders for IN clause
+ $placeholders = implode(', ', array_fill(0, count($course_ids), '%d'));
- $total_query = $wpdb->prepare("SELECT COUNT( DISTINCT post_author) FROM $table_name WHERE post_type = 'tutor_enrolled' AND post_parent IN ('" . implode("', '", $course_ids) . "')"); //phpcs:ignore
+ // Prepare safe query with placeholders
+ $enrollments_query = $wpdb->prepare(
+ "SELECT DISTINCT post_author FROM $table_name WHERE post_type = 'tutor_enrolled' AND post_parent IN ($placeholders) LIMIT %d OFFSET %d",
+ array_merge($course_ids, array($number, $offset))
+ );
+
+ // Prepare safe total query with placeholders
+ $total_query = $wpdb->prepare(
+ "SELECT COUNT(DISTINCT post_author) FROM $table_name WHERE post_type = 'tutor_enrolled' AND post_parent IN ($placeholders)",
+ $course_ids
+ );
$total = $wpdb->get_var($total_query); //phpcs:ignore
- $enrollments_query .= $wpdb->prepare(' LIMIT %d OFFSET %d', $number, $offset);
-
$enrollments = $wpdb->get_results($enrollments_query); //phpcs:ignore
if (empty($enrollments)) {
--- a/mail-mint/assets/admin/dist/automation_editor/index.min.asset.php
+++ b/mail-mint/assets/admin/dist/automation_editor/index.min.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('lodash', 'react', 'react-dom', 'wp-a11y', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-data', 'wp-deprecated', 'wp-element', 'wp-i18n', 'wp-keyboard-shortcuts', 'wp-keycodes', 'wp-plugins', 'wp-preferences', 'wp-primitives', 'wp-viewport'), 'version' => '35dc0feedbec3b86b01c');
+<?php return array('dependencies' => array('lodash', 'react', 'react-dom', 'wp-a11y', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-data', 'wp-deprecated', 'wp-element', 'wp-i18n', 'wp-keyboard-shortcuts', 'wp-keycodes', 'wp-plugins', 'wp-preferences', 'wp-primitives', 'wp-viewport'), 'version' => '1c0ca88861cfde18b48c');
--- a/mail-mint/assets/admin/dist/main/index.min.asset.php
+++ b/mail-mint/assets/admin/dist/main/index.min.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('lodash', 'react', 'react-dom', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-data', 'wp-deprecated', 'wp-editor', 'wp-element', 'wp-format-library', 'wp-i18n', 'wp-keyboard-shortcuts', 'wp-media-utils', 'wp-preferences'), 'version' => '75e90cbcc3eb80deacb5');
+<?php return array('dependencies' => array('lodash', 'react', 'react-dom', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-data', 'wp-deprecated', 'wp-editor', 'wp-element', 'wp-format-library', 'wp-i18n', 'wp-keyboard-shortcuts', 'wp-media-utils', 'wp-preferences'), 'version' => '9c0bfa66c81b9b3a2a25');
--- a/mail-mint/mail-mint.php
+++ b/mail-mint/mail-mint.php
@@ -15,7 +15,7 @@
* Plugin Name: Email Marketing Automation - Mail Mint
* Plugin URI: https://getwpfunnels.com/email-marketing-automation-mail-mint/
* Description: Effortless 📧 email marketing automation tool to collect & manage leads, run email campaigns, and initiate basic email automation.
- * Version: 1.19.2
+ * Version: 1.19.3
* Author: WPFunnels Team
* Author URI: https://getwpfunnels.com/
* License: GPL-2.0+
@@ -36,7 +36,7 @@
* Start at version 1.0.0 and use SemVer - https://semver.org
* Rename this for your plugin and update it as you release new versions.
*/
-define( 'MRM_VERSION', '1.19.2' );
+define( 'MRM_VERSION', '1.19.3' );
define( 'MAILMINT', 'mailmint' );
define( 'MRM_DB_VERSION', '1.15.3' );
define( 'MINT_DEV_MODE', false );
--- a/mail-mint/vendor/composer/installed.php
+++ b/mail-mint/vendor/composer/installed.php
@@ -3,7 +3,7 @@
'name' => 'coderex/code-rex-crm',
'pretty_version' => 'dev-master',
'version' => 'dev-master',
- 'reference' => '0c2fc08a254ef39fb59489486b82c66c27969e81',
+ 'reference' => '491a28de842a88ab08d8c876ec36dadedf6dd660',
'type' => 'wordpress-plugin',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@@ -22,7 +22,7 @@
'coderex/code-rex-crm' => array(
'pretty_version' => 'dev-master',
'version' => 'dev-master',
- 'reference' => '0c2fc08a254ef39fb59489486b82c66c27969e81',
+ 'reference' => '491a28de842a88ab08d8c876ec36dadedf6dd660',
'type' => 'wordpress-plugin',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),