Skip to content

Commit

Permalink
electron: only allow browser-window
Browse files Browse the repository at this point in the history
Only allow http request from Electron's own browser-window. Token is
generated within electron-main, which also sets it as a cookie within
browser-windows. Token is passed to the backend via environment
variables. The backend is looking for this specific token to authorize
requests.

Signed-off-by: Paul Maréchal <paul.marechal@ericsson.com>
  • Loading branch information
paul-marechal committed Feb 24, 2020
1 parent 4d4f6c2 commit 4994c00
Show file tree
Hide file tree
Showing 10 changed files with 240 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ function start() {
themeService.loadUserTheme();
const application = container.get(FrontendApplication);
application.start();
return application.start();
}
module.exports = Promise.resolve()${this.compileFrontendModuleImports(frontendModules)}
Expand Down Expand Up @@ -129,10 +129,12 @@ if (process.env.LC_ALL) {
}
process.env.LC_NUMERIC = 'C';
const uuid = require('uuid');
const electron = require('electron');
const { join, resolve } = require('path');
const { fork } = require('child_process');
const { app, dialog, shell, BrowserWindow, ipcMain, Menu, globalShortcut } = electron;
const { ElectronSecurityToken } = require('@theia/core/lib/electron-common/electron-token');
const applicationName = \`${this.pck.props.frontend.config.applicationName}\`;
const isSingleInstance = ${this.pck.props.backend.config.singleInstance === true ? 'true' : 'false'};
Expand All @@ -148,6 +150,10 @@ const nativeKeymap = require('native-keymap');
const Storage = require('electron-store');
const electronStore = new Storage();
const electronSecurityToken = {
value: uuid.v4(),
};
app.on('ready', () => {
if (disallowReloadKeybinding) {
Expand Down Expand Up @@ -300,7 +306,17 @@ app.on('ready', () => {
const loadMainWindow = (port) => {
if (!mainWindow.isDestroyed()) {
mainWindow.loadURL('file://' + join(__dirname, '../../lib/index.html') + '?port=' + port);
mainWindow.webContents.session.cookies.set({
url: \`http://localhost:\${port}/\`,
name: ElectronSecurityToken,
value: JSON.stringify(electronSecurityToken),
}, error => {
if (error) {
console.error(error);
} else {
mainWindow.loadURL('file://' + join(__dirname, '../../lib/index.html') + '?port=' + port);
}
});
}
};
Expand All @@ -322,14 +338,17 @@ app.on('ready', () => {
// We need to distinguish between bundled application and development mode when starting the clusters.
// See: https://github.com/electron/electron/issues/6337#issuecomment-230183287
if (devMode) {
process.env[ElectronSecurityToken] = JSON.stringify(electronSecurityToken);
require(mainPath).then(address => {
loadMainWindow(address.port);
}).catch((error) => {
console.error(error);
app.exit(1);
});
} else {
const cp = fork(mainPath, [], { env: Object.assign({}, process.env) });
const cp = fork(mainPath, [], { env: Object.assign({
[ElectronSecurityToken]: JSON.stringify(electronSecurityToken),
}, process.env) });
cp.on('message', (address) => {
loadMainWindow(address.port);
});
Expand Down
5 changes: 5 additions & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"@primer/octicons-react": "^9.0.0",
"@theia/application-package": "^0.15.0",
"@types/body-parser": "^1.16.4",
"@types/cookie": "^0.3.3",
"@types/express": "^4.16.0",
"@types/fs-extra": "^4.0.2",
"@types/lodash.debounce": "4.0.3",
Expand All @@ -22,6 +23,7 @@
"@types/yargs": "^11.1.0",
"ajv": "^6.5.3",
"body-parser": "^1.17.2",
"cookie": "^0.4.0",
"es6-promise": "^4.2.4",
"express": "^4.16.3",
"file-icons-js": "^1.0.3",
Expand Down Expand Up @@ -61,6 +63,9 @@
"frontend": "lib/browser/keyboard/browser-keyboard-module",
"frontendElectron": "lib/electron-browser/keyboard/electron-keyboard-module",
"backendElectron": "lib/electron-node/keyboard/electron-backend-keyboard-module"
},
{
"backendElectron": "lib/electron-node/token/electron-token-backend-module"
}
],
"keywords": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export class WebSocketConnectionProvider {
}
this.onIncomingMessageActivityEmitter.fire(undefined);
};
this.socket = socket;
(this.socket as ReconnectingWebSocket) = socket;
}

/**
Expand Down
26 changes: 26 additions & 0 deletions packages/core/src/electron-common/electron-token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/********************************************************************************
* Copyright (C) 2020 Ericsson and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

/**
* This token is unique the the current running instance. It is used by the backend
* to make sure it is an electron browser window that is connecting to its services.
*
* The identifier is a string, which makes it usable as a key for cookies or similar.
*/
export const ElectronSecurityToken = 'x-theia-electron-token';
export interface ElectronSecurityToken {
value: string;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/********************************************************************************
* Copyright (C) 2020 Ericsson and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import * as ws from 'ws';
import * as http from 'http';
import express = require('express');
import { injectable, inject } from 'inversify';
import { BackendApplicationContribution } from '../../node';
import { MessagingContribution } from '../../node/messaging/messaging-contribution';
import { ElectronTokenValidator } from './electron-token-validator';

/**
* This component contributes a middleware that will refuse all requests that do not include a specific token.
*/
@injectable()
export class ElectronTokenBackendContribution implements BackendApplicationContribution {

@inject(ElectronTokenValidator)
protected readonly tokenValidator: ElectronTokenValidator;

configure(app: express.Application): void {
app.use(this.expressMiddleware.bind(this));
}

/**
* Only allow token-bearers.
*/
protected expressMiddleware(req: express.Request, res: express.Response, next: express.NextFunction): void {
if (this.tokenValidator.allowRequest(req)) {
next();
} else {
console.error(`refused an http request: ${req.connection.remoteAddress}`);
res.sendStatus(403);
}
}

}

/**
* Override the browser MessagingContribution class to refuse connections that do not include a specific token.
*/
@injectable()
export class ElectronMessagingContribution extends MessagingContribution {

@inject(ElectronTokenValidator)
protected readonly tokenValidator: ElectronTokenValidator;

/**
* In a perfect world, this check would happen on WebSocket handshake, but to avoid a refactoring
* we'll simply close the WebSocket connection here if no valid token is found in the request.
*/
protected handleConnection(socket: ws, request: http.IncomingMessage): void {
if (this.tokenValidator.allowRequest(request)) {
super.handleConnection(socket, request);
} else {
console.error(`refused a websocket connection: ${request.connection.remoteAddress}`);
socket.close();
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/********************************************************************************
* Copyright (C) 2020 Ericsson and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import { ContainerModule } from 'inversify';
import { MessagingContribution } from '../../node/messaging/messaging-contribution';
import { ElectronSecurityToken } from '../../electron-common/electron-token';
import { ElectronMessagingContribution, ElectronTokenBackendContribution } from './electron-token-backend-contribution';
import { BackendApplicationContribution, MessagingService } from '../../node';
import { ElectronTokenValidator } from './electron-token-validator';

export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind<Promise<ElectronSecurityToken>>(Promise).toDynamicValue(async () =>
JSON.parse(process.env[ElectronSecurityToken]!)
).inSingletonScope().whenTargetNamed(ElectronSecurityToken);

bind<ElectronTokenValidator>(ElectronTokenValidator).toSelf().inSingletonScope();
bind<ElectronTokenBackendContribution>(ElectronTokenBackendContribution).toSelf().inSingletonScope();
for (const contribution of [ElectronTokenBackendContribution, ElectronTokenValidator]) {
bind<BackendApplicationContribution>(BackendApplicationContribution).toService(contribution);
}

rebind<MessagingContribution>(MessagingService.Identifier).to(ElectronMessagingContribution).inSingletonScope();
});
66 changes: 66 additions & 0 deletions packages/core/src/electron-node/token/electron-token-validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/********************************************************************************
* Copyright (C) 2020 Ericsson and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import * as http from 'http';
import * as cookie from 'cookie';
import { injectable, inject, named } from 'inversify';
import { ElectronSecurityToken } from '../../electron-common/electron-token';
import { BackendApplicationContribution } from '../../node';

/**
* On Electron, we want to make sure that only electron windows access the backend services.
*/
@injectable()
export class ElectronTokenValidator implements BackendApplicationContribution {

@inject(Promise) @named(ElectronSecurityToken)
protected readonly electronSecurityTokenPromise: Promise<ElectronSecurityToken>;

/**
* `electronSecurityToken` can be undefined while the value is being resolved.
*/
protected electronSecurityToken: ElectronSecurityToken | undefined;

async onStart(): Promise<void> {
this.electronSecurityToken = await this.electronSecurityTokenPromise;
}

allowRequest(request: http.IncomingMessage): boolean {
const token = this.extractTokenFromRequest(request);
return typeof token !== 'undefined' && this.isTokenValid(token);
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
isTokenValid(token: any): boolean {
return typeof token === 'object' && token.value === this.electronSecurityToken!.value;
}

/**
* Expects the token to be passed via cookies by default.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
protected extractTokenFromRequest(request: http.IncomingMessage): any {
const cookieHeader = request.headers.cookie;
if (typeof cookieHeader === 'string') {
const token = cookie.parse(cookieHeader)[ElectronSecurityToken];
if (typeof token === 'string') {
return JSON.parse(token);
}
}
return undefined;
}

}
4 changes: 2 additions & 2 deletions packages/core/src/node/messaging/messaging-backend-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ import { MessagingService } from './messaging-service';
export const messagingBackendModule = new ContainerModule(bind => {
bindContributionProvider(bind, ConnectionContainerModule);
bindContributionProvider(bind, MessagingService.Contribution);
bind(MessagingService.Identifier).to(MessagingContribution).inSingletonScope();
bind(MessagingContribution).toDynamicValue(({ container }) => {
const child = container.createChild();
child.bind(MessagingContainer).toConstantValue(container);
child.bind(MessagingContribution).toSelf();
return child.get(MessagingContribution);
return child.get(MessagingService.Identifier);
}).inSingletonScope();
bind(BackendApplicationContribution).toService(MessagingContribution);
});
2 changes: 2 additions & 0 deletions packages/core/src/node/messaging/messaging-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ export interface MessagingService {
ws(path: string, callback: (params: MessagingService.PathParams, socket: ws) => void): void;
}
export namespace MessagingService {
/** Inversify container identifier for the `MessagingService` component. */
export const Identifier = Symbol('MessagingService');
export interface PathParams {
[name: string]: string
}
Expand Down
7 changes: 6 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1098,6 +1098,11 @@
dependencies:
"@types/node" "*"

"@types/cookie@^0.3.3":
version "0.3.3"
resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.3.3.tgz#85bc74ba782fb7aa3a514d11767832b0e3bc6803"
integrity sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow==

"@types/decompress@^4.2.2":
version "4.2.3"
resolved "https://registry.yarnpkg.com/@types/decompress/-/decompress-4.2.3.tgz#98eed48af80001038aa05690b2094915f296fe65"
Expand Down Expand Up @@ -4152,7 +4157,7 @@ cookie-signature@1.0.6:
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw=

cookie@0.4.0:
cookie@0.4.0, cookie@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba"
integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==
Expand Down

0 comments on commit 4994c00

Please sign in to comment.