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