CVE-2020-10857: Whitelist safe URL protocols for shell.openExternal.

Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
This commit is contained in:
Anders Kaseorg
2020-03-22 21:37:48 -07:00
parent af59bb7c99
commit a03f569af9
10 changed files with 62 additions and 36 deletions

View File

@@ -1,10 +1,11 @@
import { app, dialog, shell } from 'electron';
import { app, dialog } from 'electron';
import { autoUpdater } from 'electron-updater';
import { linuxUpdateNotification } from './linuxupdater'; // Required only in case of linux
import log from 'electron-log';
import isDev from 'electron-is-dev';
import * as ConfigUtil from '../renderer/js/utils/config-util';
import * as LinkUtil from '../renderer/js/utils/link-util';
export function appUpdater(updateFromMenu = false): void {
// Don't initiate auto-updates in development
@@ -72,7 +73,7 @@ export function appUpdater(updateFromMenu = false): void {
Current Version: ${app.getVersion()}`
});
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
// listeners don't trigger multiple times.

View File

@@ -7,6 +7,7 @@ import path from 'path';
import * as DNDUtil from '../renderer/js/utils/dnd-util';
import Logger from '../renderer/js/utils/logger-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';
const appName = app.name;
@@ -48,7 +49,7 @@ function getToolsSubmenu(): Electron.MenuItemConstructorOptions[] {
{
label: t.__('Release Notes'),
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()}`));
}
},
{

View File

@@ -14,33 +14,19 @@
<div class="maintenance-info">
<p class="detail maintainer">
Maintained by
<a onclick="linkInBrowser('website')">Zulip</a>
<a href="https://zulipchat.com" target="_blank" rel="noopener noreferrer">Zulip</a>
</p>
<p class="detail license">
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>
</div>
</div>
<script>
const { app } = require('electron').remote;
const { shell } = require('electron');
const version_tag = document.querySelector('#version');
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>require('./js/shared/preventdrag.js')</script>
</body>

View File

@@ -51,6 +51,6 @@ export default function handleExternalLink(this: WebView, event: Electron.NewWin
ipcRenderer.removeAllListeners('downloadFileCompleted');
});
} else {
shell.openExternal(url.href);
LinkUtil.openBrowser(url);
}
}

View File

@@ -1,4 +1,4 @@
import { ipcRenderer, remote, clipboard, shell } from 'electron';
import { ipcRenderer, remote, clipboard } from 'electron';
import { feedbackHolder } from './feedback';
import path from 'path';
@@ -20,6 +20,7 @@ import Logger from './utils/logger-util';
import * as CommonUtil from './utils/common-util';
import * as EnterpriseUtil from './utils/enterprise-util';
import * as AuthUtil from './utils/auth-util';
import * as LinkUtil from './utils/link-util';
import * as Messages from '../../resources/messages';
interface FunctionalTabProps {
@@ -866,8 +867,7 @@ class ServerManagerView {
ipcRenderer.on('open-help', () => {
// Open help page of current active server
const helpPage = this.getCurrentActiveServer() + '/help';
shell.openExternal(helpPage);
LinkUtil.openBrowser(new URL('/help', this.getCurrentActiveServer()));
});
ipcRenderer.on('reload-viewer', this.reloadView.bind(this, this.tabs[this.activeTabIndex].props.index));

View File

@@ -1,8 +1,7 @@
'use-strict';
import { shell } from 'electron';
import BaseComponent from '../../components/base';
import * as LinkUtil from '../../utils/link-util';
import * as t from '../../utils/translation-util';
export default class FindAccounts extends BaseComponent {
@@ -45,7 +44,7 @@ export default class FindAccounts extends BaseComponent {
if (!url.startsWith('http')) {
url = 'https://' + url;
}
shell.openExternal(url + '/accounts/find');
LinkUtil.openBrowser(new URL('/accounts/find', url));
}
initListeners(): void {

View File

@@ -1,7 +1,8 @@
import { shell, ipcRenderer } from 'electron';
import { ipcRenderer } from 'electron';
import BaseComponent from '../../components/base';
import * as DomainUtil from '../../utils/domain-util';
import * as LinkUtil from '../../utils/link-util';
import * as t from '../../utils/translation-util';
export default class NewServerForm extends BaseComponent {
@@ -74,7 +75,7 @@ export default class NewServerForm extends BaseComponent {
const link = 'https://zulipchat.com/new/';
const externalCreateNewOrgElement = document.querySelector('#open-create-org-link');
externalCreateNewOrgElement.addEventListener('click', () => {
shell.openExternal(link);
LinkUtil.openBrowser(new URL(link));
});
}

View File

@@ -1,6 +1,5 @@
import { shell } from 'electron';
import BaseSection from './base-section';
import * as LinkUtil from '../../utils/link-util';
import * as t from '../../utils/translation-util';
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 externalCreateNewOrgElement = document.querySelector('#open-hotkeys-link');
externalCreateNewOrgElement.addEventListener('click', () => {
shell.openExternal(link);
LinkUtil.openBrowser(new URL(link));
});
}

View File

@@ -1,14 +1,11 @@
import { remote } from 'electron';
import cryptoRandomString from 'crypto-random-string';
import * as ConfigUtil from './config-util';
const { shell } = remote;
import * as LinkUtil from './link-util';
export const openInBrowser = (link: string): void => {
const otp = cryptoRandomString({length: 64});
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 => {

View File

@@ -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 {
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);
}
}