diff --git a/packages/electron-app/README.md b/packages/electron-app/README.md index 0727535bd..e4aa075d4 100644 --- a/packages/electron-app/README.md +++ b/packages/electron-app/README.md @@ -11,3 +11,172 @@ This command assumes that we've built a CRA app inside `packages/light-apps`. It ### `yarn start` Runs `electron-webpack dev`, which tells Electron to load http://localhost:3000, which is most likely the port on which CRA's dev mode runs. It reloads Electron on any code change inside the electron codebase. + +## Electron Security Checklist: +https://electronjs.org/docs/tutorial/security#1-only-load-secure-content + +Inspired by: https://github.com/paritytech/fether/pull/451 + +#### Cheatsheet +1. Only Load Secure Content (https/wss/ftps over http/ws/ftp) +2. Disable Node.js Integration for Remote Content + * Prevents XSS attacks from escalating to a Remote Code Execution Attack - limits the attack surface to the renderer process. + * **Node Integration + XSS in renderer process = RCE** +
+ const mainWindow = new BrowserWindow({
+ webPreferences: {
+ nodeIntegration: false,
+ nodeIntegrationInWorker: false,
+ preload: path.join(app.getAppPath(), 'preload.js')
+ }
+ });
+
+ * Good: instead of allowing full access to the Node API (including stuff like fs, io, etc.), only export necessary features via a preload script, making sure to validate arguments properly.
+
+ **N.B. SLUI: preload not necessary, as there's no need for main<->renderer communication (yet)**
+3. Context Isolation - separates Javascript contexts between preload scripts and the page's scripts, and between Electon's internal scripts and the page's scripts, preventing so-called "Prototype Pollution Attacks", i.e. extra protection against RCE's.
+ * `contextIsolation` is false by default.
+ * this means by default, you have shared context between the preload scripts and the page's scripts, i.e. `window.variable` set in `preload.js` is defined in the `BrowserWindow`'s context.
+
+ **N.B. SLUI: again not a concern considering we don't need preload scripts at all, but good to keep in mind.**
+4. Handle Session Permission Requests from Remote Content
+ * The default setting in Electron is to approve all permission requests unless the developer explicitly tells it to handle it a particular way.
+ **N.B. SLUI: only some permissions matter in our context.**
+ Required Permissions:
+ ```
+ contentSettings
+ debugger: only Dev.
+ desktopCapture: only Dev.
+ fileBrowserHandler: This is needed for JSON File Recovery.
+ idle: It could be a useful security measure in case user is AFK
+ notifications: Yes - extend from Alerts.
+ ```
+
+ * Electron implements the same types of permissions as Chromium as listed below:
+ ```
+ activeTab: we don't use tabs
+ alarms: not needed
+ background: not needed
+ bookmarks: not needed
+ browsingData: not needed
+ certificateProvider: yes
+ clipboardRead: no
+ clipboardWrite: no
+ contentSettings: only allow https, ftps, wss
+ contextMenus: no
+ cookies: no
+ debugger: not needed in production, only in development environment.
+ declarativeContent
+ declarativeNetRequest: cannot see a need to block or redirect network requests ATOW.
+ declarativeWebRequest: cannot see a need to block or redirect network requests ATOW.
+ desktopCapture: disable in Production, enable in Development.
+ displaySource: no.
+ dns: no.
+ documentScan: no
+ downloads: no.
+ enterprise.deviceAttributes: no.
+ enterprise.hardwarePlatform: no.
+ enterprise.platformKeys: no.
+ experimental: no.
+ fileBrowserHandler: This is needed for JSON File Recovery.
+ fileSystemProvider: no need
+ fontSettings: no need
+ gcm: no need
+ geolocation: definitely no need for this.
+ history: no need | using react router
+ identity: no need
+ idle: It could be a useful security measure in case user is AFK
+ management: no need
+ nativeMessaging: no need
+ networking.config: no need
+ notifications: Yes.
+ pageCapture: no need
+ platformKeys: no need.
+ power: no need.
+ printerProvider: no need.
+ privacy: no need.
+ processes: no.
+ proxy: no.
+ sessions: no.
+ signedInDevices: no.
+ storage: no.
+ system.cpu: no
+ system.display: no
+ system.memory: no
+ system.storage: no
+ tabCapture: no.
+ tabs: no need
+ topSites: no
+ tts: nice for accessibility in the future but honestly probably not necessary.
+ ttsEngine: nice for accessibility in the future but honestly probably not necessary.
+ unlimitedStorage
+ vpnProvider: no
+ wallpaper: not needed
+ webNavigation: no
+ webRequest: no
+ webRequestBlocking: no
+ ```
+5. Do Not Disable WebSecurity (default) - self explanatory
+6. Define a Content Security Policy - not relevant (yet) as we're not loading any external web pages
+7. Do Not Set allowRunningInsecureContent to true (default) - self explanatory
+8. Do Not Enable Experimental Features (default) - self explanatory
+9. Do Not Use enableBlinkFeatures (default) - self explanatory
+10. Do Not Use allowpopups (default) - self explanatory
+11. Verify WebView Options Before Creation:
+ * A WebView created in the renderer process will always create its own renderer process using its own `webPreferences`.
+ ```
+ app.on('web-contents-created', (event, contents) => {
+ contents.on('will-attach-webview', (event, webPreferences, params) => {
+ // Strip away preload scripts if unused or verify their location is legitimate
+ delete webPreferences.preload
+ delete webPreferences.preloadURL
+
+ // Disable Node.js integration
+ webPreferences.nodeIntegration = false
+
+ // Verify URL being loaded
+ if (!params.src.startsWith('https://example.com/')) {
+ event.preventDefault()
+ }
+ })
+ })
+ ```
+ **N.B. SLUI: we don't load any webpages, and if we do in the future it'll be in a browser so this is not a concern.**
+12. Disable or limit navigation
+```
+ const URL = require('url').URL
+
+ app.on('web-contents-created', (event, contents) => {
+ contents.on('will-navigate', (event, navigationUrl) => {
+ const parsedUrl = new URL(navigationUrl)
+
+ if (parsedUrl.origin !== 'https://example.com') {
+ event.preventDefault()
+ }
+ })
+ })
+```
+**N.B. SLUI: While not used yet, we may in the future want to navigate to the api docs page, or the forum, or other substrate related sites.**
+
+13. Disable or limit creation of new windows
+```
+const { shell } = require('electron')
+
+app.on('web-contents-created', (event, contents) => {
+ contents.on('new-window', (event, navigationUrl) => {
+ // if somehow there is a new-window event, block it.
+ event.preventDefault()
+ })
+})
+```
+**N.B. SLUI: we just need the one BrowserWindow**
+14. Do not use openExternal with untrusted content
+15. Disable the remote module: the `remote` module would allow the renderer process to access APIs normally only available in the main process.
+```
+ const mainWindow = new BrowserWindow({
+ webPreferences: {
+ enableRemoteModule: false
+ }
+ })
+```
+16. Filter the remote module: relevant only if we cannot disable remote module.
diff --git a/packages/electron-app/package.json b/packages/electron-app/package.json
index c86d14c12..e3c29dd7e 100644
--- a/packages/electron-app/package.json
+++ b/packages/electron-app/package.json
@@ -27,6 +27,7 @@
"dependencies": {
"@types/electron-devtools-installer": "^2.2.0",
"electron-devtools-installer": "^2.2.4",
+ "pino": "^5.12.3",
"source-map-support": "^0.5.10"
}
}
diff --git a/packages/electron-app/src/main/index.ts b/packages/electron-app/src/main/index.ts
index 96983a14f..d1a46e66a 100644
--- a/packages/electron-app/src/main/index.ts
+++ b/packages/electron-app/src/main/index.ts
@@ -4,19 +4,42 @@
import electron from 'electron';
import installExtension, { REACT_DEVELOPER_TOOLS } from 'electron-devtools-installer';
+import Pino from 'pino';
import path from 'path';
import url from 'url';
-import { staticPath } from './util/staticPath';
+import { CSP, staticPath } from './util';
-const { app, BrowserWindow } = electron;
+const { app, BrowserWindow, session } = electron;
+const pino = Pino();
let mainWindow: Electron.BrowserWindow | undefined;
+// https://electronjs.org/docs/tutorial/security#electron-security-warnings
+process.env.ELECTRON_ENABLE_SECURITY_WARNINGS = 'true';
+
function createWindow () {
mainWindow = new BrowserWindow({
height: 1920,
resizable: true,
- width: 1440
+ width: 1440,
+ webPreferences: {
+ allowRunningInsecureContent: false,
+ contextIsolation: true, // prevent context sharing between renderer<->main processes
+ devTools: true,
+ enableBlinkFeatures: '', // https://electronjs.org/docs/tutorial/security#9-do-not-use-enableblinkfeatures
+ enableRemoteModule: false,
+ experimentalFeatures: false, // Do not set to true
+ navigateOnDragDrop: false,
+ nativeWindowOpen: true,
+ nodeIntegration: false, // https://electronjs.org/docs/tutorial/security#2-disable-nodejs-integration-for-remote-content
+ nodeIntegrationInWorker: false, // https://electronjs.org/docs/tutorial/security#2-disable-nodejs-integration-for-remote-content
+ plugins: false,
+ sandbox: true, // isolate all BrowserWindow instance environments
+ safeDialogs: true,
+ safeDialogsMessage: 'Electron consecutive dialog protection was triggered',
+ webSecurity: true, // https://electronjs.org/docs/tutorial/security#5-do-not-disable-websecurity
+ webviewTag: false // Associated with `will-attach-webview`
+ }
});
mainWindow.loadURL(
@@ -34,6 +57,23 @@ function createWindow () {
.catch((err: string) => console.log('An error occurred: ', err));
}
+ // Content Security Policy (CSP)
+ session.defaultSession!.webRequest.onHeadersReceived((details, callback) => {
+ // Note: `onHeadersReceived` will not be called in prod, because we use the
+ // file:// protocol: https://electronjs.org/docs/tutorial/security#csp-meta-tag
+ // Instead, the CSP are the ones in the meta tag inside index.html
+ pino.debug(
+ 'Configuring Content-Security-Policy for environment development'
+ );
+
+ callback({
+ responseHeaders: {
+ ...details.responseHeaders,
+ 'Content-Security-Policy': [CSP]
+ }
+ });
+ });
+
mainWindow.on('closed', () => {
mainWindow = undefined;
});
@@ -49,6 +89,13 @@ app.on('activate', function () {
}
});
+// Uncomment this block if ever we decide to use a webview in the app.
+// app.on('web-contents-created', (event, contents) => {
+// contents.on('will-attach-webview', (event, webPreferences, params) => {
+// event.preventDefault();
+// });
+// });
+
// Quit when all windows are closed.
app.on('window-all-closed', function () {
// On OS X it is common for applications and their menu bar
diff --git a/packages/electron-app/src/main/util/constants.ts b/packages/electron-app/src/main/util/constants.ts
new file mode 100644
index 000000000..960b0f70f
--- /dev/null
+++ b/packages/electron-app/src/main/util/constants.ts
@@ -0,0 +1,5 @@
+// Copyright 2018-2019 @paritytech/substrate-light-ui authors & contributors
+// This software may be modified and distributed under the terms
+// of the Apache-2.0 license. See the LICENSE file for details.
+
+export const IS_PROD = process.env.NODE_ENV === 'production';
diff --git a/packages/electron-app/src/main/util/csp.ts b/packages/electron-app/src/main/util/csp.ts
new file mode 100644
index 000000000..9f5a809f0
--- /dev/null
+++ b/packages/electron-app/src/main/util/csp.ts
@@ -0,0 +1,47 @@
+// Copyright 2018-2019 @paritytech/substrate-light-ui authors & contributors
+// This software may be modified and distributed under the terms
+// of the Apache-2.0 license. See the LICENSE file for details.
+
+import { IS_PROD } from './constants';
+
+// References:
+// * https://github.com/parity-js/shell
+// * https://github.com/paritytech/fether
+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: IS_PROD
+ ? 'connect-src ws:;'
+ // Also allow http in dev mode, for CRA
+ : '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, we allow https because we are loading from Google Fonts (FIXME don't load from google)
+ fontSrc: "font-src 'self' data: https:;",
+ // Disallow submitting any forms
+ formAction: "form-action 'none';",
+ // Disallow framing.
+ frameSrc: "frame-src 'none';",
+ // Restrict images to only images from known sources
+ imgSrc: "img-src 'self' data:;",
+ // Disallow manifests.
+ manifestSrc: "manifest-src 'none';",
+ // Disallow media.
+ mediaSrc: "media-src 'none';",
+ // Disallow fonts and `