mirror of
				https://github.com/zulip/zulip.git
				synced 2025-10-30 19:43:47 +00:00 
			
		
		
		
	electron_bridge: Harden against hypothetical DOM clobbering attacks.
Signed-off-by: Anders Kaseorg <anders@zulip.com>
This commit is contained in:
		
				
					committed by
					
						 Tim Abbott
						Tim Abbott
					
				
			
			
				
	
			
			
			
						parent
						
							6701d0c068
						
					
				
				
					commit
					2440c6d244
				
			| @@ -102,7 +102,7 @@ EXEMPT_FILES = make_set( | ||||
|         "web/src/drafts_overlay_ui.js", | ||||
|         "web/src/dropdown_widget.ts", | ||||
|         "web/src/echo.ts", | ||||
|         "web/src/electron_bridge.d.ts", | ||||
|         "web/src/electron_bridge.ts", | ||||
|         "web/src/email_pill.ts", | ||||
|         "web/src/emoji_picker.ts", | ||||
|         "web/src/emojisets.ts", | ||||
|   | ||||
| @@ -3,6 +3,7 @@ import assert from "minimalistic-assert"; | ||||
| import {z} from "zod"; | ||||
|  | ||||
| import * as channel from "./channel"; | ||||
| import {electron_bridge} from "./electron_bridge"; | ||||
| import {page_params} from "./page_params"; | ||||
| import * as presence from "./presence"; | ||||
| import * as watchdog from "./watchdog"; | ||||
| @@ -84,8 +85,8 @@ export function compute_active_status(): ActivityState { | ||||
|     // | ||||
|     // The check for `get_idle_on_system === undefined` is feature | ||||
|     // detection; older desktop app releases never set that property. | ||||
|     if (window.electron_bridge?.get_idle_on_system !== undefined) { | ||||
|         if (window.electron_bridge.get_idle_on_system()) { | ||||
|     if (electron_bridge?.get_idle_on_system !== undefined) { | ||||
|         if (electron_bridge.get_idle_on_system()) { | ||||
|             return ActivityState.IDLE; | ||||
|         } | ||||
|         return ActivityState.ACTIVE; | ||||
|   | ||||
| @@ -2,6 +2,7 @@ import $ from "jquery"; | ||||
|  | ||||
| import * as browser_history from "./browser_history"; | ||||
| import * as channel from "./channel"; | ||||
| import {electron_bridge} from "./electron_bridge"; | ||||
| import * as feedback_widget from "./feedback_widget"; | ||||
| import {$t} from "./i18n"; | ||||
| import * as message_store from "./message_store"; | ||||
| @@ -9,19 +10,19 @@ import * as message_view from "./message_view"; | ||||
| import * as stream_data from "./stream_data"; | ||||
|  | ||||
| export function initialize() { | ||||
|     if (window.electron_bridge === undefined) { | ||||
|     if (electron_bridge === undefined) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     window.electron_bridge.on_event("logout", () => { | ||||
|     electron_bridge.on_event("logout", () => { | ||||
|         $("#logout_form").trigger("submit"); | ||||
|     }); | ||||
|  | ||||
|     window.electron_bridge.on_event("show-keyboard-shortcuts", () => { | ||||
|     electron_bridge.on_event("show-keyboard-shortcuts", () => { | ||||
|         browser_history.go_to_location("keyboard-shortcuts"); | ||||
|     }); | ||||
|  | ||||
|     window.electron_bridge.on_event("show-notification-settings", () => { | ||||
|     electron_bridge.on_event("show-notification-settings", () => { | ||||
|         browser_history.go_to_location("settings/notifications"); | ||||
|     }); | ||||
|  | ||||
| @@ -29,10 +30,8 @@ export function initialize() { | ||||
|     // is often referred to as inline reply feature. This is done so desktop app doesn't | ||||
|     // have to depend on channel.post for setting crsf_token and message_view.narrow_by_topic | ||||
|     // to narrow to the message being sent. | ||||
|     if (window.electron_bridge.set_send_notification_reply_message_supported !== undefined) { | ||||
|         window.electron_bridge.set_send_notification_reply_message_supported(true); | ||||
|     } | ||||
|     window.electron_bridge.on_event("send_notification_reply_message", (message_id, reply) => { | ||||
|     electron_bridge.set_send_notification_reply_message_supported?.(true); | ||||
|     electron_bridge.on_event("send_notification_reply_message", (message_id, reply) => { | ||||
|         const message = message_store.get(message_id); | ||||
|         const data = { | ||||
|             type: message.type, | ||||
| @@ -56,7 +55,7 @@ export function initialize() { | ||||
|         } | ||||
|  | ||||
|         function error(error) { | ||||
|             window.electron_bridge.send_event("send_notification_reply_message_failed", { | ||||
|             electron_bridge.send_event("send_notification_reply_message_failed", { | ||||
|                 data, | ||||
|                 message_id, | ||||
|                 error, | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import $ from "jquery"; | ||||
| import assert from "minimalistic-assert"; | ||||
|  | ||||
| import {electron_bridge} from "./electron_bridge"; | ||||
| import type {Message} from "./message_store"; | ||||
|  | ||||
| type NoticeMemory = Map< | ||||
| @@ -33,8 +34,8 @@ export class ElectronBridgeNotification extends EventTarget { | ||||
|  | ||||
|     constructor(title: string, options: NotificationOptions) { | ||||
|         super(); | ||||
|         assert(window.electron_bridge?.new_notification !== undefined); | ||||
|         const notification_data = window.electron_bridge.new_notification( | ||||
|         assert(electron_bridge?.new_notification !== undefined); | ||||
|         const notification_data = electron_bridge.new_notification( | ||||
|             title, | ||||
|             options, | ||||
|             (type, eventInit) => this.dispatchEvent(new Event(type, eventInit)), | ||||
| @@ -63,7 +64,7 @@ export class ElectronBridgeNotification extends EventTarget { | ||||
|     } | ||||
| } | ||||
|  | ||||
| if (window.electron_bridge?.new_notification) { | ||||
| if (electron_bridge?.new_notification) { | ||||
|     NotificationAPI = ElectronBridgeNotification; | ||||
| } else if (window.Notification) { | ||||
|     NotificationAPI = window.Notification; | ||||
|   | ||||
| @@ -40,6 +40,10 @@ export type ElectronBridge = { | ||||
| declare global { | ||||
|     // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
 | ||||
|     interface Window { | ||||
|         electron_bridge?: ElectronBridge; | ||||
|         electron_bridge?: ElectronBridge | Element; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // Check for Element for extra defense against DOM clobbering attacks
 | ||||
| export const electron_bridge: ElectronBridge | undefined = | ||||
|     window.electron_bridge instanceof Element ? undefined : window.electron_bridge; | ||||
| @@ -1,6 +1,7 @@ | ||||
| import _ from "lodash"; | ||||
| import assert from "minimalistic-assert"; | ||||
|  | ||||
| import {electron_bridge} from "./electron_bridge"; | ||||
| import * as favicon from "./favicon"; | ||||
| import type {Filter} from "./filter"; | ||||
| import {$t} from "./i18n"; | ||||
| @@ -115,11 +116,9 @@ export function update_unread_counts(counts: FullUnreadCountsData): void { | ||||
|     favicon.update_favicon(unread_count, pm_count); | ||||
|  | ||||
|     // Notify the current desktop app's UI about the new unread count. | ||||
|     if (window.electron_bridge !== undefined) { | ||||
|         window.electron_bridge.send_event("total_unread_count", unread_count); | ||||
|     } | ||||
|     electron_bridge?.send_event("total_unread_count", unread_count); | ||||
|  | ||||
|     // TODO: Add a `window.electron_bridge.updateDirectMessageCount(new_pm_count);` call? | ||||
|     // TODO: Add a `electron_bridge.updateDirectMessageCount(new_pm_count);` call? | ||||
|     redraw_title(); | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,3 +1,5 @@ | ||||
| import {electron_bridge} from "../electron_bridge"; | ||||
|  | ||||
| document.querySelector<HTMLFormElement>("form#form")!.addEventListener("submit", () => { | ||||
|     document.querySelector<HTMLParagraphElement>("p#bad-token")!.hidden = false; | ||||
| }); | ||||
| @@ -42,8 +44,8 @@ void (async () => { | ||||
|     // key and a promise; as soon as something encrypted to that key is copied | ||||
|     // to the clipboard, the app decrypts it and resolves the promise to the | ||||
|     // plaintext.  This lets us skip the manual paste step. | ||||
|     const {key, pasted} = window.electron_bridge?.decrypt_clipboard | ||||
|         ? window.electron_bridge.decrypt_clipboard(1) | ||||
|     const {key, pasted} = electron_bridge?.decrypt_clipboard | ||||
|         ? electron_bridge.decrypt_clipboard(1) | ||||
|         : await decrypt_manual(); | ||||
|  | ||||
|     const keyHex = [...key].map((b) => b.toString(16).padStart(2, "0")).join(""); | ||||
|   | ||||
| @@ -16,6 +16,7 @@ import * as compose_closed_ui from "./compose_closed_ui"; | ||||
| import * as compose_pm_pill from "./compose_pm_pill"; | ||||
| import * as compose_recipient from "./compose_recipient"; | ||||
| import * as compose_state from "./compose_state"; | ||||
| import {electron_bridge} from "./electron_bridge"; | ||||
| import * as emoji from "./emoji"; | ||||
| import * as emoji_picker from "./emoji_picker"; | ||||
| import * as gear_menu from "./gear_menu"; | ||||
| @@ -261,8 +262,8 @@ export function dispatch_normal_event(event) { | ||||
|                         realm_settings[event.property](); | ||||
|                         settings_org.sync_realm_settings(event.property); | ||||
|  | ||||
|                         if (event.property === "name" && window.electron_bridge !== undefined) { | ||||
|                             window.electron_bridge.send_event("realm_name", event.value); | ||||
|                         if (event.property === "name") { | ||||
|                             electron_bridge?.send_event("realm_name", event.value); | ||||
|                         } | ||||
|  | ||||
|                         if (event.property === "invite_to_realm_policy") { | ||||
| @@ -326,15 +327,7 @@ export function dispatch_normal_event(event) { | ||||
|                             realm.realm_icon_url = event.data.icon_url; | ||||
|                             realm.realm_icon_source = event.data.icon_source; | ||||
|                             realm_icon.rerender(); | ||||
|                             { | ||||
|                                 const electron_bridge = window.electron_bridge; | ||||
|                                 if (electron_bridge !== undefined) { | ||||
|                                     electron_bridge.send_event( | ||||
|                                         "realm_icon_url", | ||||
|                                         event.data.icon_url, | ||||
|                                     ); | ||||
|                                 } | ||||
|                             } | ||||
|                             electron_bridge?.send_event("realm_icon_url", event.data.icon_url); | ||||
|                             break; | ||||
|                         case "logo": | ||||
|                             realm.realm_logo_url = event.data.logo_url; | ||||
|   | ||||
| @@ -26,6 +26,7 @@ const _document = { | ||||
| }; | ||||
|  | ||||
| const channel = mock_esm("../src/channel"); | ||||
| const electron_bridge = mock_esm("../src/electron_bridge"); | ||||
| const padded_widget = mock_esm("../src/padded_widget"); | ||||
| const pm_list = mock_esm("../src/pm_list"); | ||||
| const popovers = mock_esm("../src/popovers"); | ||||
| @@ -878,7 +879,7 @@ test("electron_bridge", ({override_rewire}) => { | ||||
|  | ||||
|     function with_bridge_idle(bridge_idle, f) { | ||||
|         with_overrides(({override}) => { | ||||
|             override(window, "electron_bridge", { | ||||
|             override(electron_bridge, "electron_bridge", { | ||||
|                 get_idle_on_system: () => bridge_idle, | ||||
|             }); | ||||
|             return f(); | ||||
| @@ -893,7 +894,7 @@ test("electron_bridge", ({override_rewire}) => { | ||||
|     }); | ||||
|  | ||||
|     with_overrides(({override}) => { | ||||
|         override(window, "electron_bridge", undefined); | ||||
|         override(electron_bridge, "electron_bridge", undefined); | ||||
|         activity.mark_client_idle(); | ||||
|         assert.equal(activity.compute_active_status(), "idle"); | ||||
|         activity.mark_client_active(); | ||||
|   | ||||
| @@ -30,6 +30,9 @@ const audible_notifications = mock_esm("../src/audible_notifications"); | ||||
| const bot_data = mock_esm("../src/bot_data"); | ||||
| const compose_banner = mock_esm("../src/compose_banner"); | ||||
| const compose_pm_pill = mock_esm("../src/compose_pm_pill"); | ||||
| const {electron_bridge} = mock_esm("../src/electron_bridge", { | ||||
|     electron_bridge: {}, | ||||
| }); | ||||
| const theme = mock_esm("../src/theme"); | ||||
| const emoji_picker = mock_esm("../src/emoji_picker"); | ||||
| const gear_menu = mock_esm("../src/gear_menu"); | ||||
| @@ -99,8 +102,6 @@ const overlays = mock_esm("../src/overlays"); | ||||
| mock_esm("../src/giphy"); | ||||
| const {Filter} = zrequire("filter"); | ||||
|  | ||||
| const electron_bridge = set_global("electron_bridge", {}); | ||||
|  | ||||
| message_lists.update_recipient_bar_background_color = noop; | ||||
| message_lists.current = { | ||||
|     get_row: noop, | ||||
|   | ||||
| @@ -7,6 +7,7 @@ const {run_test} = require("./lib/test"); | ||||
| const $ = require("./lib/zjquery"); | ||||
| const {current_user, page_params, user_settings} = require("./lib/zpage_params"); | ||||
|  | ||||
| mock_esm("../src/electron_bridge"); | ||||
| mock_esm("../src/spoilers", {hide_spoilers_in_notification() {}}); | ||||
|  | ||||
| const user_topics = zrequire("user_topics"); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user