Skip to content
This repository was archived by the owner on Feb 26, 2020. It is now read-only.

Commit b002de1

Browse files
authored
[stable] Electron security (#136)
* Electron security * Bump to 0.1.3
1 parent e1e66f8 commit b002de1

File tree

13 files changed

+255
-72
lines changed

13 files changed

+255
-72
lines changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "parity-ui",
3-
"version": "0.1.2",
3+
"version": "0.1.3",
44
"description": "The Electron app for Parity UI",
55
"main": ".build/electron.js",
66
"jsnext:main": ".build/electron.js",
@@ -28,7 +28,7 @@
2828
"build": "npm run build:inject && npm run build:app && npm run build:electron && npm run build:embed",
2929
"build:app": "webpack --config webpack/app",
3030
"build:electron": "webpack --config webpack/electron",
31-
"build:inject": "webpack --config webpack/inject",
31+
"build:inject": "webpack --config webpack/inject && webpack --config webpack/preload",
3232
"build:embed": "cross-env EMBED=1 node webpack/embed",
3333
"build:i18n": "npm run clean && npm run build && babel-node ./scripts/build-i18n.js",
3434
"ci:build": "cross-env NODE_ENV=production npm run build",
@@ -186,4 +186,4 @@
186186
"solc": "ngotchac/solc-js",
187187
"store": "1.3.20"
188188
}
189-
}
189+
}

src/Dapp/dapp.css

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
flex-grow: 1;
2424
margin: 0;
2525
padding: 0;
26-
opacity: 0;
2726
width: 100%;
2827
z-index: 1;
2928
}

src/Dapp/dapp.js

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -83,17 +83,16 @@ export default class Dapp extends Component {
8383
// Reput eventListeners when webview has finished loading dapp
8484
webview.addEventListener('did-finish-load', () => {
8585
this.setState({ loading: false });
86-
// Listen to IPC messages from this webview
87-
webview.addEventListener('ipc-message', event =>
88-
this.requestsStore.receiveMessage({
89-
...event.args[0],
90-
source: event.target
91-
}));
92-
// Send ping message to tell dapp we're ready to listen to its ipc messages
93-
webview.send('ping');
9486
});
9587

96-
this.onDappLoad();
88+
// Listen to IPC messages from this webview. More particularly, to IPC
89+
// messages coming from the preload.js injected in this webview.
90+
webview.addEventListener('ipc-message', event => {
91+
this.requestsStore.receiveMessage({
92+
...event.args[0],
93+
source: event.target
94+
});
95+
});
9796
};
9897

