CVE-2022-23656: Fix cross-site scripting vulnerability in tooltips.

An attacker could maliciously craft a full name for their account and
send messages to a topic with several participants; a victim who then
opens an overflow tooltip including this full name on the recent
topics page could trigger execution of JavaScript code controlled by
the attacker.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
This commit is contained in:
Anders Kaseorg 2022-02-21 01:51:58 -08:00
parent 05a17e5854
commit e090027adc
10 changed files with 21 additions and 14 deletions

View File

@ -32,7 +32,6 @@ const default_popover_props = {
delay: 0,
appendTo: () => document.body,
trigger: "click",
allowHTML: true,
interactive: true,
hideOnClick: true,
/* The light-border TippyJS theme is a bit of a misnomer; it
@ -60,6 +59,7 @@ export function initialize() {
delegate("body", {
...default_popover_props,
target: "#streams_inline_icon",
allowHTML: true,
onShow(instance) {
const can_create_streams =
settings_data.user_can_create_private_streams() ||
@ -89,6 +89,7 @@ export function initialize() {
...default_popover_props,
target: ".compose_mobile_button",
placement: "top",
allowHTML: true,
onShow(instance) {
on_show_prep(instance);
instance.setContent(
@ -125,6 +126,7 @@ export function initialize() {
...default_popover_props,
target: ".compose_control_menu_wrapper",
placement: "top",
allowHTML: true,
onShow(instance) {
instance.setContent(
render_compose_control_buttons_popover({
@ -143,6 +145,7 @@ export function initialize() {
...default_popover_props,
target: ".enter_sends",
placement: "top",
allowHTML: true,
onShow(instance) {
on_show_prep(instance);
instance.setContent(

View File

@ -1,4 +1,5 @@
import $ from "jquery";
import _ from "lodash";
import render_recent_topic_row from "../templates/recent_topic_row.hbs";
import render_recent_topics_filters from "../templates/recent_topics_filters.hbs";
@ -291,7 +292,9 @@ function format_topic(topic_data) {
),
);
}
const other_sender_names = displayed_other_names.join("<br/>");
const other_sender_names_html = displayed_other_names
.map((name) => _.escape(name))
.join("<br />");
return {
// stream info
@ -309,7 +312,7 @@ function format_topic(topic_data) {
topic_url: hash_util.by_stream_topic_uri(stream_id, topic),
senders: senders_info,
other_senders_count: Math.max(0, all_senders.length - MAX_AVATAR),
other_sender_names,
other_sender_names_html,
muted,
topic_muted,
participated: topic_data.participated,

View File

@ -171,7 +171,9 @@ export const update_elements = (content) => {
text: rendered_time.text,
});
$(this).html(rendered_timestamp);
$(this).attr("data-tippy-content", rendered_time.tooltip_content);
$(this)
.attr("data-tippy-content", rendered_time.tooltip_content_html)
.attr("data-tippy-allowHTML", "true");
} else {
// This shouldn't happen. If it does, we're very interested in debugging it.
blueslip.error(`Could not parse datetime supplied by backend: ${time_str}`);

View File

@ -226,17 +226,17 @@ export function render_date(time: Date, time_above: Date | undefined, today: Dat
// Renders the timestamp returned by the <time:> Markdown syntax.
export function render_markdown_timestamp(time: number | Date): {
text: string;
tooltip_content: string;
tooltip_content_html: string;
} {
const hourformat = user_settings.twenty_four_hour_time ? "HH:mm" : "h:mm a";
const timestring = format(time, "E, MMM d yyyy, " + hourformat);
const tz_offset_str = get_tz_with_UTC_offset(time);
const tooltip_html_content = render_markdown_time_tooltip({tz_offset_str});
const tooltip_content_html = render_markdown_time_tooltip({tz_offset_str});
return {
text: timestring,
tooltip_content: tooltip_html_content,
tooltip_content_html: tooltip_content_html,
};
}

View File

@ -35,7 +35,7 @@ tippy.setDefaultProps({
appendTo: "parent",
// html content is not supported by default
// enable it by passing data-tippy-allowHtml="true"
// enable it by passing data-tippy-allowHTML="true"
// in the tag or a parameter.
});
@ -200,7 +200,6 @@ export function initialize() {
".rendered_markdown .copy_codeblock",
"#compose_top_right [data-tippy-content]",
],
allowHTML: true,
appendTo: () => document.body,
onHidden(instance) {
instance.destroy();

View File

@ -203,7 +203,7 @@ function initialize_compose_box() {
embedded: $("#compose").attr("data-embedded") === "",
file_upload_enabled: page_params.max_file_upload_size_mib > 0,
giphy_enabled: giphy.is_giphy_enabled(),
scroll_to_bottom_key: common.has_mac_keyboard()
scroll_to_bottom_key_html: common.has_mac_keyboard()
? "Fn + <span class='tooltip_right_arrow'>→</span>"
: "End",
}),

View File

@ -4,7 +4,7 @@
minimal css and no JS. We keep it `position: absolute` to prevent
it changing compose box layout in any way. --}}
<div id="scroll-to-bottom-button-container">
<div id="scroll-to-bottom-button-clickable-area" data-tippy-content="{{t 'Scroll to bottom' }} <span class='hotkey-hint'>({{scroll_to_bottom_key}})</span>" data-tippy-allowHtml="true">
<div id="scroll-to-bottom-button-clickable-area" data-tippy-content="{{t 'Scroll to bottom' }} &lt;span class='hotkey-hint'&gt;({{scroll_to_bottom_key_html}})&lt;/span&gt;" data-tippy-allowHTML="true">
<div id="scroll-to-bottom-button">
<i class="fa fa-chevron-down"></i>
</div>

View File

@ -7,7 +7,7 @@
<a role="button" class="undo_markdown_preview compose_control_button fa fa-edit" aria-label="{{t 'Write' }}" tabindex=0 style="display:none;" data-tippy-content="{{t 'Write' }}"></a>
<a role="button" class="compose_control_button fa fa-video-camera video_link" aria-label="{{t 'Add video call' }}" tabindex=0 data-tippy-content="{{t 'Add video call' }}"></a>
<a role="button" class="compose_control_button fa fa-smile-o emoji_map" aria-label="{{t 'Add emoji' }}" tabindex=0 data-tippy-content="{{t 'Add emoji' }}"></a>
<a role="button" class="compose_control_button fa fa-clock-o time_pick" aria-label="{{t 'Add global time' }}" tabindex=0 data-tippy-content="{{t 'Add global time<br />Everyone sees global times in their own time zone.' }}" data-tippy-maxWidth="none" data-tippy-allowHtml="true"></a>
<a role="button" class="compose_control_button fa fa-clock-o time_pick" aria-label="{{t 'Add global time' }}" tabindex=0 data-tippy-content="{{t 'Add global time<br />Everyone sees global times in their own time zone.' }}" data-tippy-allowHTML="true" data-tippy-maxWidth="none"></a>
<a role="button" class="compose_control_button compose_gif_icon {{#unless giphy_enabled }} hide {{/unless}} zulip-icon zulip-icon-gif" aria-label="{{t 'Add GIF' }}" tabindex=0 data-tippy-content="{{t 'Add GIF' }}"></a>
<div class="divider hide-sm">|</div>
<div class="{{#if message_id}}hide-lg{{else}}hide-sm{{/if}}">

View File

@ -32,7 +32,7 @@
<td class='recent_topic_users'>
<ul class="recent_topics_participants">
{{#if other_senders_count}}
<li class="recent_topics_participant_item tippy-zulip-tooltip" data-tippy-content="{{other_sender_names}}" data-tippy-allowHtml="true">
<li class="recent_topics_participant_item tippy-zulip-tooltip" data-tippy-content="{{other_sender_names_html}}" data-tippy-allowHTML="true">
<span class="recent_topics_participant_overflow">+{{other_senders_count}}</span>
</li>
{{/if}}

View File

@ -27,7 +27,7 @@
<a id="invite-user-link" href="#invite"><i class="fa fa-user-plus" aria-hidden="true"></i>{{t 'Invite more users' }}</a>
{{/if}}
<a id="sidebar-keyboard-shortcuts" data-overlay-trigger="keyboard-shortcuts" class="hidden-for-spectators">
<i class="fa fa-keyboard-o fa-2x tippy-zulip-tooltip" id="keyboard-icon" data-tippy-allowHtml="true" data-tippy-content="{{t 'Keyboard shortcuts' }} <span class='hotkey-hint'>(?)</span>"></i>
<i class="fa fa-keyboard-o fa-2x tippy-zulip-tooltip" id="keyboard-icon" data-tippy-allowHTML="true" data-tippy-content="{{t 'Keyboard shortcuts' }} &lt;span class='hotkey-hint'&gt;(?)&lt;/span&gt;"></i>
</a>
<div class="only-visible-for-spectators">
<div class="realm-description">