Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/email-subscribers/email-subscribers.php
+++ b/email-subscribers/email-subscribers.php
@@ -3,7 +3,7 @@
* Plugin Name: Icegram Express - Email Subscribers, Newsletters and Marketing Automation Plugin
* Plugin URI: https://www.icegram.com/
* Description: Add subscription forms on website, send HTML newsletters & automatically notify subscribers about new blog posts once it is published.
- * Version: 5.9.27
+ * Version: 5.9.28
* Author: Icegram
* Author URI: https://www.icegram.com/
* Requires at least: 3.9
@@ -187,7 +187,7 @@
/* ***************************** Initial Compatibility Work (End) ******************* */
if ( ! defined( 'ES_PLUGIN_VERSION' ) ) {
- define( 'ES_PLUGIN_VERSION', '5.9.27' );
+ define( 'ES_PLUGIN_VERSION', '5.9.28' );
}
// Plugin Folder Path.
--- a/email-subscribers/lite/admin/class-email-subscribers-admin.php
+++ b/email-subscribers/lite/admin/class-email-subscribers-admin.php
@@ -419,7 +419,20 @@
}
}
}
+
+ $unsubscribe_feedbacks = array();
+ if ( class_exists( 'IG_ES_Unsubscribe_Feedback' ) ) {
+ $unsubscribe_feedback_instance = new IG_ES_Unsubscribe_Feedback();
+ $unsubscribe_feedbacks = $unsubscribe_feedback_instance->get_default_unsubscribe_feedbacks();
+ }
+
+ $unsubscribe_feedbacks = array();
+ if ( class_exists( 'IG_ES_Unsubscribe_Feedback' ) ) {
+ $unsubscribe_feedback_instance = new IG_ES_Unsubscribe_Feedback();
+ $unsubscribe_feedbacks = $unsubscribe_feedback_instance->get_default_unsubscribe_feedbacks();
+ }
+
wp_localize_script( 'es-shadcn-dashboard', 'icegramExpressAdminData', array(
'apiUrl' => admin_url( 'admin-ajax.php' ),
'baseUrl' => ES_PLUGIN_URL . 'lite/admin/shadcn-frontend/dist/',
@@ -429,6 +442,14 @@
'security' => wp_create_nonce( 'ig-es-admin-ajax-nonce' ),
'plan' => ES()->get_plan(),
'defaultRoute' => $default_route,
+ 'campaignStatus' => array(
+ 'DRAFT' => IG_ES_CAMPAIGN_STATUS_IN_ACTIVE,
+ 'ACTIVE' => IG_ES_CAMPAIGN_STATUS_ACTIVE,
+ 'SCHEDULED' => IG_ES_CAMPAIGN_STATUS_SCHEDULED,
+ 'QUEUED' => IG_ES_CAMPAIGN_STATUS_QUEUED,
+ 'PAUSED' => IG_ES_CAMPAIGN_STATUS_PAUSED,
+ 'FINISHED' => IG_ES_CAMPAIGN_STATUS_FINISHED,
+ ),
'currentUser' => array(
'displayName' => $current_user->display_name,
'firstName' => $current_user->first_name,
@@ -450,6 +471,7 @@
'ig_es_track_utm' => get_option( 'ig_es_track_utm', 'no' ),
),
'pricingBanner' => Email_Subscribers_Pricing::get_pricing_banner_config(),
+ 'unsubscribeFeedbacks' => $unsubscribe_feedbacks,
) );
wp_register_style( 'es-shadcn-dashboard', plugin_dir_url( __FILE__ ) . 'shadcn-frontend/dist/index.css', array(), $this->version );
wp_enqueue_script( 'es-shadcn-dashboard' );
--- a/email-subscribers/lite/includes/classes/class-es-handle-subscription.php
+++ b/email-subscribers/lite/includes/classes/class-es-handle-subscription.php
@@ -260,6 +260,14 @@
$name = ! empty( $form_data['esfpx_name'] ) ? str_replace( '.', ' ', sanitize_text_field( $form_data['esfpx_name'] ) ) : '';
$ip_address = ! empty( $form_data['esfpx_ip_address'] ) ? sanitize_text_field( $form_data['esfpx_ip_address'] ) : '';
+ // Security: Strip shortcode delimiters from subscriber name before storage.
+ // Prevents shortcode injection via subscriber-controlled name fields.
+ // See Wordfence advisory: Unauthenticated Arbitrary Shortcode Execution via Subscriber Name Field.
+ if ( ! empty( $name ) ) {
+ $name = wp_strip_all_tags( $name );
+ $name = str_replace( array( '[', ']' ), '', $name );
+ }
+
$first_name = '';
$last_name = '';
if ( ! empty( $name ) ) {
--- a/email-subscribers/lite/includes/classes/class-es-mailer.php
+++ b/email-subscribers/lite/includes/classes/class-es-mailer.php
@@ -1205,6 +1205,13 @@
$campaign_id = ig_es_get_data( $merge_tags, 'campaign_id', 0 );
$message_id = ig_es_get_data( $merge_tags, 'message_id', 0 );
$list_ids = ig_es_get_data( $merge_tags, 'list_ids', '' );
+
+ // Security: strip shortcodes from subscriber-controlled name fields
+ // before merging them into email content to prevent Arbitrary
+ // Shortcode Execution (Wordfence advisory, affects <= 5.9.27).
+ $name = strip_shortcodes( (string) $name );
+ $first_name = strip_shortcodes( (string) $first_name );
+ $last_name = strip_shortcodes( (string) $last_name );
$link_data = array(
'message_id' => $message_id,
--- a/email-subscribers/lite/includes/controllers/class-es-campaigns-controller.php
+++ b/email-subscribers/lite/includes/controllers/class-es-campaigns-controller.php
@@ -371,6 +371,15 @@
// Use pre-fetched mailing queue data
$report = isset( $campaign_queue_data[ $campaign_id ] ) ? $campaign_queue_data[ $campaign_id ] : array();
+
+ // Add sent date from mailing queue for sent campaigns
+ if ( IG_ES_CAMPAIGN_STATUS_FINISHED === $campaign_status && ! empty( $report ) ) {
+ if ( ! empty( $report['finish_at'] ) && '0000-00-00 00:00:00' !== $report['finish_at'] ) {
+ $campaign['sent_at'] = $report['finish_at'];
+ } elseif ( ! empty( $report['start_at'] ) && '0000-00-00 00:00:00' !== $report['start_at'] ) {
+ $campaign['sent_at'] = $report['start_at'];
+ }
+ }
if ( self::is_post_campaign( $campaign_type ) ) {
if ( $report && ! empty( $report['meta'] ) ) {
--- a/email-subscribers/lite/includes/db/class-ig-es-db-unsubscribe-feedback.php
+++ b/email-subscribers/lite/includes/db/class-ig-es-db-unsubscribe-feedback.php
@@ -169,7 +169,97 @@
),
ARRAY_A
);
-// phpcs:enable
+ // phpcs:enable
return $feedback_counts;
}
+
+
+ /**
+ * Get feedback count for audience unsubscribed contacts only
+ *
+ * @param int $number_of_days Number of days to look back
+ * @return array Feedback counts filtered by unsubscribed contacts
+ *
+ * @since 5.7.55
+ */
+ public static function get_audience_unsubscribe_feedback_counts( $number_of_days ) {
+ global $wpdb;
+
+ $feedback_table = $wpdb->prefix . 'ig_unsubscribe_feedback';
+ $lists_contacts_table = $wpdb->prefix . 'ig_lists_contacts';
+
+ // phpcs:disable
+ $feedback_counts = $wpdb->get_results(
+ $wpdb->prepare(
+ "SELECT uf.feedback_slug, COUNT(DISTINCT uf.contact_id) AS feedback_count
+ FROM `{$feedback_table}` uf
+ WHERE uf.contact_id IN (
+ SELECT DISTINCT contact_id
+ FROM `{$lists_contacts_table}`
+ WHERE status = 'unsubscribed'
+ AND unsubscribed_at >= DATE_SUB(NOW(), INTERVAL %d DAY)
+ )
+ GROUP BY uf.feedback_slug",
+ $number_of_days
+ ),
+ ARRAY_A
+ );
+ // phpcs:enable
+
+ return $feedback_counts;
+ }
+
+
+ /**
+ * Get feedback count for campaign level unsubscribed contacts only
+ *
+ * @param int $number_of_days Number of days to look back
+ * @return array Feedback counts filtered by campaign
+ *
+ * @since 5.7.55
+ */
+ public static function get_campaign_level_unsubscribe_feedback_counts( $number_of_days, $mailing_queue_id = 0, $campaign_id = 0 ) {
+
+ global $wpdb;
+
+ $actions_table = $wpdb->prefix . 'ig_actions';
+ $feedback_table = $wpdb->prefix . 'ig_unsubscribe_feedback';
+
+ $where_conditions = array();
+
+ $where_conditions[] = $wpdb->prepare( "a.created_at >= UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL %d DAY))", $number_of_days );
+
+ $where_conditions[] = $wpdb->prepare( "a.type = %d", IG_CONTACT_UNSUBSCRIBE );
+
+ $where_clause = implode( ' AND ', $where_conditions );
+
+ // phpcs:disable
+ $query = "SELECT
+ CASE
+ WHEN uf.feedback_slug IS NULL OR uf.feedback_slug = '' THEN 'no_feedback'
+ ELSE uf.feedback_slug
+ END AS feedback_slug,
+ COUNT(DISTINCT a.contact_id) AS feedback_count
+ FROM `{$actions_table}` a
+ LEFT JOIN `{$feedback_table}` uf
+ ON a.contact_id = uf.contact_id
+ AND a.list_id = uf.list_id";
+
+ if ( ! empty( $campaign_id ) ) {
+ $query .= $wpdb->prepare( " AND uf.campaign_id = %d", $campaign_id );
+ }
+
+ if ( ! empty( $mailing_queue_id ) ) {
+ $query .= $wpdb->prepare( " AND uf.mailing_queue_id = %d", $mailing_queue_id );
+ }
+
+ $query .= " WHERE {$where_clause}
+ GROUP BY feedback_slug";
+
+
+ $feedback_counts = $wpdb->get_results( $query, ARRAY_A );
+ // phpcs:enable
+
+ return $feedback_counts;
+ }
}
--- a/email-subscribers/lite/includes/feedback.php
+++ b/email-subscribers/lite/includes/feedback.php
@@ -959,18 +959,18 @@
$total_contacts = ES()->contacts_db->count_active_contacts_by_list_id();
if ( $total_contacts >= 5 ) {
- $survey_title = __( 'WhatsApp for your store - should we build it?', 'email-subscribers' );
+ $survey_title = __( 'Unlock more sales with WhatsApp messaging', 'email-subscribers' );
$survey_slug = 'ig-es-whatsapp-integration-survey';
$desc = '<div class="wa-survey-container">';
- $desc .= '<p class="wa-survey-intro">' . __( 'Send order updates & shipping alerts on WhatsApp.', 'email-subscribers' ) . '</p>';
- $desc .= '<p class="wa-survey-intro">' . __( 'Customers actually read them.', 'email-subscribers' ) . '</p>';
+ $desc .= '<p class="wa-survey-intro">' . __( 'Send order updates on WhatsApp', 'email-subscribers' ) . '</p>';
+ $desc .= '<p class="wa-survey-intro">' . __( ' where they won't get lost in an inbox.', 'email-subscribers' ) . '</p>';
$desc .= '<div class="wa-stats-container">';
$desc .= '<div class="wa-stat-item"><strong class="wa-stat-number">98%</strong><br><span class="wa-stat-label">' . __( 'open rate', 'email-subscribers' ) . '</span></div>';
$desc .= '<div class="wa-stat-item"><strong class="wa-stat-number">5 min</strong><br><span class="wa-stat-label">' . __( 'avg read', 'email-subscribers' ) . '</span></div>';
- $desc .= '<div class="wa-stat-item"><strong class="wa-stat-number">3× CTR</strong><br><span class="wa-stat-label">' . __( 'vs email', 'email-subscribers' ) . '</span></div>';
+ $desc .= '<div class="wa-stat-item"><strong class="wa-stat-number">3x CTR</strong><br><span class="wa-stat-label">' . __( 'vs email', 'email-subscribers' ) . '</span></div>';
$desc .= '</div>';
$desc .= '</div>';
--- a/email-subscribers/lite/includes/services/class-es-service-email-sending.php
+++ b/email-subscribers/lite/includes/services/class-es-service-email-sending.php
@@ -184,6 +184,11 @@
check_ajax_referer( 'ig-es-admin-ajax-nonce', 'security' );
+ $can_access_settings = ES_Common::ig_es_can_access( 'settings' );
+ if ( ! $can_access_settings ) {
+ return 0;
+ }
+
$request = ig_es_get_request_data( 'request' );
if ( ! empty( $request ) ) {
--- a/email-subscribers/lite/includes/workflows/actions/class-es-action-send-email.php
+++ b/email-subscribers/lite/includes/workflows/actions/class-es-action-send-email.php
@@ -182,10 +182,10 @@
// If we don't replace it here then for workflow configured to be sent to admins, {{EMAIL}}, {{NAME}} gets replaced with admin email and names which is not desired for subscriber based workflows.
if ( isset( $data['source'] ) && 'es' === $data['source'] ) {
$subject = str_replace( '{{EMAIL}}', isset( $data['email'] ) ? $data['email'] : '', $subject );
- $subject = str_replace( '{{NAME}}', isset( $data['name'] ) ? $data['name'] : '', $subject );
+ $subject = str_replace( '{{NAME}}', isset( $data['name'] ) ? strip_shortcodes( $data['name'] ) : '', $subject );
$email_content = str_replace( '{{EMAIL}}', isset( $data['email'] ) ? $data['email'] : '', $email_content );
- $email_content = str_replace( '{{NAME}}', isset( $data['name'] ) ? $data['name'] : '', $email_content );
+ $email_content = str_replace( '{{NAME}}', isset( $data['name'] ) ? strip_shortcodes( $data['name'] ) : '', $email_content );
$email_content = str_replace( '{{LIST}}', isset( $data['list_name'] ) ? $data['list_name'] : '', $email_content );
}
--- a/email-subscribers/lite/includes/workflows/admin/class-es-workflow-admin-ajax.php
+++ b/email-subscribers/lite/includes/workflows/admin/class-es-workflow-admin-ajax.php
@@ -50,6 +50,11 @@
check_ajax_referer( 'ig-es-admin-ajax-nonce', 'security' );
+ $can_access_workflows = ES_Common::ig_es_can_access( 'workflows' );
+ if ( ! $can_access_workflows ) {
+ return 0;
+ }
+
$trigger_name = ig_es_get_request_data( 'trigger_name' );
$workflow_id = ig_es_get_request_data( 'workflow_id' );
@@ -91,6 +96,11 @@
check_ajax_referer( 'ig-es-admin-ajax-nonce', 'security' );
+ $can_access_workflows = ES_Common::ig_es_can_access( 'workflows' );
+ if ( ! $can_access_workflows ) {
+ return 0;
+ }
+
$action_name = ig_es_get_request_data( 'action_name' );
$action_number = ig_es_get_request_data( 'action_number' );
$trigger_name = ig_es_get_request_data( 'trigger_name' );
@@ -129,6 +139,12 @@
public static function toggle_workflow_status() {
check_ajax_referer( 'ig-es-admin-ajax-nonce', 'security' );
+
+ $can_access_workflows = ES_Common::ig_es_can_access( 'workflows' );
+ if ( ! $can_access_workflows ) {
+ return 0;
+ }
+
$args = array(
'workflow_id' => ig_es_get_request_data( 'workflow_id' ),
'new_state' => ig_es_get_request_data( 'new_state' ),
@@ -151,6 +167,11 @@
check_ajax_referer( 'ig-es-admin-ajax-nonce', 'security' );
+ $can_access_workflows = ES_Common::ig_es_can_access( 'workflows' );
+ if ( ! $can_access_workflows ) {
+ return 0;
+ }
+
$variable = IG_ES_Variables::get_variable( ES_Clean::string( ig_es_get_request_data( 'variable' ) ) );
if ( $variable ) {
ES_Workflow_Admin::get_view(
@@ -172,6 +193,11 @@
check_ajax_referer( 'ig-es-admin-ajax-nonce', 'security' );
+ $can_access_workflows = ES_Common::ig_es_can_access( 'workflows' );
+ if ( ! $can_access_workflows ) {
+ return 0;
+ }
+
$item_name = ig_es_get_request_data( 'item_name' );
if ( ! $item_name ) {
--- a/email-subscribers/lite/includes/workflows/variables/subscriber/subscriber-first-name.php
+++ b/email-subscribers/lite/includes/workflows/variables/subscriber/subscriber-first-name.php
@@ -25,7 +25,9 @@
* @return string
*/
public function get_value( $subscriber, $parameters ) {
- return $subscriber['first_name'];
+ // Security: strip shortcodes from subscriber-controlled name to prevent
+ // arbitrary shortcode execution when this value is merged into email content.
+ return strip_shortcodes( (string) $subscriber['first_name'] );
}
}
--- a/email-subscribers/lite/includes/workflows/variables/subscriber/subscriber-last-name.php
+++ b/email-subscribers/lite/includes/workflows/variables/subscriber/subscriber-last-name.php
@@ -25,7 +25,9 @@
* @return string
*/
public function get_value( $subscriber, $parameters ) {
- return $subscriber['last_name'];
+ // Security: strip shortcodes from subscriber-controlled name to prevent
+ // arbitrary shortcode execution when this value is merged into email content.
+ return strip_shortcodes( (string) $subscriber['last_name'] );
}
}
--- a/email-subscribers/lite/includes/workflows/variables/subscriber/subscriber-name.php
+++ b/email-subscribers/lite/includes/workflows/variables/subscriber/subscriber-name.php
@@ -25,7 +25,9 @@
* @return string
*/
public function get_value( $subscriber, $parameters ) {
- return $subscriber['name'];
+ // Security: strip shortcodes from subscriber-controlled name to prevent
+ // arbitrary shortcode execution when this value is merged into email content.
+ return strip_shortcodes( (string) $subscriber['name'] );
}
}