9998
loadApp (id) {
@@ -115,7 +114,6 @@ export default class Dapp extends Component {
115114
frameBorder={ 0 }
116115
id='dappFrame'
117116
name={ name }
118-
onLoad={ this.onDappLoad }
119117
ref={ this.handleIframe }
120118
sandbox='allow-forms allow-popups allow-same-origin allow-scripts allow-top-navigation'
121119
scrolling='auto'
@@ -135,16 +133,17 @@ export default class Dapp extends Component {
135133
posixDirName,
136134
'..',
137135
'.build',
138-
'inject.js'
136+
'preload.js'
139137
)}`;
140138

139+
// https://electronjs.org/docs/tutorial/security#3-enable-context-isolation-for-remote-content
141140
return <webview
142141
className={ styles.frame }
143142
id='dappFrame'
144-
nodeintegration='false'
145143
preload={ preload }
146144
ref={ this.handleWebview }
147145
src={ `${src}${hash}` }
146+
webpreferences='contextIsolation'
148147
/>;
149148
}
150149

@@ -210,10 +209,4 @@ export default class Dapp extends Component {
210209
? this.renderWebview(src, hash)
211210
: this.renderIframe(src, hash);
212211
}
213-
214-
onDappLoad = () => {
215-
const frame = document.getElementById('dappFrame');
216-
217-
frame.style.opacity = 1;
218-
}
219212
}

src/index.electron.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,41 @@ function createWindow () {
9191
}
9292
});
9393

94+
// Do not accept all kind of web permissions (camera, location...)
95+
// https://electronjs.org/docs/tutorial/security#4-handle-session-permission-requests-from-remote-content
96+
session.defaultSession
97+
.setPermissionRequestHandler((webContents, permission, callback) => {
98+
if (!webContents.getURL().startsWith('file:')) {
99+
// Denies the permissions request for all non-file://. Currently all
100+
// network dapps are loaded on http://127.0.0.1:8545, so they won't
101+
// have any permissions.
102+
return callback(false);
103+
}
104+
105+
// All others loaded on file:// (shell, builtin, local) can have those
106+
// permissions.
107+
return callback(true);
108+
});
109+
110+
// Verify WebView Options Before Creation
111+
// https://electronjs.org/docs/tutorial/security#12-verify-webview-options-before-creation
112+
mainWindow.webContents.on('will-attach-webview', (event, webPreferences, params) => {
113+
// Strip away inline preload scripts, ours is at preloadURL
114+
delete webPreferences.preload;
115+
// Verify the location of our prelaod script is legitimate (unless uiDev has been passed)
116+
if (webPreferences.preloadURL !== encodeURI(url.format({
117+
pathname: path.join(__dirname, 'preload.js'),
118+
protocol: 'file:',
119+
slashes: true
120+
}))) {
121+
throw new Error(`Unknown preload.js is being injected, quitting for security reasons. ${webPreferences.preloadURL}`);
122+
}
123+
124+
// Disable Node.js integration
125+
webPreferences.nodeIntegration = false;
126+
webPreferences.contextIsolation = true;
127+
});
128+
94129
// Create the Application's main menu
95130
// https://github.com/electron/electron/blob/master/docs/api/menu.md#examples
96131
const template = [

src/index.parity.ejs

Lines changed: 39 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,43 @@
11
<!DOCTYPE html>
22
<html>
3-
<head>
4-
<meta charset="utf-8">
5-
<meta name="viewport" content="width=device-width">
6-
<meta http-equiv="X-UA-Compatible" content="IE=edge">
7-
<title><%= htmlWebpackPlugin.options.title %></title>
8-
<style>
9-
html {
10-
background: white;
11-
background-repeat: round;
12-
}
133

14-
html, body, #container {
15-
width: 100%;
16-
height: 100%;
17-
margin: 0;
18-
padding: 0;
19-
font-family: Sans-Serif;
20-
}
4+
<head>
5+
<meta charset="utf-8">
6+
<meta name="viewport" content="width=device-width">
7+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
8+
<meta http-equiv="Content-Security-Policy" content="<%= htmlWebpackPlugin.options.csp %>">
9+
<title>
10+
<%= htmlWebpackPlugin.options.title %>
11+
</title>
12+
<style>
13+
html {
14+
background: white;
15+
background-repeat: round;
16+
}
2117
22-
.loading {
23-
text-align: center;
24-
padding-top: 5em;
25-
font-size: 2em;
26-
color: #999;
27-
}
28-
</style>
29-
</head>
30-
<body>
31-
<div id="container">
32-
<div class="loading">Loading</div>
33-
</div>
34-
</body>
35-
</html>
18+
html,
19+
body,
20+
#container {
21+
width: 100%;
22+
height: 100%;
23+
margin: 0;
24+
padding: 0;
25+
font-family: Sans-Serif;
26+
}
27+
28+
.loading {
29+
text-align: center;
30+
padding-top: 5em;
31+
font-size: 2em;
32+
color: #999;
33+
}
34+
</style>
35+
</head>
36+
37+
<body>
38+
<div id="container">
39+
<div class="loading">Loading</div>
40+
</div>
41+
</body>
42+
43+
</html>

src/inject.js

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
1616

1717
import Api from '@parity/api';
18-
import isElectron from 'is-electron';
1918
import qs from 'query-string';
2019

2120
console.log('This inject.js has been injected by the shell.');
@@ -26,9 +25,9 @@ function initProvider () {
2625

2726
let appId = match ? match[0] : query.appId;
2827

29-
const ethereum = isElectron()
30-
? new Api.Provider.Ipc(appId)
31-
: new Api.Provider.PostMessage(appId);
28+
// The dapp will use the PostMessage provider, send postMessages to
29+
// preload.js, and preload.js will relay those messages to the shell.
30+
const ethereum = new Api.Provider.PostMessage(appId);
3231

3332
console.log(`Requesting API communications token for ${appId}`);
3433

@@ -69,4 +68,19 @@ if (typeof window !== 'undefined' && !window.isParity) {
6968
initParity(ethereum);
7069

7170
console.warn('Deprecation: Dapps should only used the exposed EthereumProvider on `window.ethereum`, the use of `window.parity` and `window.web3` will be removed in future versions of this injector');
71+
72+
// Disable eval() for dapps
73+
// https://electronjs.org/docs/tutorial/security#7-override-and-disable-eval
74+
//
75+
// TODO Currently Web3 Console dapp needs eval(), and is the only builtin
76+
// that needs it, so we cannot blindly disable it as per the recommendation.
77+
// One idea is to check here in inject.js if allowJsEval is set to true, but
78+
// this requires more work (future PR).
79+
// For now we simply allow eval(). All builtin dapps are trusted and can use
80+
// eval(), and all network dapps are served on 127.0.0.1:8545, which have CSP
81+
// that disallow eval(). So security-wise it should be enough.
82+
//
83+
// window.eval = global.eval = function () { // eslint-disable-line
84+
// throw new Error(`Sorry, this app does not support window.eval().`);
85+
// };
7286
}

src/preload.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
2+
// This file is part of Parity.
3+
4+
// Parity is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
9+
// Parity is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU General Public License for more details.
13+
14+
// You should have received a copy of the GNU General Public License
15+
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
16+
17+
import fs from 'fs';
18+
import { ipcRenderer, remote, webFrame } from 'electron';
19+
import path from 'path';
20+
21+
// The following two lines is just a proxy between the webview and the renderer process.
22+
// If we receive an IPC message from the shell, we relay it to the webview as
23+
// postMessage.
24+
ipcRenderer.on('PARITY_SHELL_IPC_CHANNEL', (_, data) => window.postMessage(data, '*'));
25+
// If we receive a postMessage from the webview, we transfer it to the renderer
26+
// as an IPC message.
27+
window.addEventListener('message', (event) => {
28+
ipcRenderer.sendToHost('PARITY_SHELL_IPC_CHANNEL', { data: event.data });
29+
});
30+
31+
// Load inject.js as a string, and inject it into the webview with executeJavaScript
32+
fs.readFile(path.join(remote.getGlobal('dirName'), '..', '.build', 'inject.js'), 'utf8', (err, injectFile) => {
33+
if (err) {
34+
console.error(err);
35+
return;
36+
}
37+
webFrame.executeJavaScript(injectFile);
38+
});

src/util/csp.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
const shared = [
2+
// Restrict everything to the same origin by default.
3+
"default-src 'self';",
4+
// Allow connecting to WS servers and HTTP(S) servers.
5+
// We could be more restrictive and allow only RPC server URL.
6+
'connect-src http: https: ws: wss:;',
7+
// Allow framing any content from HTTP(S).
8+
// Again we could only allow embedding from RPC server URL.
9+
// (deprecated)
10+
"frame-src 'none';",
11+
// Allow framing and web workers from HTTP(S).
12+
"child-src 'self' http: https:;",
13+
// We allow data: blob: and HTTP(s) images.
14+
// We could get rid of wildcarding HTTP and only allow RPC server URL.
15+
// (http required for local dapps icons)
16+
"img-src 'self' 'unsafe-inline' file: data: blob: http: https:;",
17+
// Allow style from data: blob: and HTTPS.
18+
"style-src 'self' 'unsafe-inline' data: blob: https:;",
19+
// Allow fonts from data: and HTTPS.
20+
"font-src 'self' data: https:;",
21+
// Same restrictions as script-src with additional
22+
// blob: that is required for camera access (worker)
23+
"worker-src 'self' https: blob:;",
24+
// Disallow submitting forms from any dapps
25+
"form-action 'none';",
26+
// Never allow mixed content
27+
'block-all-mixed-content;'
28+
];
29+
30+
// These are the CSP for the renderer process (aka the shell)
31+
const rendererCsp = [
32+
...shared,
33+
// Allow <webview> which are objects
34+
"object-src 'self';",
35+
// Allow scripts
36+
"script-src 'self';"
37+
];
38+
39+
module.exports = {
40+
rendererCsp
41+
};

webpack/app.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const CopyWebpackPlugin = require('copy-webpack-plugin');
2626
const HtmlWebpackPlugin = require('html-webpack-plugin');
2727
const ServiceWorkerWebpackPlugin = require('serviceworker-webpack-plugin');
2828

29+
const { rendererCsp } = require('../src/util/csp');
2930
const rulesEs6 = require('./rules/es6');
3031
const rulesParity = require('./rules/parity');
3132
const Shared = require('./shared');
@@ -54,7 +55,7 @@ module.exports = {
5455
cache: !isProd,
5556
devtool: isProd
5657
? false
57-
: isEmbed ? '#source-map' : '#eval',
58+
: '#source-map',
5859
context: path.join(__dirname, '../src'),
5960
entry,
6061
output: {
@@ -175,6 +176,7 @@ module.exports = {
175176
plugins,
176177

177178
new HtmlWebpackPlugin({
179+
csp: rendererCsp.join(' '),
178180
title: 'Parity UI',
179181
filename: 'index.html',
180182
template: './index.parity.ejs',

0 commit comments

Comments
 (0)