diff --git a/inc/list-tables/class-webhook-list-table.php b/inc/list-tables/class-webhook-list-table.php index 258821d43..b53fe440a 100644 --- a/inc/list-tables/class-webhook-list-table.php +++ b/inc/list-tables/class-webhook-list-table.php @@ -65,14 +65,14 @@ public function column_name($item): string { '%s ', wu_network_admin_url('wp-ultimo-edit-webhook', $url_atts), - $item->get_name(), + esc_html($item->get_name()), $item->get_id(), __('Sending Test..', 'ultimate-multisite') ); $actions = [ 'edit' => sprintf('%s', wu_network_admin_url('wp-ultimo-edit-webhook', $url_atts), __('Edit', 'ultimate-multisite')), - 'test' => sprintf('%s', $item->get_webhook_url(), __('Send Test', 'ultimate-multisite')), + 'test' => sprintf('%s', esc_url($item->get_webhook_url()), __('Send Test', 'ultimate-multisite')), 'delete' => sprintf( '%s', __('Delete', 'ultimate-multisite'), @@ -97,7 +97,7 @@ public function column_name($item): string { */ public function column_webhook_url($item) { - $trimmed_url = mb_strimwidth((string) $item->get_webhook_url(), 0, 50, '...'); + $trimmed_url = esc_html(mb_strimwidth((string) $item->get_webhook_url(), 0, 50, '...')); return "{$trimmed_url}"; } @@ -112,7 +112,7 @@ public function column_webhook_url($item) { */ public function column_event($item) { - $event = $item->get_event(); + $event = esc_html((string) $item->get_event()); return "{$event}"; } diff --git a/inc/models/class-event.php b/inc/models/class-event.php index 01b938416..41b0eba97 100644 --- a/inc/models/class-event.php +++ b/inc/models/class-event.php @@ -271,23 +271,32 @@ public function interpolate_message($message, $payload): string { $payload = json_decode(json_encode($payload), true); // phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode - $interpolation_keys = []; - - foreach ($payload as $key => &$value) { - $interpolation_keys[] = "{{{$key}}}"; - + /* + * The message templates (get_default_system_messages) intentionally + * contain HTML (e.g. {{model}}) and the rendered result + * is output with Vue v-html in the activity-stream widget. The + * interpolated payload values, however, include attacker-influenced model + * fields (customer display names, site titles, etc.), so every value must + * be HTML-escaped before substitution to prevent stored XSS — while the + * template's own markup is preserved. + */ + $interpolation = []; + + foreach ($payload as $key => $value) { if (is_array($value)) { - $value = implode(' → ', wu_array_flatten($value)); + $value = implode(' → ', array_map('esc_html', wu_array_flatten($value))); + } else { + $value = esc_html((string) $value); } - } - $interpolation = array_combine($interpolation_keys, $payload); + $interpolation[ "{{{$key}}}" ] = $value; + } - $interpolation['{{payload}}'] = implode(' - ', wu_array_flatten($payload, true)); + $interpolation['{{payload}}'] = implode(' - ', array_map('esc_html', wu_array_flatten($payload, true))); - $interpolation['{{model}}'] = wu_slug_to_name($this->object_type); + $interpolation['{{model}}'] = esc_html(wu_slug_to_name($this->object_type)); - $interpolation['{{object_id}}'] = $this->object_id; + $interpolation['{{object_id}}'] = esc_html((string) $this->object_id); return strtr($message, $interpolation); } diff --git a/inc/template-library/class-api-client.php b/inc/template-library/class-api-client.php index 68755e6a1..712ca94d3 100644 --- a/inc/template-library/class-api-client.php +++ b/inc/template-library/class-api-client.php @@ -153,8 +153,12 @@ private function parse_template_data(array $template): array { 'slug' => $template['slug'] ?? '', 'name' => $template['name'] ?? '', 'description' => $template['description'] ?? '', - 'short_description' => $template['short_description'] ?? '', - 'price_html' => $template['price_html'] ?? '', + // short_description and price_html are rendered with Vue v-html in the + // Template Library grid; the data is fetched over HTTP from the remote + // marketplace, so sanitize the HTML to allowed post markup to neutralize + // any injected script while preserving legitimate formatting. + 'short_description' => wp_kses_post($template['short_description'] ?? ''), + 'price_html' => wp_kses_post($template['price_html'] ?? ''), 'permalink' => $template['permalink'] ?? '', 'is_free' => empty($template['prices']['price'] ?? 0), 'prices' => $template['prices'] ?? [], diff --git a/views/events/widget-payload.php b/views/events/widget-payload.php index bb0025fbd..6ce54f103 100644 --- a/views/events/widget-payload.php +++ b/views/events/widget-payload.php @@ -10,7 +10,7 @@
  • -
    
    +