From 7a090980d93debf42697cbe17e3a327be2d2b34b Mon Sep 17 00:00:00 2001 From: vuckro Date: Wed, 10 Jun 2026 14:27:15 +0200 Subject: [PATCH 1/2] fix(security): escape attacker-influenced data rendered in admin views (XSS) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Several network-admin views rendered untrusted data as HTML: - Event::interpolate_message() substituted payload values (customer display names, site titles/descriptions, etc.) into HTML message templates without escaping; the result is rendered with Vue v-html in the activity-stream widget — a stored XSS. Escape each interpolated value with esc_html() while preserving the template's own markup. - views/events/widget-payload.php rendered the JSON payload dump with v-html; switched to v-text (it is preformatted debug text). - Template library: short_description / price_html come from the remote marketplace API and are rendered with v-html; sanitize them server-side with wp_kses_post() so injected script is stripped while formatting is kept. - Webhook list table column_name interpolated the webhook name/event/url into HTML and data-* attributes unescaped; apply esc_html()/esc_attr()/esc_url(). Co-Authored-By: Claude Fable 5 --- inc/list-tables/class-webhook-list-table.php | 4 +-- inc/models/class-event.php | 31 +++++++++++++------- inc/template-library/class-api-client.php | 8 +++-- views/events/widget-payload.php | 2 +- 4 files changed, 29 insertions(+), 16 deletions(-) diff --git a/inc/list-tables/class-webhook-list-table.php b/inc/list-tables/class-webhook-list-table.php index 258821d43..7ad1923c8 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'), 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 @@
  • -
    
    +	
    
     
     	
  • From d3be4b182ab69319c3c32a0efc0c5d968c61b702 Mon Sep 17 00:00:00 2001 From: vuckro Date: Wed, 10 Jun 2026 22:01:39 +0200 Subject: [PATCH 2/2] fix(security): escape webhook url/event columns too (CodeRabbit follow-up) Apply the same esc_html() treatment to column_webhook_url() and column_event() that column_name() received, so all webhook list-table columns escape model data before output. Co-Authored-By: Claude Fable 5 --- inc/list-tables/class-webhook-list-table.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/inc/list-tables/class-webhook-list-table.php b/inc/list-tables/class-webhook-list-table.php index 7ad1923c8..b53fe440a 100644 --- a/inc/list-tables/class-webhook-list-table.php +++ b/inc/list-tables/class-webhook-list-table.php @@ -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}"; }