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 `` objects + objectSrc: "object-src 'none';", + // Disallow unknown scripts + scriptSrc: "script-src 'self' 'unsafe-inline';", + // Disallow stylesheets, we allow https because we are loading from Google Fonts (FIXME don't load from google) + styleSrc: "style-src 'self' 'unsafe-inline' https:;", + // Disallow workers, allow `blob:` for camera access if needed + workerSrc: "worker-src 'none';" +}; + +export const CSP = Object.values(CSP_CONFIG).join(' '); diff --git a/packages/electron-app/src/main/util/declarations/pino.d.ts b/packages/electron-app/src/main/util/declarations/pino.d.ts new file mode 100644 index 000000000..df978563c --- /dev/null +++ b/packages/electron-app/src/main/util/declarations/pino.d.ts @@ -0,0 +1 @@ +declare module 'pino'; \ No newline at end of file diff --git a/packages/electron-app/src/main/util/index.ts b/packages/electron-app/src/main/util/index.ts new file mode 100644 index 000000000..fc9bab9b9 --- /dev/null +++ b/packages/electron-app/src/main/util/index.ts @@ -0,0 +1,7 @@ +// 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 * from './constants'; +export * from './csp'; +export * from './staticPath'; diff --git a/packages/light-apps/public/index.html b/packages/light-apps/public/index.html index 225662bc0..d9f45b8b5 100644 --- a/packages/light-apps/public/index.html +++ b/packages/light-apps/public/index.html @@ -1,16 +1,34 @@ - - - - - - + + - - - Substrate Light UI - - - -
- - diff --git a/packages/light-apps/public/manifest.json b/packages/light-apps/public/manifest.json index 1f2f141fa..1bdfe6010 100644 --- a/packages/light-apps/public/manifest.json +++ b/packages/light-apps/public/manifest.json @@ -1,6 +1,6 @@ { - "short_name": "React App", - "name": "Create React App Sample", + "short_name": "SLUI", + "name": "Substrate Light UI", "icons": [ { "src": "favicon.ico", diff --git a/yarn.lock b/yarn.lock index c7a305cd4..4c16cfc6c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3213,11 +3213,6 @@ "@types/styled-components" "*" csstype "^2.2.0" -"@types/tapable@1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.2.tgz#e13182e1b69871a422d7863e11a4a6f5b814a4bd" - integrity sha512-42zEJkBpNfMEAvWR5WlwtTH22oDzcMjFsL9gDGExwF8X8WvAiw7Vwop7hPw03QT8TKfec83LwbHj6SvpqM4ELQ== - "@types/unist@*", "@types/unist@^2.0.0", "@types/unist@^2.0.2": version "2.0.3" resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.3.tgz#9c088679876f374eb5983f150d4787aa6fb32d7e" @@ -8123,6 +8118,16 @@ fast-levenshtein@~2.0.4: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= +fast-redact@^1.4.4: + version "1.5.0" + resolved "https://registry.yarnpkg.com/fast-redact/-/fast-redact-1.5.0.tgz#302892f566750c4f5eec7b830bfc9bc473484034" + integrity sha512-Afo61CgUjkzdvOKDHn08qnZ0kwck38AOGcMlvSGzvJbIab6soAP5rdoQayecGCDsD69AiF9vJBXyq31eoEO2tQ== + +fast-safe-stringify@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.6.tgz#04b26106cc56681f51a044cfc0d76cf0008ac2c2" + integrity sha512-q8BZ89jjc+mz08rSxROs8VsrBBcn1SIw1kq9NjolL509tkABRk9io01RAjSaEv1Xb2uFLt8VtRiZbGp5H8iDtg== + fastparse@^1.1.1: version "1.1.2" resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.2.tgz#91728c5a5942eced8531283c79441ee4122c35a9" @@ -8371,6 +8376,11 @@ flat-cache@^2.0.1: rimraf "2.6.3" write "1.0.3" +flatstr@^1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/flatstr/-/flatstr-1.0.9.tgz#0950d56fec02de1030c1311847ecd58c25690eb9" + integrity sha512-qFlJnOBWDfIaunF54/lBqNKmXOI0HqNhu+mHkLmbaBXlS71PUd9OjFOdyevHt/aHoHB1+eW7eKHgRKOG5aHSpw== + flatted@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.0.tgz#55122b6536ea496b4b44893ee2608141d10d9916" @@ -9276,10 +9286,10 @@ html-to-react@^1.3.4: lodash.camelcase "^4.3.0" ramda "^0.26" -html-webpack-plugin@4.0.0-alpha.2: - version "4.0.0-alpha.2" - resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-4.0.0-alpha.2.tgz#7745967e389a57a098e26963f328ebe4c19b598d" - integrity sha512-tyvhjVpuGqD7QYHi1l1drMQTg5i+qRxpQEGbdnYFREgOKy7aFDf/ocQ/V1fuEDlQx7jV2zMap3Hj2nE9i5eGXw== +html-webpack-plugin@4.0.0-beta.5, html-webpack-plugin@^4.0.0-beta.2: + version "4.0.0-beta.5" + resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-4.0.0-beta.5.tgz#2c53083c1151bfec20479b1f8aaf0039e77b5513" + integrity sha512-y5l4lGxOW3pz3xBTFdfB9rnnrWRPVxlAhX6nrBYIcW+2k2zC3mSp/3DxlWVCMBfnO6UAnoF8OcFn0IMy6kaKAQ== dependencies: html-minifier "^3.5.20" loader-utils "^1.1.0" @@ -12989,6 +12999,23 @@ pinkie@^2.0.0: resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= +pino-std-serializers@^2.3.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/pino-std-serializers/-/pino-std-serializers-2.4.0.tgz#c0588b5ab0905ac55bae2cb61639ed93153681fb" + integrity sha512-ysT2ylXu1aEec9k8cm/lz7emBcfpdxFWHqvHeGXf1wvfw7TKPMGhLWwS+ciHw6u4ffnmV+pkAMF4MUIZmZZdSg== + +pino@^5.12.3: + version "5.12.3" + resolved "https://registry.yarnpkg.com/pino/-/pino-5.12.3.tgz#5ebc7d15b3584b91de8d3cdae01ef3a10e7fb977" + integrity sha512-/PiX7QXOMZz7edRVGm/apt22nkdRByB6ki0ftWcDpHjjNmjnE7UubNd7NeDX7cBey27obxvBvZy3oQuJbz1+Ag== + dependencies: + fast-redact "^1.4.4" + fast-safe-stringify "^2.0.6" + flatstr "^1.0.9" + pino-std-serializers "^2.3.0" + quick-format-unescaped "^3.0.2" + sonic-boom "^0.7.3" + pirates@^4.0.0, pirates@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.1.tgz#643a92caf894566f91b2b986d2c66950a8e2fb87" @@ -14315,6 +14342,11 @@ querystringify@^2.0.0: resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.1.tgz#60e5a5fd64a7f8bfa4d2ab2ed6fdf4c85bad154e" integrity sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA== +quick-format-unescaped@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/quick-format-unescaped/-/quick-format-unescaped-3.0.2.tgz#0137e94d8fb37ffeb70040535111c378e75396fb" + integrity sha512-FXTaCkwvpIlkdKeGDNgcq07SXWS383noQUuZjvdE1QcTt+eLuqof6/BDiEPqB59FWLie/l91+HtlJSw7iCViSA== + quick-lru@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-1.1.0.tgz#4360b17c61136ad38078397ff11416e186dcfbb8" @@ -15894,6 +15926,13 @@ socks@~2.3.2: ip "^1.1.5" smart-buffer "4.0.2" +sonic-boom@^0.7.3: + version "0.7.3" + resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-0.7.3.tgz#cbfc18e87c2b8078b00e38ad9475c05fce5ea696" + integrity sha512-A9EyoIeLD+g9vMLYQKjNCatJtAKdBQMW03+L8ZWWX/A6hq+srRCwdqHrBD1R8oSMLXov3oHN13dljtZf12q2Ow== + dependencies: + flatstr "^1.0.9" + sort-keys@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad"