[MM-20363] prevent using api to navigate within the app's window (#1137)
* prevent using api to navigate within the app's window * api calls handled differently * add aliases to ease development * small refactor of url navigation * hardcode http/s protocol being allowed * add protocols specified on electron-builder
This commit is contained in:
parent
b98e6a451d
commit
58cf6d5b28
|
@ -21,8 +21,10 @@
|
|||
"build:main": "webpack-cli --bail --config webpack.config.main.js",
|
||||
"build:renderer": "webpack-cli --bail --config webpack.config.renderer.js",
|
||||
"start": "electron src --disable-dev-mode",
|
||||
"restart": "npm run build && npm run start",
|
||||
"storybook": "start-storybook -p 9001 -c src/.storybook",
|
||||
"clean": "rm -rf release/ node_modules/ src/node_modules/ && find src -name '*_bundle.js' | xargs rm",
|
||||
"clean-install": "npm run clean && npm install",
|
||||
"watch": "run-p watch:*",
|
||||
"watch:main": "node scripts/watch_main_and_preload.js",
|
||||
"watch:renderer": "webpack-dev-server --config webpack.config.renderer.js",
|
||||
|
|
|
@ -118,13 +118,16 @@ export default class MattermostView extends React.Component {
|
|||
} else if (destURL.path.match(/^\/help\//)) {
|
||||
// continue to open special case internal urls in default browser
|
||||
shell.openExternal(e.url);
|
||||
} else {
|
||||
} else if (Utils.isTeamUrl(this.props.src, e.url, true) || Utils.isPluginUrl(this.props.src, e.url)) {
|
||||
// New window should disable nodeIntegration.
|
||||
window.open(e.url, remote.app.getName(), 'nodeIntegration=no, contextIsolation=yes, show=yes');
|
||||
} else {
|
||||
e.preventDefault();
|
||||
shell.openExternal(e.url);
|
||||
}
|
||||
} else {
|
||||
// if the link is external, use default browser.
|
||||
shell.openExternal(e.url);
|
||||
// if the link is external, use default os' application.
|
||||
ipcRenderer.send('confirm-protocol', destURL.protocol, e.url);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
107
src/main.js
107
src/main.js
|
@ -5,8 +5,6 @@
|
|||
import os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
import {URL} from 'url';
|
||||
|
||||
import electron from 'electron';
|
||||
import isDev from 'electron-is-dev';
|
||||
import installExtension, {REACT_DEVELOPER_TOOLS} from 'electron-devtools-installer';
|
||||
|
@ -427,9 +425,12 @@ function handleAppWebContentsCreated(dc, contents) {
|
|||
|
||||
contents.on('will-navigate', (event, url) => {
|
||||
const contentID = event.sender.id;
|
||||
const parsedURL = parseURL(url);
|
||||
|
||||
if (isTrustedURL(parsedURL) || isTrustedPopupWindow(event.sender)) {
|
||||
const parsedURL = Utils.parseURL(url);
|
||||
const serverURL = Utils.getServer(parsedURL, config.teams);
|
||||
if ((serverURL !== null && Utils.isTeamUrl(serverURL.url, parsedURL)) || isTrustedPopupWindow(event.sender)) {
|
||||
return;
|
||||
}
|
||||
if (isCustomLoginURL(parsedURL)) {
|
||||
return;
|
||||
}
|
||||
if (parsedURL.protocol === 'mailto:') {
|
||||
|
@ -439,7 +440,7 @@ function handleAppWebContentsCreated(dc, contents) {
|
|||
return;
|
||||
}
|
||||
|
||||
log.info(`Untrusted URL blocked: ${url}`);
|
||||
log.info(`Prevented desktop from navigating to: ${url}`);
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
|
@ -450,7 +451,7 @@ function handleAppWebContentsCreated(dc, contents) {
|
|||
// - indicate custom login is NOT in progress
|
||||
contents.on('did-start-navigation', (event, url) => {
|
||||
const contentID = event.sender.id;
|
||||
const parsedURL = parseURL(url);
|
||||
const parsedURL = Utils.parseURL(url);
|
||||
|
||||
if (!isTrustedURL(parsedURL)) {
|
||||
return;
|
||||
|
@ -465,36 +466,42 @@ function handleAppWebContentsCreated(dc, contents) {
|
|||
|
||||
contents.on('new-window', (event, url) => {
|
||||
event.preventDefault();
|
||||
if (!isTrustedURL(url)) {
|
||||
|
||||
const parsedURL = Utils.parseURL(url);
|
||||
const server = Utils.getServer(parsedURL, config.teams);
|
||||
|
||||
if (!server) {
|
||||
log.info(`Untrusted popup window blocked: ${url}`);
|
||||
return;
|
||||
}
|
||||
if (isTeamUrl(url) === true) {
|
||||
if (Utils.isTeamUrl(server.url, parsedURL, true) === true) {
|
||||
log.info(`${url} is a known team, preventing to open a new window`);
|
||||
return;
|
||||
}
|
||||
if (popupWindow && popupWindow.getURL() === url) {
|
||||
if (popupWindow && !popupWindow.closed && popupWindow.getURL() === url) {
|
||||
log.info(`Popup window already open at provided url: ${url}`);
|
||||
return;
|
||||
}
|
||||
if (!popupWindow) {
|
||||
popupWindow = new BrowserWindow({
|
||||
backgroundColor: '#fff', // prevents blurry text: https://electronjs.org/docs/faq#the-font-looks-blurry-what-is-this-and-what-can-i-do
|
||||
parent: mainWindow,
|
||||
show: false,
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
},
|
||||
});
|
||||
popupWindow.once('ready-to-show', () => {
|
||||
popupWindow.show();
|
||||
});
|
||||
popupWindow.once('closed', () => {
|
||||
popupWindow = null;
|
||||
});
|
||||
if (Utils.isPluginUrl(server.url, parsedURL)) {
|
||||
if (!popupWindow || popupWindow.closed) {
|
||||
popupWindow = new BrowserWindow({
|
||||
backgroundColor: '#fff', // prevents blurry text: https://electronjs.org/docs/faq#the-font-looks-blurry-what-is-this-and-what-can-i-do
|
||||
parent: mainWindow,
|
||||
show: false,
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
},
|
||||
});
|
||||
popupWindow.once('ready-to-show', () => {
|
||||
popupWindow.show();
|
||||
});
|
||||
popupWindow.once('closed', () => {
|
||||
popupWindow = null;
|
||||
});
|
||||
}
|
||||
popupWindow.loadURL(url);
|
||||
}
|
||||
popupWindow.loadURL(url);
|
||||
});
|
||||
|
||||
// implemented to temporarily help solve for https://community-daily.mattermost.com/core/pl/b95bi44r4bbnueqzjjxsi46qiw
|
||||
|
@ -924,51 +931,13 @@ function handleMainWindowWebContentsCrashed() {
|
|||
// helper functions
|
||||
//
|
||||
|
||||
function parseURL(url) {
|
||||
if (!url) {
|
||||
return null;
|
||||
}
|
||||
if (url instanceof URL) {
|
||||
return url;
|
||||
}
|
||||
try {
|
||||
return new URL(url);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function isTeamUrl(url) {
|
||||
const parsedURL = parseURL(url);
|
||||
if (!parsedURL) {
|
||||
return null;
|
||||
}
|
||||
if (isCustomLoginURL(parsedURL)) {
|
||||
return false;
|
||||
}
|
||||
const nonTeamUrlPaths = ['plugins', 'signup', 'login', 'admin', 'channel', 'post', 'api', 'oauth'];
|
||||
return !(nonTeamUrlPaths.some((testPath) => parsedURL.pathname.toLowerCase().startsWith(`/${testPath}/`)));
|
||||
}
|
||||
|
||||
function isTrustedURL(url) {
|
||||
const parsedURL = parseURL(url);
|
||||
const parsedURL = Utils.parseURL(url);
|
||||
if (!parsedURL) {
|
||||
console.log('not an url');
|
||||
return false;
|
||||
}
|
||||
|
||||
const teamURLs = config.teams.reduce((urls, team) => {
|
||||
const parsedTeamURL = parseURL(team.url);
|
||||
if (parsedTeamURL) {
|
||||
return urls.concat(parsedTeamURL);
|
||||
}
|
||||
return urls;
|
||||
}, []);
|
||||
for (const teamURL of teamURLs) {
|
||||
if (parsedURL.origin === teamURL.origin) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return Utils.getServer(parsedURL, config.teams) !== null;
|
||||
}
|
||||
|
||||
function isTrustedPopupWindow(webContents) {
|
||||
|
@ -982,7 +951,7 @@ function isTrustedPopupWindow(webContents) {
|
|||
}
|
||||
|
||||
function isCustomLoginURL(url) {
|
||||
const parsedURL = parseURL(url);
|
||||
const parsedURL = Utils.parseURL(url);
|
||||
if (!parsedURL) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -8,17 +8,33 @@ import fs from 'fs';
|
|||
|
||||
import {app, dialog, ipcMain, shell} from 'electron';
|
||||
|
||||
import {protocols} from '../../electron-builder.json';
|
||||
|
||||
import * as Validator from './Validator';
|
||||
|
||||
const allowedProtocolFile = path.resolve(app.getPath('userData'), 'allowedProtocols.json');
|
||||
let allowedProtocols = [];
|
||||
|
||||
function addScheme(scheme) {
|
||||
const proto = `${scheme}:`;
|
||||
if (!allowedProtocols.includes(proto)) {
|
||||
allowedProtocols.push(proto);
|
||||
}
|
||||
}
|
||||
|
||||
function init(mainWindow) {
|
||||
fs.readFile(allowedProtocolFile, 'utf-8', (err, data) => {
|
||||
if (!err) {
|
||||
allowedProtocols = JSON.parse(data);
|
||||
allowedProtocols = Validator.validateAllowedProtocols(allowedProtocols) || [];
|
||||
}
|
||||
addScheme('http');
|
||||
addScheme('https');
|
||||
protocols.forEach((protocol) => {
|
||||
if (protocol.schemes && protocol.schemes.length > 0) {
|
||||
protocol.schemes.forEach(addScheme);
|
||||
}
|
||||
});
|
||||
initDialogEvent(mainWindow);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -19,6 +19,20 @@ function isValidURI(testURL) {
|
|||
return Boolean(isUri(testURL));
|
||||
}
|
||||
|
||||
function parseURL(inputURL) {
|
||||
if (!inputURL) {
|
||||
return null;
|
||||
}
|
||||
if (inputURL instanceof URL) {
|
||||
return inputURL;
|
||||
}
|
||||
try {
|
||||
return new URL(inputURL);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// isInternalURL determines if the target url is internal to the application.
|
||||
// - currentURL is the current url inside the webview
|
||||
// - basename is the global export from the Mattermost application defining the subpath, if any
|
||||
|
@ -34,6 +48,57 @@ function isInternalURL(targetURL, currentURL, basename = '/') {
|
|||
return true;
|
||||
}
|
||||
|
||||
function getServerInfo(serverUrl) {
|
||||
const parsedServer = parseURL(serverUrl);
|
||||
if (!parsedServer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// does the server have a subpath?
|
||||
const pn = parsedServer.pathname.toLowerCase();
|
||||
const subpath = pn.endsWith('/') ? pn.toLowerCase() : `${pn}/`;
|
||||
return {origin: parsedServer.origin, subpath, url: parsedServer};
|
||||
}
|
||||
|
||||
function isTeamUrl(serverUrl, inputUrl, withApi) {
|
||||
const parsedURL = parseURL(inputUrl);
|
||||
const server = getServerInfo(serverUrl);
|
||||
if (!parsedURL || !server) {
|
||||
return null;
|
||||
}
|
||||
const nonTeamUrlPaths = ['plugins', 'signup', 'login', 'admin', 'channel', 'post', 'oauth', 'admin_console'];
|
||||
if (withApi) {
|
||||
nonTeamUrlPaths.push('api');
|
||||
}
|
||||
return !(nonTeamUrlPaths.some((testPath) => parsedURL.pathname.toLowerCase().startsWith(`${server.subpath}${testPath}/`)));
|
||||
}
|
||||
|
||||
function isPluginUrl(serverUrl, inputURL) {
|
||||
const server = getServerInfo(serverUrl);
|
||||
const parsedURL = parseURL(inputURL);
|
||||
if (!parsedURL || !server) {
|
||||
return false;
|
||||
}
|
||||
return server.origin === parsedURL.origin && parsedURL.pathname.toLowerCase().startsWith(`${server.subpath}plugins/`);
|
||||
}
|
||||
|
||||
function getServer(inputURL, teams) {
|
||||
const parsedURL = parseURL(inputURL);
|
||||
if (!parsedURL) {
|
||||
return null;
|
||||
}
|
||||
let parsedServerUrl;
|
||||
for (let i = 0; i < teams.length; i++) {
|
||||
parsedServerUrl = parseURL(teams[i].url);
|
||||
|
||||
// check server and subpath matches (without subpath pathname is \ so it always matches)
|
||||
if (parsedServerUrl.origin === parsedURL.origin && parsedURL.pathname.startsWith(parsedServerUrl.pathname)) {
|
||||
return {name: teams[i].name, url: parsedServerUrl};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getDisplayBoundaries() {
|
||||
const {screen} = electron;
|
||||
|
||||
|
@ -56,5 +121,9 @@ export default {
|
|||
isValidURL,
|
||||
isValidURI,
|
||||
isInternalURL,
|
||||
parseURL,
|
||||
getServer,
|
||||
isTeamUrl,
|
||||
isPluginUrl,
|
||||
getDisplayBoundaries,
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue