Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/simply-schedule-appointments/includes/class-appointment-model.php
+++ b/simply-schedule-appointments/includes/class-appointment-model.php
@@ -246,11 +246,19 @@
return $data;
}
- foreach ( $data['customer_information'] as &$info ) {
- if ( is_string( $info ) ) {
- $info = trim( $info );
- // sanitize
- $info = sanitize_textarea_field($info);
+ foreach ( $data['customer_information'] as $key => $value ) {
+ if ( is_string( $value ) ) {
+ $data['customer_information'][ $key ] = sanitize_textarea_field( $value );
+ } elseif ( is_array( $value ) ) {
+ // Checkbox fields are the only legitimate source of nested input;
+ // accept exactly a one-level array of strings.
+ $sanitized = array();
+ foreach ( $value as $element ) {
+ if ( is_string( $element ) ) {
+ $sanitized[] = sanitize_text_field( $element );
+ }
+ }
+ $data['customer_information'][ $key ] = $sanitized;
}
}
--- a/simply-schedule-appointments/includes/class-appointment-object.php
+++ b/simply-schedule-appointments/includes/class-appointment-object.php
@@ -852,6 +852,8 @@
'date_created' => '',
'date_modified' => '',
'public_edit_url' => '',
+ 'price_full' => '',
+ 'payment_received' => '',
'payment_method' => '',
'web_meeting_url' => '',
'web_meeting_password' => '',
@@ -874,6 +876,17 @@
$payload['appointment']['appointment_type_title'] = $appointment_type_object->title;
}
+ // price_full is not a column on modern installs (dropped in #652) but a
+ // residual DECIMAL(9,2) column still exists on pre-Mar-2023 DBs, where
+ // SELECT * leaks a stale "0.00". Treat 0/""/null all as "no price set"
+ // and backfill from the appointment type's configured price.
+ if ( empty( (float) $payload['appointment']['price_full'] ) && ! empty( $payload['appointment']['appointment_type_id'] ) ) {
+ $appointment_type_object = $this->get_appointment_type();
+ if ( ! empty( $appointment_type_object->payments['price'] ) ) {
+ $payload['appointment']['price_full'] = (float) $appointment_type_object->payments['price'];
+ }
+ }
+
$settings_global = ssa()->settings->get()['global'];
foreach ( $dates_to_localize as $key ) {
if ( empty( $payload['appointment'][$key] ) ) {
--- a/simply-schedule-appointments/includes/class-elementor.php
+++ b/simply-schedule-appointments/includes/class-elementor.php
@@ -20,7 +20,7 @@
*
* @var string The plugin version.
*/
- const VERSION = '1.6.12.2';
+ const VERSION = '1.6.12.4';
/**
* Minimum Elementor Version
@@ -29,7 +29,7 @@
*
* @var string Minimum Elementor version required to run the plugin.
*/
- const MINIMUM_ELEMENTOR_VERSION = '1.6.12.2';
+ const MINIMUM_ELEMENTOR_VERSION = '1.6.12.4';
/**
* Minimum PHP Version
@@ -38,7 +38,7 @@
*
* @var string Minimum PHP version required to run the plugin.
*/
- const MINIMUM_PHP_VERSION = '1.6.12.2';
+ const MINIMUM_PHP_VERSION = '1.6.12.4';
/**
* Instance
--- a/simply-schedule-appointments/includes/class-google-calendar-client.php
+++ b/simply-schedule-appointments/includes/class-google-calendar-client.php
@@ -201,9 +201,10 @@
ssa_debug_log( print_r( $response, true ), 10); // phpcs:ignore
throw new Exception( 'Failed to validate Google Calendar access token' );
}
-
+
return true;
}
+
/**
* use in place of ->calendarList->listCalendarList( $options = array() ) {}
* this method will return all calendars, not just the first page
@@ -489,7 +490,15 @@
// if less than 300 seconds remaining, refresh the token anyways
$created = 0;
- if ( isset( $token['created'] ) ) {
+ if ( isset( $token['ssa_fetched_at'] ) ) {
+ // Local stamp recorded when the token entered the plugin (see refresh and
+ // quick-connect paths). Preferred over `created` and the id_token's `iat`
+ // claim because it uses the same local clock as the `time()` comparison
+ // below — comparing a remote-clock-derived timestamp against the local
+ // clock can falsely declare a fresh token expired on a host whose system
+ // clock has drifted.
+ $created = $token['ssa_fetched_at'];
+ } elseif ( isset( $token['created'] ) ) {
$created = $token['created'];
} elseif ( isset( $token['id_token'] ) ) {
// check the ID token for "iat"
@@ -551,7 +560,7 @@
ssa_debug_log( print_r( $response, true ), 10 ); // phpcs:ignore
return false;
}
-
+
$data = json_decode(wp_remote_retrieve_body($response), true);
if( empty( $data['refresh_token'] ) ) {
@@ -581,6 +590,8 @@
ssa_debug_log( 'Failed to refresh access token for staff id ' . (string) $this->staff_id . print_r($response, true), 10); // phpcs:ignore
throw new Exception( 'Failed to refresh access token' );
}
+ // Local mint stamp for clock-drift-tolerant expiry — see is_access_token_expired().
+ $response['ssa_fetched_at'] = time();
return $response;
}
@@ -682,9 +693,16 @@
if ( is_wp_error($response) || wp_remote_retrieve_response_code($response) > 299 ) {
throw new Throwable( $response );
}
-
+
$data = json_decode(wp_remote_retrieve_body($response), true);
-
+
+ if ( empty( $data ) || ! is_array( $data ) || empty( $data['access_token'] ) ) {
+ ssa_debug_log( 'Failed to exchange auth code for staff id ' . (string) $this->staff_id . print_r( $response, true ), 10 ); // phpcs:ignore
+ return false;
+ }
+
+ // Local mint stamp for clock-drift-tolerant expiry — see is_access_token_expired().
+ $data['ssa_fetched_at'] = time();
$this->access_token = $data;
return true;
--- a/simply-schedule-appointments/includes/class-notifications.php
+++ b/simply-schedule-appointments/includes/class-notifications.php
@@ -1110,6 +1110,12 @@
$subject = '';
} else {
$subject = wp_strip_all_tags( $this->get_rendered_template_string_for_appointment( $appointment_object, $notification['subject'], $notification_vars ), true );
+ // Email subjects are plain-text MIME headers and don't decode HTML
+ // entities. The kses-final render pipeline leaves customer_information
+ // values in their entity-encoded form (e.g. "Smith & Jones"), so
+ // decode here for legibility. Safe after wp_strip_all_tags because no
+ // HTML markup remains to be reanimated.
+ $subject = html_entity_decode( $subject );
}
$message = $this->get_rendered_template_string_for_appointment( $appointment_object, $notification['message'], $notification_vars );
@@ -1167,6 +1173,13 @@
continue;
}
+ // SMS is plain-text. Strip any HTML the kses pipeline allowed
+ // through and decode entities so customer_information values
+ // like "Smith & Jones" don't arrive as "Smith & Jones".
+ // Safe in this order because wp_strip_all_tags removes any
+ // markup before html_entity_decode runs.
+ $sms_message = html_entity_decode( wp_strip_all_tags( $message ) );
+
$response = array();
foreach ($recipients['sms_to'] as $key => $to_number) {
$sms_args = apply_filters( 'ssa/notifications/sms/args', array(
@@ -1175,7 +1188,7 @@
'notification_vars' => $notification_vars,
'appointment_object' => $appointment_object,
'subject' => $subject,
- 'message' => $message,
+ 'message' => $sms_message,
) );
if ( empty( $sms_args['to_number'] ) ) {
@@ -1275,7 +1288,10 @@
array( ' ' ),
$template_string
);
- $template_string = htmlspecialchars_decode( $template_string );
+ // wp_kses_post (inside render_template_string) must remain the last
+ // sanitizing transformation — a decode after it can resurrect encoded
+ // payloads. make_clickable composes safely with kses output: its URL
+ // regex matches "&" in full and esc_url re-encodes it as "&".
$template_string = make_clickable( $template_string );
return $template_string;
@@ -1287,7 +1303,7 @@
'example_appointment_type_id' => $appointment_type_object->id,
) );
}
-
+
$template_string = $this->plugin->templates->cleanup_variables_in_string( $template_string );
$template_string = $this->prepare_notification_template( $template_string );
$template_string = $this->plugin->templates->render_template_string( $template_string, $notification_vars );
@@ -1296,7 +1312,10 @@
array( ' ' ),
$template_string
);
- $template_string = htmlspecialchars_decode( $template_string );
+ // wp_kses_post (inside render_template_string) must remain the last
+ // sanitizing transformation — a decode after it can resurrect encoded
+ // payloads. make_clickable composes safely with kses output: its URL
+ // regex matches "&" in full and esc_url re-encodes it as "&".
$template_string = make_clickable( $template_string );
return $template_string;
--- a/simply-schedule-appointments/includes/class-paypal-ipn-listener.php
+++ b/simply-schedule-appointments/includes/class-paypal-ipn-listener.php
@@ -23,7 +23,7 @@
* @package PHP-PayPal-IPN
* @author Micah Carrick
* @copyright (c) 2011 - Micah Carrick
- * @version 1.6.12.2
+ * @version 1.6.12.4
* @license http://opensource.org/licenses/gpl-3.0.html
*/
--- a/simply-schedule-appointments/simply-schedule-appointments.php
+++ b/simply-schedule-appointments/simply-schedule-appointments.php
@@ -3,7 +3,7 @@
* Plugin Name: Simply Schedule Appointments
* Plugin URI: https://simplyscheduleappointments.com
* Description: Easy appointment scheduling
- * Version: 1.6.12.2
+ * Version: 1.6.12.4
* Requires PHP: 7.4
* Author: NSquared
* Author URI: https://nsquared.io/
@@ -15,7 +15,7 @@
* @link https://simplyscheduleappointments.com
*
* @package Simply_Schedule_Appointments
- * @version 1.6.12.2
+ * @version 1.6.12.4
*
* Built using generator-plugin-wp (https://github.com/WebDevStudios/generator-plugin-wp)
*/
@@ -207,7 +207,7 @@
* @var string
* @since 0.0.0
*/
- const VERSION = '1.6.12.2';
+ const VERSION = '1.6.12.4';
/**
* URL of plugin directory.
--- a/simply-schedule-appointments/vendor/composer/installed.php
+++ b/simply-schedule-appointments/vendor/composer/installed.php
@@ -3,7 +3,7 @@
'name' => '__root__',
'pretty_version' => 'dev-master',
'version' => 'dev-master',
- 'reference' => '9dde10134016987a3069f0b466799d1a81afee1e',
+ 'reference' => '248abb0fcbf9bebef5f7cba7358ae587fc9beeee',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@@ -13,7 +13,7 @@
'__root__' => array(
'pretty_version' => 'dev-master',
'version' => 'dev-master',
- 'reference' => '9dde10134016987a3069f0b466799d1a81afee1e',
+ 'reference' => '248abb0fcbf9bebef5f7cba7358ae587fc9beeee',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),