diff --git a/package.json b/package.json index c40fae5be..6d4751b88 100644 --- a/package.json +++ b/package.json @@ -44,8 +44,8 @@ "build": "lerna run build", "preelectron": "yarn build", "electron": "cd packages/fether-electron && yarn electron", - "lint-files": "./scripts/lint-files.sh", - "lint": "yarn lint-files '**/*.js'", + "lint-files": "./scripts/lint-files.sh '**/*.js'", + "lint": "yarn lint-files", "prepackage": "yarn build", "package": "cd packages/fether-electron && yarn package", "release": "cd packages/fether-electron && yarn release", @@ -76,4 +76,4 @@ "ts-node": "^8.0.3", "typescript": "^3.3.4000" } -} \ No newline at end of file +} diff --git a/packages/fether-electron/build/icons/webpack/compile.png b/packages/fether-electron/build/icons/webpack/compile.png new file mode 100644 index 000000000..0233a8492 Binary files /dev/null and b/packages/fether-electron/build/icons/webpack/compile.png differ diff --git a/packages/fether-electron/build/icons/webpack/failure.png b/packages/fether-electron/build/icons/webpack/failure.png new file mode 100644 index 000000000..7554523c3 Binary files /dev/null and b/packages/fether-electron/build/icons/webpack/failure.png differ diff --git a/packages/fether-electron/build/icons/webpack/success.png b/packages/fether-electron/build/icons/webpack/success.png new file mode 100644 index 000000000..c800d4e86 Binary files /dev/null and b/packages/fether-electron/build/icons/webpack/success.png differ diff --git a/packages/fether-electron/build/icons/webpack/warning.png b/packages/fether-electron/build/icons/webpack/warning.png new file mode 100644 index 000000000..f92a4beb2 Binary files /dev/null and b/packages/fether-electron/build/icons/webpack/warning.png differ diff --git a/packages/fether-electron/custom.webpack.additions.js b/packages/fether-electron/custom.webpack.additions.js new file mode 100644 index 000000000..3bfebd124 --- /dev/null +++ b/packages/fether-electron/custom.webpack.additions.js @@ -0,0 +1,22 @@ +// https://webpack.electron.build/add-ons +// https://www.npmjs.com/package/webpack-build-notifier +const path = require('path'); +const WebpackBuildNotifierPlugin = require('webpack-build-notifier'); + +const withWebpackBuildNotifier = process.env.NOTIFIER === 'true'; + +module.exports = withWebpackBuildNotifier + ? { + plugins: [ + new WebpackBuildNotifierPlugin({ + title: 'Fether Webpack Build', + logo: path.resolve('./build/icons/icon.ico'), + suppressSuccess: false, + compileIcon: path.resolve('./build/icons/webpack/compile.png'), + failureIcon: path.resolve('./build/icons/webpack/failure.png'), + successIcon: path.resolve('./build/icons/webpack/success.png'), + warningIcon: path.resolve('./build/icons/webpack/warning.png') + }) + ] + } + : {}; diff --git a/packages/fether-electron/electron-webpack.json b/packages/fether-electron/electron-webpack.json index 021c27ab4..df173bc1b 100644 --- a/packages/fether-electron/electron-webpack.json +++ b/packages/fether-electron/electron-webpack.json @@ -1,5 +1,9 @@ { + "main": { + "webpackConfig": "custom.webpack.additions.js" + }, "renderer": { "sourceDirectory": null - } + }, + "title": "Fether" } diff --git a/packages/fether-electron/package.json b/packages/fether-electron/package.json index 253809301..6384cc9f7 100644 --- a/packages/fether-electron/package.json +++ b/packages/fether-electron/package.json @@ -30,12 +30,12 @@ "scripts": { "prebuild": "copyfiles -u 2 \"../fether-react/build/**/*\" static/ && ./scripts/fixElectronBug.sh", "build": "electron-webpack", - "electron": "cross-env SKIP_PREFLIGHT_CHECK=true electron dist/main/main.js", + "electron": "electron dist/main/main.js", "prepackage": "./scripts/revertElectronBug.sh", "package": "electron-builder", "prerelease": "./scripts/revertElectronBug.sh", "release": "electron-builder", - "start": "cross-env ELECTRON_START_URL=http://localhost:3000 electron-webpack dev --ws-origins all", + "start": "cross-env ELECTRON_START_URL=http://localhost:3000 electron-webpack dev", "test": "jest --all --color --coverage" }, "dependencies": { @@ -48,7 +48,8 @@ "fether-react": "^0.3.0", "pino": "^4.16.1", "pino-multi-stream": "^3.1.2", - "source-map-support": "^0.5.10" + "source-map-support": "^0.5.10", + "url-pattern": "^1.0.3" }, "devDependencies": { "copyfiles": "^2.1.0", @@ -56,6 +57,7 @@ "electron": "^4.0.1", "electron-builder": "^20.38.5", "electron-webpack": "^2.6.1", - "webpack": "^4.29.1" + "webpack": "^4.29.1", + "webpack-build-notifier": "^0.1.30" } -} \ No newline at end of file +} diff --git a/packages/fether-electron/src/main/app/cli/index.js b/packages/fether-electron/src/main/app/cli/index.js index 09a590264..49a6299e5 100644 --- a/packages/fether-electron/src/main/app/cli/index.js +++ b/packages/fether-electron/src/main/app/cli/index.js @@ -4,7 +4,7 @@ // SPDX-License-Identifier: BSD-3-Clause import cli from 'commander'; - +import { DEFAULT_CHAIN, DEFAULT_WS_PORT } from '../constants'; const { productName } = require('../../../../electron-builder.json'); const { version } = require('../../../../package.json'); @@ -24,22 +24,17 @@ cli .allowUnknownOption() .option( '--chain ', - 'The network to connect to, can be one of "foundation", "kovan" or "ropsten". (default: "kovan")', - 'kovan' + `The network to connect to, can be one of "foundation", "kovan" or "ropsten". (default: "${DEFAULT_CHAIN}")`, + DEFAULT_CHAIN ) .option( '--no-run-parity', `${productName} will not attempt to run the locally installed parity.` ) - .option( - '--ws-interface ', - `Specify the hostname portion of the WebSockets server ${productName} will connect to. IP should be an interface's IP address. (default: 127.0.0.1)`, - '127.0.0.1' - ) .option( '--ws-port ', - `Specify the port portion of the WebSockets server ${productName} will connect to. (default: 8546)`, - 8546 + `Specify the port portion of the WebSockets server ${productName} will connect to. (default: ${DEFAULT_WS_PORT})`, + DEFAULT_WS_PORT ) .parse( @@ -47,7 +42,13 @@ cli // We want to ignore some flags and not pass them down to Parity: // --inspect: `electron-webpack dev` runs Electron with the `--inspect` flag for HMR // -psn_*: https://github.com/paritytech/fether/issues/188 - .filter(arg => !arg.startsWith('--inspect') && !arg.startsWith('-psn_')) + .filter( + arg => + !arg.startsWith('--inspect') && + !arg.startsWith('-psn_') && + !arg.startsWith('--ws-interface') && + !arg.startsWith('--ws-origins') + ) ); export default cli; diff --git a/packages/fether-electron/src/main/app/constants/index.js b/packages/fether-electron/src/main/app/constants/index.js new file mode 100644 index 000000000..3368f83be --- /dev/null +++ b/packages/fether-electron/src/main/app/constants/index.js @@ -0,0 +1,27 @@ +// Copyright 2015-2019 Parity Technologies (UK) Ltd. +// This file is part of Parity. +// +// SPDX-License-Identifier: BSD-3-Clause + +import { IS_PACKAGED } from '../utils/paths'; + +const IS_PROD = process.env.NODE_ENV === 'production'; + +/** + * Security. Additional network security is configured after `cli` is available: + * in fether-electron/src/main/app/options/config/index.js + * + * Note: 127.0.0.1 is a trusted loopback and more trustworthy than localhost. + * See https://letsencrypt.org/docs/certificates-for-localhost/ + */ +const DEFAULT_CHAIN = 'kovan'; +const DEFAULT_WS_PORT = '8546'; +const TRUSTED_LOOPBACK = '127.0.0.1'; + +export { + DEFAULT_CHAIN, + DEFAULT_WS_PORT, + IS_PACKAGED, + IS_PROD, + TRUSTED_LOOPBACK +}; diff --git a/packages/fether-electron/src/main/app/menu/template/index.js b/packages/fether-electron/src/main/app/menu/template/index.js index b67beb1e8..9377470cf 100644 --- a/packages/fether-electron/src/main/app/menu/template/index.js +++ b/packages/fether-electron/src/main/app/menu/template/index.js @@ -4,6 +4,7 @@ // SPDX-License-Identifier: BSD-3-Clause import electron from 'electron'; +import { IS_PROD } from '../../constants'; const { shell } = electron; @@ -170,7 +171,7 @@ const getContextTrayMenuTemplate = fetherApp => { } ]; - if (process.env.NODE_ENV !== 'production') { + if (!IS_PROD) { template.push({ label: 'Reload', click: () => fetherApp.win.webContents.reload() diff --git a/packages/fether-electron/src/main/app/methods/setupGlobals.js b/packages/fether-electron/src/main/app/methods/setupGlobals.js index ab62136c3..af38acaba 100644 --- a/packages/fether-electron/src/main/app/methods/setupGlobals.js +++ b/packages/fether-electron/src/main/app/methods/setupGlobals.js @@ -3,11 +3,13 @@ // // SPDX-License-Identifier: BSD-3-Clause +import { DEFAULT_WS_PORT, TRUSTED_LOOPBACK } from '../constants'; import cli from '../cli'; function setupGlobals () { // Globals for fether-react parityStore - global.wsInterface = cli.wsInterface; + global.defaultWsInterface = TRUSTED_LOOPBACK; + global.defaultWsPort = DEFAULT_WS_PORT; global.wsPort = cli.wsPort; } diff --git a/packages/fether-electron/src/main/app/methods/setupRequestListeners.js b/packages/fether-electron/src/main/app/methods/setupRequestListeners.js index 09745f124..e0449fd60 100644 --- a/packages/fether-electron/src/main/app/methods/setupRequestListeners.js +++ b/packages/fether-electron/src/main/app/methods/setupRequestListeners.js @@ -5,7 +5,12 @@ import electron from 'electron'; +import { IS_PROD } from '../constants'; +import { CSP } from '../utils/csp'; import messages from '../messages'; +import Pino from '../utils/pino'; + +const pino = Pino(); const { ipcMain, session } = electron; function setupRequestListeners (fetherApp) { @@ -30,6 +35,24 @@ function setupRequestListeners (fetherApp) { callback({ requestHeaders: details.requestHeaders }); // eslint-disable-line } ); + + // Content Security Policy (CSP) + session.defaultSession.webRequest.onHeadersReceived((details, callback) => { + pino.debug( + `Configuring Content-Security-Policy for environment ${ + IS_PROD ? 'production' : 'development' + }` + ); + + /* eslint-disable */ + callback({ + responseHeaders: { + ...details.responseHeaders, + "Content-Security-Policy": [CSP] + } + }); + /* eslint-enable */ + }); } export default setupRequestListeners; diff --git a/packages/fether-electron/src/main/app/methods/setupWinListeners.js b/packages/fether-electron/src/main/app/methods/setupWinListeners.js index dd83a61c2..24d463d3e 100644 --- a/packages/fether-electron/src/main/app/methods/setupWinListeners.js +++ b/packages/fether-electron/src/main/app/methods/setupWinListeners.js @@ -3,20 +3,71 @@ // // SPDX-License-Identifier: BSD-3-Clause -import electron from 'electron'; import debounce from 'lodash/debounce'; +import { SECURITY_OPTIONS } from '../options/config'; import Pino from '../utils/pino'; +const { TRUSTED_HOSTS } = SECURITY_OPTIONS.fetherNetwork; +const trustedHostsAll = Object.values(TRUSTED_HOSTS).flat(); const pino = Pino(); function setupWinListeners (fetherApp) { const { onWindowClose, processSaveWinPosition, win } = fetherApp; - // Open external links in browser - win.webContents.on('new-window', (event, url) => { - event.preventDefault(); - electron.shell.openExternal(url); + /** + * Insecure TLS Validation - verify the application does not explicitly opt-out of TLS validation + * + * References: + * - https://doyensec.com/resources/us-17-Carettoni-Electronegativity-A-Study-Of-Electron-Security-wp.pdf + * - https://electronjs.org/docs/api/session#sessetcertificateverifyprocproc + */ + win.webContents.session.setCertificateVerifyProc((request, callback) => { + const { hostname, certificate, verificationResult, errorCode } = request; // eslint-disable-line + + pino.debug( + 'Processing server certificate verification request for the session in setCertificateVerifyProc with hostname: ', + hostname + ); + + if (errorCode) { + pino.error( + 'Error processing server certificate verification request for the session in setCertificateVerifyProc: ', + errorCode + ); + + // Failure accepting certificate due to errorCode + callback(-2); // eslint-disable-line + } else if (!trustedHostsAll.includes(hostname)) { + pino.info( + 'Failure accepting server certification due to its hostname being an untrusted host in setCertificateVerifyProc: ', + hostname + ); + + // Failure accepting server certificate due to its source hostname being untrusted + callback(-2); // eslint-disable-line + } else if (!verificationResult === 'net::OK') { + pino.info( + 'Failure accepting server certificate due to it failing Chromium verification: ', + hostname, + verificationResult + ); + + // Failure accepting server certificate due to it failing Chromium verification + callback(-2); // eslint-disable-line + } else { + pino.info( + 'Fallback to using the verification result from Chromium: ', + hostname, + verificationResult + ); + + // Fallback to using the verification result from Chromium + callback(-3); // eslint-disable-line + + // // Success and accept the certifcate, disable Certificate Transparency verification + // callback(0); // eslint-disable-line + } }); // Windows and Linux (unchecked on others) diff --git a/packages/fether-electron/src/main/app/options/config/index.js b/packages/fether-electron/src/main/app/options/config/index.js index d32798ff1..a16c88d7d 100644 --- a/packages/fether-electron/src/main/app/options/config/index.js +++ b/packages/fether-electron/src/main/app/options/config/index.js @@ -6,10 +6,61 @@ import path from 'path'; import url from 'url'; +import Pino from '../../utils/pino'; import { staticPath } from '../../utils/paths'; +import cli from '../../cli'; +import { + DEFAULT_CHAIN, + DEFAULT_WS_PORT, + IS_PROD, + TRUSTED_LOOPBACK +} from '../../constants'; + +const pino = Pino(); + +pino.info( + `Running Fether in ${IS_PROD ? 'production' : 'development'} environment` +); + +/** + * Note: If the user provides a custom CLI port to `cli.wsPort` then + * we 'dynamically' trust it in addition to the `DEFAULT_WS_PORT` in + * fether-electron/src/main/index.js, which is where we only + * permit requests from trusted paths. + * + * WARNING: DO NOT add the custom CLI interface `cli.wsInterface` as a + * trusted host. This may avoid Fether being launched with a + * malicious remote `cli.wsInterface` and sending sensitive user information + * (i.e. account password) over RPC. + * See https://github.com/paritytech/fether/pull/451#discussion_r268732256 + * + * Note: We also disallows users from using Fether + * with a remote node. + * WARNING: SSH tunnels from an attacker are still possible. + */ +const DEFAULT_HTTP_PORT = '3000'; +const CUSTOM_WS_PORT = cli.wsPort; +const TRUSTED_HOSTS = { + github: ['api.github.com', 'github.com', 'raw.githubusercontent.com'], + blockscout: ['blockscout.com'] +}; +const TRUSTED_WS_PORTS = [DEFAULT_WS_PORT, CUSTOM_WS_PORT]; +const DEFAULT_HTTP_TRUSTED_LOOPBACK = `http://${TRUSTED_LOOPBACK}:${DEFAULT_HTTP_PORT}`; +const TRUSTED_URLS = [ + DEFAULT_HTTP_TRUSTED_LOOPBACK, + `ws://${TRUSTED_LOOPBACK}:${DEFAULT_WS_PORT}`, + `ws://${TRUSTED_LOOPBACK}:${CUSTOM_WS_PORT}`, + 'https://parity.io', + 'https://wiki.parity.io/Fether-FAQ', + 'https://github.com/paritytech/fether/issues/new', + 'https://api.github.com/repos/paritytech/fether/releases/latest' +]; + +// https://electronjs.org/docs/tutorial/security#electron-security-warnings +process.env.ELECTRON_ENABLE_SECURITY_WARNINGS = true; const INDEX_HTML_PATH = - process.env.ELECTRON_START_URL || + (!IS_PROD && process.env.ELECTRON_START_URL) || url.format({ pathname: path.join(staticPath, 'build', 'index.html'), protocol: 'file:', @@ -60,10 +111,6 @@ const DEFAULT_OPTIONS = { show: false, // Run showWindow later showDockIcon: true, // macOS usage only tabbingIdentifier: 'parity', - webPreferences: { - devTools: true, - enableRemoteModule: true // Remote is required in fether-react parityStore.js - }, width: 360, windowPosition: windowPosition, // Required withTaskbar: false @@ -79,4 +126,87 @@ const TASKBAR_OPTIONS = { withTaskbar: true }; -export { DEFAULT_OPTIONS, TASKBAR_OPTIONS }; +const SECURITY_OPTIONS = { + /** + * Note: The keys used in this options object are passed as an argument + * to the `BrowserWindow` as defined in the Electron API documents + * https://electronjs.org/docs/api/browser-window#browserwindow. + * However, `fetherNetwork` is a custom property that is not part of + * of the API and has been added just to keep the configuration together. + * It has been given a unique name to prevent naming conflicts. + */ + fetherNetwork: { + DEFAULT_CHAIN, + DEFAULT_WS_PORT, + TRUSTED_HOSTS, + TRUSTED_LOOPBACK, + TRUSTED_URLS, + TRUSTED_WS_PORTS + }, + webPreferences: { + /** + * Potential security risk options set explicitly even when default is favourable. + * Reference: https://electronjs.org/docs/tutorial/security + */ + devTools: true, + /** + * `nodeIntegration` when enabled allows the software to use Electron's APIs + * and gain access to Node.js. It must be disabled to restricting access to + * Node.js global symbols like `require` from global scope and requires the + * user to sanitise user inputs to reduce the possible XSS attack surface. + */ + nodeIntegration: false, // Must be disabled + nodeIntegrationInWorker: false, // Must be disabled + /** + * Electron security recommends us to set this to `true`. However, we need + * some communication between the main process and the renderer process + * (via ipcMain and ipcRenderer), so we need to disabled contextIsolation. + * https://stackoverflow.com/questions/55164360/with-contextisolation-true-is-it-possible-to-use-ipcrenderer + * Currently experimental and may change or be removed in future Electron releases. + */ + contextIsolation: false, // Should be enabled + /** + * Isolate access to Electron/Node.js from the Fether web app, by creating + * a bridge which plays the role of an API between main and renderer + * processes. + * https://github.com/electron/electron/issues/9920#issuecomment-336757899 + */ + preload: path.join(staticPath, 'preload.js'), + /** + * Sandbox the BrowserWindow renderer associated with the window to mitigate + * against the risk of malicious preload scripts, whilst still allowing access to + * all underlying Electron/Node.js primitives using `remote` or internal IPC + * Reference: https://doyensec.com/resources/us-17-Carettoni-Electronegativity-A-Study-Of-Electron-Security-wp.pdf + */ + sandbox: true, // Do not set to false. Run electron with `electron --enable-sandbox` to sandbox all BrowserWindow instances + enableRemoteModule: true, // Remote is required in fether-react parityStore.js + // Enables same origin policy to prevent execution of insecure code. Do not set to false + webSecurity: true, + allowRunningInsecureContent: false, // Do not set to true + plugins: false, + experimentalFeatures: false, // Do not set to true + enableBlinkFeatures: '', // Do not enable any of them + nativeWindowOpen: true, + /** + * `webviewTag` when enabled allows content to be embedded into the + * Electron app and to be run as a separate process when Electron handles + * new browser windows. It is important to reduce privileges + * to try and prevent attackers from controlling the new browser windows + * with the `window.open` command and passing a WebView tag + * (see `webView`) to enable `nodeIntegration`. + * + * If any webview's https://electronjs.org/docs/api/webview-tag are implemented + * then it is important to check if it is necessary to update security by + * implementing the `''will-attach-webview'` listener + * https://electronjs.org/blog/webview-fix to intercept and prevent + * a new WebView (that may be used by an attacker to gain access to the + * file system) in addition to setting `webviewTag: false`. + */ + webviewTag: false, // Associated with `will-attach-webview` + safeDialogs: true, + safeDialogsMessage: 'Electron consecutive dialog protection was triggered', + navigateOnDragDrop: false + } +}; + +export { DEFAULT_OPTIONS, SECURITY_OPTIONS, TASKBAR_OPTIONS }; diff --git a/packages/fether-electron/src/main/app/options/index.js b/packages/fether-electron/src/main/app/options/index.js index f6c547b25..1364d4f90 100644 --- a/packages/fether-electron/src/main/app/options/index.js +++ b/packages/fether-electron/src/main/app/options/index.js @@ -3,9 +3,15 @@ // // SPDX-License-Identifier: BSD-3-Clause -import { DEFAULT_OPTIONS, TASKBAR_OPTIONS } from './config'; +import { DEFAULT_OPTIONS, SECURITY_OPTIONS, TASKBAR_OPTIONS } from './config'; export default (withTaskbar, customOptions) => withTaskbar - ? Object.assign({}, DEFAULT_OPTIONS, TASKBAR_OPTIONS, customOptions || {}) - : Object.assign({}, DEFAULT_OPTIONS, customOptions || {}); + ? Object.assign( + {}, + DEFAULT_OPTIONS, + TASKBAR_OPTIONS, + customOptions || {}, + SECURITY_OPTIONS + ) + : Object.assign({}, DEFAULT_OPTIONS, customOptions || {}, SECURITY_OPTIONS); diff --git a/packages/fether-electron/src/main/app/parityEthereum/index.js b/packages/fether-electron/src/main/app/parityEthereum/index.js index 384fa5517..c2c78d483 100644 --- a/packages/fether-electron/src/main/app/parityEthereum/index.js +++ b/packages/fether-electron/src/main/app/parityEthereum/index.js @@ -61,7 +61,6 @@ class ParityEthereum { isRunning = async () => { return isParityRunning({ - wsInterface: cli.wsInterface, wsPort: cli.wsPort }); }; @@ -75,8 +74,6 @@ class ParityEthereum { '--light', '--chain', cli.chain, - '--ws-interface', - cli.wsInterface, '--ws-port', cli.wsPort ], diff --git a/packages/fether-electron/src/main/app/utils/csp.js b/packages/fether-electron/src/main/app/utils/csp.js new file mode 100644 index 000000000..f7c53f7bc --- /dev/null +++ b/packages/fether-electron/src/main/app/utils/csp.js @@ -0,0 +1,57 @@ +// Copyright 2015-2019 Parity Technologies (UK) Ltd. +// This file is part of Parity. +// +// SPDX-License-Identifier: BSD-3-Clause + +import { IS_PROD } from '../constants'; + +/* eslint-disable */ +// References: https://github.com/parity-js/shell +const CSP_CONFIG = { + // Disallow mixed content + blockAllMixedContent: "block-all-mixed-content;", + // Disallow framing and web workers. + childSrc: "child-src 'none';", + // FIXME - Only allow connecting to WSS and HTTPS servers. + connectSrc: "connect-src http: ws:;", + // Fallback for missing directives. + // Reference: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/default-src + // + // Disallow everything as fallback by default for all CSP fetch directives. + defaultSrc: "default-src 'none';", + // Disallow fonts. + fontSrc: "font-src 'none';", // Additionally used in Parity-JS Shell `'self' data: https:` + // Disallow submitting any forms + formAction: "form-action 'none';", + // Disallow framing. + frameSrc: "frame-src 'none';", + imgSrc: !IS_PROD + ? // Only allow HTTPS for images. Token provider logos must be https:// + // Allow `data:` `blob:`. + "img-src 'self' 'unsafe-inline' file: data: blob: https:;" + : // Only allow HTTPS for images. Token provider logos must be https:// + // Allow `data:` `blob:`. + "img-src 'unsafe-inline' file: data: blob: https:;", // Additionally used in Parity-JS Shell `'self'` + // Disallow manifests. + manifestSrc: "manifest-src 'none';", + // Disallow media. + mediaSrc: "media-src 'none';", + // Disallow fonts and `` objects + objectSrc: "object-src 'none';", + // Disallow prefetching. + prefetchSrc: "prefetch-src 'none';", + scriptSrc: !IS_PROD + ? // Only allow `http:` and `unsafe-eval` in dev mode (required by create-react-app) + "script-src 'self' file: http: blob: 'unsafe-inline' 'unsafe-eval';" + : "script-src file: 'unsafe-inline';", + styleSrc: !IS_PROD + ? "style-src 'self' 'unsafe-inline' file: blob:;" // Additionally used in Parity-JS Shell `data: https:` + : "style-src unsafe-inline' file: blob:;", // Additionally used in Parity-JS Shell `data: https:` + // Allow `blob:` for camera access (worker) + workerSrc: "worker-src blob:;" // Additionally used in Parity-JS Shell `'self' https:` +}; +/* eslint-enable */ + +const CSP = Object.values(CSP_CONFIG).join(' '); + +export { CSP }; diff --git a/packages/fether-electron/src/main/app/utils/isTrustedUrlPattern.js b/packages/fether-electron/src/main/app/utils/isTrustedUrlPattern.js new file mode 100644 index 000000000..d3e541bd9 --- /dev/null +++ b/packages/fether-electron/src/main/app/utils/isTrustedUrlPattern.js @@ -0,0 +1,141 @@ +// Copyright 2015-2019 Parity Technologies (UK) Ltd. +// This file is part of Parity. +// +// SPDX-License-Identifier: BSD-3-Clause + +import UrlPattern from 'url-pattern'; + +import { SECURITY_OPTIONS } from '../options/config'; + +const { TRUSTED_HOSTS } = SECURITY_OPTIONS.fetherNetwork; + +// Github +const GITHUB_TRUSTED_HOSTS = TRUSTED_HOSTS.github; + +// Blockscout - Only includes those that are available on the Blockscout website +const BLOCKSCOUT_TRUSTED_HOSTS = TRUSTED_HOSTS.blockscout; +const BLOCKSCOUT_CHAINS_SUPPORTED = ['eth', 'etc', 'poa']; +const BLOCKSCOUT_NETWORKS_SUPPORTED = [ + 'mainnet', + 'classic', + 'ropsten', + 'kovan', + 'goerli', + 'core', + 'dai', + 'sokol', + 'rinkeby' +]; +const BLOCKSCOUT_HASH_KINDS = ['tx', 'address']; +const BLOCKSCOUT_HASH_TRAILERS = [ + 'coin_balances', + 'internal_transactions', + 'tokens', + 'transactions' +]; +const HASH_ADDRESS_LENGTH = 40; +const HASH_TX_LENGTH = 64; + +// Url Patterns +const GENERAL_PATTERN = new UrlPattern( + '(https\\://)(:subdomain.):domain.:tld(\\::port)(/*)' +); +const GITHUB_PATTERN_1 = new UrlPattern( + '(https\\://)(:subdomain.):domain.:tld(/)(*)(.png|.jpg)' +); +const GITHUB_PATTERN_2 = new UrlPattern( + '(https\\://)(:subdomain.):domain.:tld(/)atomiclabs/cryptocurrency-icons/(*)(.png|.jpg)' +); +const GITHUB_PATTERN_3 = new UrlPattern( + '(https\\://)(:subdomain.):domain.:tld(/)ethcore/(*)(.png|.jpg)' +); +const BLOCKSCOUT_PATTERN = new UrlPattern( + '(https\\://)(:subdomain.):domain.:tld(/):chain(/):network(/):hashKind(/0x):hash(/:hashTrailer)' +); + +function isValidGithubUrl (url) { + const match = + GITHUB_PATTERN_1.match(url) || + GITHUB_PATTERN_2.match(url) || + GITHUB_PATTERN_3.match(url); + + if (!match) { + return false; + } + + return true; +} + +function isValidBlockscoutUrl (url) { + const match = BLOCKSCOUT_PATTERN.match(url); + + if (!match) { + return false; + } + + if ( + BLOCKSCOUT_CHAINS_SUPPORTED.includes(match.chain) && + BLOCKSCOUT_NETWORKS_SUPPORTED.includes(match.network) && + BLOCKSCOUT_HASH_KINDS.includes(match.hashKind) && + [HASH_ADDRESS_LENGTH, HASH_TX_LENGTH].includes(match.hash.length) && + BLOCKSCOUT_HASH_TRAILERS.includes(match.hashTrailer) + ) { + return true; + } + + return false; +} + +/** + * List of trusted subdomains, domain extensions, and ports, as relevant. + * Detect trusted prefix and then call method to validate it. + */ +function isValidUrl (url) { + const match = GENERAL_PATTERN.match(url); + + if (!match) { + return false; + } + + if (!url.startsWith('https')) { + return false; + } + + // Blockscout URL + if ( + BLOCKSCOUT_TRUSTED_HOSTS.includes( + (match.subdomain && match.subdomain) + + (match.subdomain && '.') + + (match.domain && match.domain) + + (match.domain && '.') + + (match.tld && match.tld) + ) || + BLOCKSCOUT_TRUSTED_HOSTS.includes( + (match.domain && match.domain) + + (match.domain && '.') + + (match.tld && match.tld) + ) + ) { + return isValidBlockscoutUrl(url); + } + + // Github URL + if ( + GITHUB_TRUSTED_HOSTS.includes( + (match.subdomain && match.subdomain) + + (match.subdomain && '.') + + (match.domain && match.domain) + + (match.domain && '.') + + (match.tld && match.tld) + ) || + GITHUB_TRUSTED_HOSTS.includes( + (match.domain && match.domain) + + (match.domain && '.') + + (match.tld && match.tld) + ) + ) { + return isValidGithubUrl(url); + } +} + +export default isValidUrl; diff --git a/packages/fether-electron/src/main/app/utils/isTrustedUrlPattern.spec.js b/packages/fether-electron/src/main/app/utils/isTrustedUrlPattern.spec.js new file mode 100644 index 000000000..e09905735 --- /dev/null +++ b/packages/fether-electron/src/main/app/utils/isTrustedUrlPattern.spec.js @@ -0,0 +1,61 @@ +// Copyright 2015-2019 Parity Technologies (UK) Ltd. +// This file is part of Parity. +// +// SPDX-License-Identifier: BSD-3-Clause + +/* eslint-env jest */ + +import isTrustedUrlPattern from './isTrustedUrlPattern'; + +jest.mock('./pino', () => () => ({ + info: () => {} +})); + +let url; + +describe('trust url pattern', () => { + describe('blockscout', () => { + test('should not trust insecure (non-https) links', () => { + url = 'http://blockscout.com'; + + expect(isTrustedUrlPattern(url)).toBe(false); + }); + + test('should trust link to internal transaction on ethereum mainnet', () => { + url = + 'https://blockscout.com/eth/mainnet/tx/0xfe7e97d1de24b47d92e815024757e388809425f1681920bc1923368ec1f0fcf1/internal_transactions'; + + expect(isTrustedUrlPattern(url)).toBe(true); + }); + }); + + describe('github token icons', () => { + test('should trust token .png icons from Github ethcore', () => { + url = + 'https://raw.githubusercontent.com/ethcore/dapp-assets/9e135f76fe9ba61e2d8ccbd72ed144c26c450780/tokens/gavcoin-64x64.png'; + + expect(isTrustedUrlPattern(url)).toBe(true); + }); + + test('should trust token .svg icons from Github ethcore', () => { + url = + 'https://raw.githubusercontent.com/ethcore/dapp-assets/9e135f76fe9ba61e2d8ccbd72ed144c26c450780/tokens/gavcoin-64x64.svg'; + + expect(isTrustedUrlPattern(url)).toBe(true); + }); + + test('should trust token icons from Github atomiclabs', () => { + url = + 'https://raw.githubusercontent.com/atomiclabs/cryptocurrency-icons/tree/master/32/black/arg.png'; + + expect(isTrustedUrlPattern(url)).toBe(true); + }); + }); + + describe('chrome developer tools', () => { + url = + 'chrome-devtools://devtools/bundled/toolbox.html?remoteBase=https://chrome-devtools-frontend.appspot.com/serve_file/@123/&can_dock=true&toolbarColor=rgba(223,223,223,1)&textColor=rgba(0,0,0,1)&experiments=true'; + + expect(isTrustedUrlPattern(url)).toBe(false); + }); +}); diff --git a/packages/fether-electron/src/main/app/utils/paths.js b/packages/fether-electron/src/main/app/utils/paths.js index b715d3ab2..ebb62ae2d 100644 --- a/packages/fether-electron/src/main/app/utils/paths.js +++ b/packages/fether-electron/src/main/app/utils/paths.js @@ -3,20 +3,21 @@ // // SPDX-License-Identifier: BSD-3-Clause -/* global __static */ - +import electron from 'electron'; import path from 'path'; -const appIsPackaged = !process.defaultApp; +const { app } = electron; +const IS_TEST = !app; +const IS_PACKAGED = !IS_TEST && app.isPackaged; /** * Get the path to the `static` folder. * * @see https://github.com/electron-userland/electron-webpack/issues/52 */ -const staticPath = appIsPackaged +const staticPath = IS_PACKAGED ? __dirname.replace(/app\.asar$/, 'static') - : __static; + : path.join(process.cwd(), 'static'); /** * Get the path to the bundled Parity Ethereum binary. @@ -26,4 +27,4 @@ const bundledParityPath = ? path.join(staticPath, 'parity.exe') : path.join(staticPath, 'parity'); -export { staticPath, bundledParityPath }; +export { IS_PACKAGED, bundledParityPath, staticPath }; diff --git a/packages/fether-electron/src/main/index.js b/packages/fether-electron/src/main/index.js index fa11368e7..ce096535c 100644 --- a/packages/fether-electron/src/main/index.js +++ b/packages/fether-electron/src/main/index.js @@ -4,18 +4,26 @@ // SPDX-License-Identifier: BSD-3-Clause import electron from 'electron'; +import { URL } from 'url'; import { killParity } from '@parity/electron'; import Pino from './app/utils/pino'; +import isTrustedUrlPattern from './app/utils/isTrustedUrlPattern'; import FetherApp from './app'; +import { SECURITY_OPTIONS } from './app/options/config'; import fetherAppOptions from './app/options'; -const { app } = electron; const pino = Pino(); +const { app, shell } = electron; +const { TRUSTED_URLS } = SECURITY_OPTIONS.fetherNetwork; let withTaskbar = process.env.TASKBAR !== 'false'; pino.info('Platform detected: ', process.platform); +pino.info('Process type: ', process.type); +pino.info('Process ID: ', process.pid); +pino.info('Process args: ', process.argv); +pino.info('Electron version: ', process.versions['electron']); // Disable gpu acceleration on linux // https://github.com/parity-js/fether/issues/85 @@ -26,12 +34,36 @@ if (!['darwin', 'win32'].includes(process.platform)) { let fetherApp; const options = fetherAppOptions(withTaskbar, {}); -app.on('ready', () => { +const gotTheLock = app.requestSingleInstanceLock(); +pino.info( + `Single Fether instance lock obtained by ${ + app.hasSingleInstanceLock() ? 'this instance' : 'another instance' + }` +); + +if (!gotTheLock) { + pino.info( + 'Multiple instances of Fether on the same device are not permitted' + ); + app.quit(); +} + +app.once('ready', () => { fetherApp = new FetherApp(app, options); return fetherApp; }); +// Prevent a second instance of Fether. Focus the first window instance +app.on('second-instance', (event, commandLine, workingDirectory) => { + if (fetherApp.win) { + if (fetherApp.win.isMinimized()) { + fetherApp.win.restore(); + } + fetherApp.win.focus(); + } +}); + // Event triggered by clicking the Electron icon in the menu Dock // Reference: https://electronjs.org/docs/api/app#event-activate-macos app.on('activate', (event, hasVisibleWindows) => { @@ -66,7 +98,78 @@ app.on('will-quit', killParity); app.on('quit', () => { pino.info('Leaving Fether'); + app.releaseSingleInstanceLock(); killParity(); }); +// Security +app.on('web-contents-created', (eventOuter, win) => { + win.on('will-navigate', (event, url) => { + // FIXME - check that parser is memory-safe + // + // Reference: https://letsencrypt.org/docs/certificates-for-localhost/ + + const parsedUrl = new URL(url); + + pino.info( + 'Processing request to navigate to url in will-navigate listener: ', + parsedUrl.href + ); + + if ( + !TRUSTED_URLS.includes(parsedUrl.href) && + !isTrustedUrlPattern(parsedUrl.href) + ) { + pino.info( + 'Unable to navigate to untrusted content url due to will-navigate listener: ', + parsedUrl.href + ); + } + }); + + /** + * Security. Intercept new-window events (i.e. `window.open`) before opening + * external links in the browser by overriding event.newGuest without using + * the supplied options tag to try to mitigate risk of an exploit re-enabling + * node integration despite being turned off in the configuration + * (i.e. `nodeIntegration: false`). + * + * References: + * - https://www.electronjs.org/blog/webview-fix + * - https://blog.scottlogic.com/2016/03/09/As-It-Stands-Electron-Security.html + */ + win.on( + 'new-window', + (event, url, frameName, disposition, options, additionalFeatures) => { + event.preventDefault(); + + const parsedUrl = new URL(url); + + pino.info( + 'Processing request to navigate to url in new-window listener: ', + parsedUrl.href + ); + + if (!TRUSTED_URLS.includes(parsedUrl.href) && !isTrustedUrlPattern(url)) { + pino.info( + 'Unable to open new window with untrusted content url due to new-window listener: ', + parsedUrl.href + ); + + return; + } + + // FIXME - Note that we need to check for a valid certificate in 'certificate-error' event handler + // so we only allow trusted content. + // See https://electronjs.org/docs/tutorial/security#14-do-not-use-openexternal-with-untrusted-content + shell.openExternal(url); + } + ); + + // Security vulnerability fix https://electronjs.org/blog/window-open-fix + win.on('-add-new-contents', event => { + event.preventDefault(); + }); +}); + export { fetherApp }; diff --git a/packages/fether-electron/static/preload.js b/packages/fether-electron/static/preload.js new file mode 100644 index 000000000..5b1ca2354 --- /dev/null +++ b/packages/fether-electron/static/preload.js @@ -0,0 +1,53 @@ +// Copyright 2015-2019 Parity Technologies (UK) Ltd. +// This file is part of Parity. +// +// SPDX-License-Identifier: BSD-3-Clause + +/** + * Preload script is run before Electron loads the guest page into + * the renderer so it still has access to Electron and Node.js APIs + * allowing us to use it to define a gated API in an isolated context that + * only exposes the bare minimum functionality that the Fether web app + * requires (instead of unrestricted access to Node.js file system and + * network stack). + * + * Reference: https://slack.engineering/interops-labyrinth-sharing-code-between-web-electron-apps-f9474d62eccc + */ + +const { ipcRenderer, remote } = require('electron'); + +const IS_PROD = process.env.NODE_ENV === 'production'; + +function init () { + console.log( + `Initialising Electron Preload Script in environment: ${ + IS_PROD ? 'production' : 'development' + }` + ); + + /** + * Expose only a bridging API to the Fether web app. + * Set methods on global `window`. Additional methods added later by web app + * + * Do not expose functionality or APIs that could compromise the computer + * such as core Electron (i.e. `electron`, `remote`), IPC (`ipcRenderer`) + * or Node.js modules like `require`. + * + * Note however that we require `ipcRenderer` to be exposed for communication + * between the main process and the renderer process. Hence why + * we have had no other choice but to set `contextIsolation: false` + * + * Example 1: Do not expose as `window.bridge.electron` or `window.bridge.remote`. + * Example 2: `require` should not be defined in Chrome Developer Tools Console. + */ + window.bridge = { + defaultWsInterface: remote.getGlobal('defaultWsInterface'), + defaultWsPort: remote.getGlobal('defaultWsPort'), + ipcRenderer, + isParityRunningStatus: remote.getGlobal('isParityRunning'), + IS_PROD, + wsPort: remote.getGlobal('wsPort') + }; +} + +init(); diff --git a/packages/fether-react/package.json b/packages/fether-react/package.json index 106802333..5a909418b 100644 --- a/packages/fether-react/package.json +++ b/packages/fether-react/package.json @@ -52,7 +52,6 @@ "file-saver": "^2.0.0", "final-form": "^4.8.3", "final-form-calculate": "^1.2.1", - "is-electron": "^2.1.0", "localforage": "^1.7.2", "localforage-observable": "^1.4.0", "lodash": "^4.17.10", @@ -84,4 +83,4 @@ "not ie <= 11", "not op_mini all" ] -} \ No newline at end of file +} diff --git a/packages/fether-react/public/index.html b/packages/fether-react/public/index.html index e6e279423..9401b38d9 100644 --- a/packages/fether-react/public/index.html +++ b/packages/fether-react/public/index.html @@ -4,7 +4,7 @@ - +