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 { 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.

View File

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

View File

@@ -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>

View File

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

View File

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

View File

@@ -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 {

View File

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

View File

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

View File

@@ -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 => {

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