mirror of
https://github.com/zulip/zulip-desktop.git
synced 2025-10-24 00:23:36 +00:00
CVE-2020-10857: Whitelist safe URL protocols for shell.openExternal.
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
This commit is contained in:
@@ -1,10 +1,11 @@
|
|||||||
import { app, dialog, shell } from 'electron';
|
import { app, dialog } from 'electron';
|
||||||
import { autoUpdater } from 'electron-updater';
|
import { autoUpdater } from 'electron-updater';
|
||||||
import { linuxUpdateNotification } from './linuxupdater'; // Required only in case of linux
|
import { linuxUpdateNotification } from './linuxupdater'; // Required only in case of linux
|
||||||
|
|
||||||
import log from 'electron-log';
|
import log from 'electron-log';
|
||||||
import isDev from 'electron-is-dev';
|
import isDev from 'electron-is-dev';
|
||||||
import * as ConfigUtil from '../renderer/js/utils/config-util';
|
import * as ConfigUtil from '../renderer/js/utils/config-util';
|
||||||
|
import * as LinkUtil from '../renderer/js/utils/link-util';
|
||||||
|
|
||||||
export function appUpdater(updateFromMenu = false): void {
|
export function appUpdater(updateFromMenu = false): void {
|
||||||
// Don't initiate auto-updates in development
|
// Don't initiate auto-updates in development
|
||||||
@@ -72,7 +73,7 @@ export function appUpdater(updateFromMenu = false): void {
|
|||||||
Current Version: ${app.getVersion()}`
|
Current Version: ${app.getVersion()}`
|
||||||
});
|
});
|
||||||
if (response === 0) {
|
if (response === 0) {
|
||||||
shell.openExternal('https://zulipchat.com/apps/');
|
LinkUtil.openBrowser(new URL('https://zulipchat.com/apps/'));
|
||||||
}
|
}
|
||||||
// Remove all autoUpdator listeners so that next time autoUpdator is manually called these
|
// Remove all autoUpdator listeners so that next time autoUpdator is manually called these
|
||||||
// listeners don't trigger multiple times.
|
// listeners don't trigger multiple times.
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import path from 'path';
|
|||||||
import * as DNDUtil from '../renderer/js/utils/dnd-util';
|
import * as DNDUtil from '../renderer/js/utils/dnd-util';
|
||||||
import Logger from '../renderer/js/utils/logger-util';
|
import Logger from '../renderer/js/utils/logger-util';
|
||||||
import * as ConfigUtil from '../renderer/js/utils/config-util';
|
import * as ConfigUtil from '../renderer/js/utils/config-util';
|
||||||
|
import * as LinkUtil from '../renderer/js/utils/link-util';
|
||||||
import * as t from '../renderer/js/utils/translation-util';
|
import * as t from '../renderer/js/utils/translation-util';
|
||||||
|
|
||||||
const appName = app.name;
|
const appName = app.name;
|
||||||
@@ -48,7 +49,7 @@ function getToolsSubmenu(): Electron.MenuItemConstructorOptions[] {
|
|||||||
{
|
{
|
||||||
label: t.__('Release Notes'),
|
label: t.__('Release Notes'),
|
||||||
click() {
|
click() {
|
||||||
shell.openExternal(`https://github.com/zulip/zulip-desktop/releases/tag/v${app.getVersion()}`);
|
LinkUtil.openBrowser(new URL(`https://github.com/zulip/zulip-desktop/releases/tag/v${app.getVersion()}`));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -14,33 +14,19 @@
|
|||||||
<div class="maintenance-info">
|
<div class="maintenance-info">
|
||||||
<p class="detail maintainer">
|
<p class="detail maintainer">
|
||||||
Maintained by
|
Maintained by
|
||||||
<a onclick="linkInBrowser('website')">Zulip</a>
|
<a href="https://zulipchat.com" target="_blank" rel="noopener noreferrer">Zulip</a>
|
||||||
</p>
|
</p>
|
||||||
<p class="detail license">
|
<p class="detail license">
|
||||||
Available under the
|
Available under the
|
||||||
<a onclick="linkInBrowser('license')">Apache 2.0 License</a>
|
<a href="https://github.com/zulip/zulip-desktop/blob/master/LICENSE" target="_blank" rel="noopener noreferrer">Apache 2.0 License</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
const { app } = require('electron').remote;
|
const { app } = require('electron').remote;
|
||||||
const { shell } = require('electron');
|
|
||||||
const version_tag = document.querySelector('#version');
|
const version_tag = document.querySelector('#version');
|
||||||
version_tag.innerHTML = 'v' + app.getVersion();
|
version_tag.innerHTML = 'v' + app.getVersion();
|
||||||
|
|
||||||
function linkInBrowser(type) {
|
|
||||||
let url;
|
|
||||||
switch (type) {
|
|
||||||
case 'website':
|
|
||||||
url = "https://zulipchat.com";
|
|
||||||
break;
|
|
||||||
case 'license':
|
|
||||||
url = "https://github.com/zulip/zulip-desktop/blob/master/LICENSE";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
shell.openExternal(url);
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
<script>require('./js/shared/preventdrag.js')</script>
|
<script>require('./js/shared/preventdrag.js')</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -51,6 +51,6 @@ export default function handleExternalLink(this: WebView, event: Electron.NewWin
|
|||||||
ipcRenderer.removeAllListeners('downloadFileCompleted');
|
ipcRenderer.removeAllListeners('downloadFileCompleted');
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
shell.openExternal(url.href);
|
LinkUtil.openBrowser(url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ipcRenderer, remote, clipboard, shell } from 'electron';
|
import { ipcRenderer, remote, clipboard } from 'electron';
|
||||||
import { feedbackHolder } from './feedback';
|
import { feedbackHolder } from './feedback';
|
||||||
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
@@ -20,6 +20,7 @@ import Logger from './utils/logger-util';
|
|||||||
import * as CommonUtil from './utils/common-util';
|
import * as CommonUtil from './utils/common-util';
|
||||||
import * as EnterpriseUtil from './utils/enterprise-util';
|
import * as EnterpriseUtil from './utils/enterprise-util';
|
||||||
import * as AuthUtil from './utils/auth-util';
|
import * as AuthUtil from './utils/auth-util';
|
||||||
|
import * as LinkUtil from './utils/link-util';
|
||||||
import * as Messages from '../../resources/messages';
|
import * as Messages from '../../resources/messages';
|
||||||
|
|
||||||
interface FunctionalTabProps {
|
interface FunctionalTabProps {
|
||||||
@@ -866,8 +867,7 @@ class ServerManagerView {
|
|||||||
|
|
||||||
ipcRenderer.on('open-help', () => {
|
ipcRenderer.on('open-help', () => {
|
||||||
// Open help page of current active server
|
// Open help page of current active server
|
||||||
const helpPage = this.getCurrentActiveServer() + '/help';
|
LinkUtil.openBrowser(new URL('/help', this.getCurrentActiveServer()));
|
||||||
shell.openExternal(helpPage);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcRenderer.on('reload-viewer', this.reloadView.bind(this, this.tabs[this.activeTabIndex].props.index));
|
ipcRenderer.on('reload-viewer', this.reloadView.bind(this, this.tabs[this.activeTabIndex].props.index));
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
'use-strict';
|
'use-strict';
|
||||||
|
|
||||||
import { shell } from 'electron';
|
|
||||||
|
|
||||||
import BaseComponent from '../../components/base';
|
import BaseComponent from '../../components/base';
|
||||||
|
import * as LinkUtil from '../../utils/link-util';
|
||||||
import * as t from '../../utils/translation-util';
|
import * as t from '../../utils/translation-util';
|
||||||
|
|
||||||
export default class FindAccounts extends BaseComponent {
|
export default class FindAccounts extends BaseComponent {
|
||||||
@@ -45,7 +44,7 @@ export default class FindAccounts extends BaseComponent {
|
|||||||
if (!url.startsWith('http')) {
|
if (!url.startsWith('http')) {
|
||||||
url = 'https://' + url;
|
url = 'https://' + url;
|
||||||
}
|
}
|
||||||
shell.openExternal(url + '/accounts/find');
|
LinkUtil.openBrowser(new URL('/accounts/find', url));
|
||||||
}
|
}
|
||||||
|
|
||||||
initListeners(): void {
|
initListeners(): void {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { shell, ipcRenderer } from 'electron';
|
import { ipcRenderer } from 'electron';
|
||||||
|
|
||||||
import BaseComponent from '../../components/base';
|
import BaseComponent from '../../components/base';
|
||||||
import * as DomainUtil from '../../utils/domain-util';
|
import * as DomainUtil from '../../utils/domain-util';
|
||||||
|
import * as LinkUtil from '../../utils/link-util';
|
||||||
import * as t from '../../utils/translation-util';
|
import * as t from '../../utils/translation-util';
|
||||||
|
|
||||||
export default class NewServerForm extends BaseComponent {
|
export default class NewServerForm extends BaseComponent {
|
||||||
@@ -74,7 +75,7 @@ export default class NewServerForm extends BaseComponent {
|
|||||||
const link = 'https://zulipchat.com/new/';
|
const link = 'https://zulipchat.com/new/';
|
||||||
const externalCreateNewOrgElement = document.querySelector('#open-create-org-link');
|
const externalCreateNewOrgElement = document.querySelector('#open-create-org-link');
|
||||||
externalCreateNewOrgElement.addEventListener('click', () => {
|
externalCreateNewOrgElement.addEventListener('click', () => {
|
||||||
shell.openExternal(link);
|
LinkUtil.openBrowser(new URL(link));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { shell } from 'electron';
|
|
||||||
|
|
||||||
import BaseSection from './base-section';
|
import BaseSection from './base-section';
|
||||||
|
import * as LinkUtil from '../../utils/link-util';
|
||||||
import * as t from '../../utils/translation-util';
|
import * as t from '../../utils/translation-util';
|
||||||
|
|
||||||
export default class ShortcutsSection extends BaseSection {
|
export default class ShortcutsSection extends BaseSection {
|
||||||
@@ -331,7 +330,7 @@ export default class ShortcutsSection extends BaseSection {
|
|||||||
const link = 'https://zulipchat.com/help/keyboard-shortcuts';
|
const link = 'https://zulipchat.com/help/keyboard-shortcuts';
|
||||||
const externalCreateNewOrgElement = document.querySelector('#open-hotkeys-link');
|
const externalCreateNewOrgElement = document.querySelector('#open-hotkeys-link');
|
||||||
externalCreateNewOrgElement.addEventListener('click', () => {
|
externalCreateNewOrgElement.addEventListener('click', () => {
|
||||||
shell.openExternal(link);
|
LinkUtil.openBrowser(new URL(link));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
import { remote } from 'electron';
|
|
||||||
|
|
||||||
import cryptoRandomString from 'crypto-random-string';
|
import cryptoRandomString from 'crypto-random-string';
|
||||||
import * as ConfigUtil from './config-util';
|
import * as ConfigUtil from './config-util';
|
||||||
|
import * as LinkUtil from './link-util';
|
||||||
const { shell } = remote;
|
|
||||||
|
|
||||||
export const openInBrowser = (link: string): void => {
|
export const openInBrowser = (link: string): void => {
|
||||||
const otp = cryptoRandomString({length: 64});
|
const otp = cryptoRandomString({length: 64});
|
||||||
ConfigUtil.setConfigItem('desktopOtp', otp);
|
ConfigUtil.setConfigItem('desktopOtp', otp);
|
||||||
shell.openExternal(`${link}?desktop_flow_otp=${otp}`);
|
LinkUtil.openBrowser(new URL(`?desktop_flow_otp=${encodeURIComponent(otp)}`, link));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const xorStrings = (a: string, b: string): string => {
|
export const xorStrings = (a: string, b: string): string => {
|
||||||
|
|||||||
@@ -1,3 +1,45 @@
|
|||||||
|
import { shell } from 'electron';
|
||||||
|
import escape from 'escape-html';
|
||||||
|
import fs from 'fs';
|
||||||
|
import os from 'os';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
export function isUploadsUrl(server: string, url: URL): boolean {
|
export function isUploadsUrl(server: string, url: URL): boolean {
|
||||||
return url.origin === server && url.pathname.startsWith('/user_uploads/');
|
return url.origin === server && url.pathname.startsWith('/user_uploads/');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function openBrowser(url: URL): void {
|
||||||
|
if (['http:', 'https:', 'mailto:'].includes(url.protocol)) {
|
||||||
|
shell.openExternal(url.href);
|
||||||
|
} else {
|
||||||
|
// For security, indirect links to non-whitelisted protocols
|
||||||
|
// through a real web browser via a local HTML file.
|
||||||
|
const dir = fs.mkdtempSync(
|
||||||
|
path.join(os.tmpdir(), 'zulip-redirect-')
|
||||||
|
);
|
||||||
|
const file = path.join(dir, 'redirect.html');
|
||||||
|
fs.writeFileSync(file, `\
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta http-equiv="Refresh" content="0; url=${escape(url.href)}" />
|
||||||
|
<title>Redirecting</title>
|
||||||
|
<style>
|
||||||
|
html {
|
||||||
|
font-family: menu, "Helvetica Neue", sans-serif;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p>Opening <a href="${escape(url.href)}">${escape(url.href)}</a>…</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`);
|
||||||
|
shell.openItem(file);
|
||||||
|
setTimeout(() => {
|
||||||
|
fs.unlinkSync(file);
|
||||||
|
fs.rmdirSync(dir);
|
||||||
|
}, 15000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user