From 9a0a9090803b7c8f6343c228bae46c9633beaecd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20Mar=C3=A9chal?= Date: Thu, 13 Feb 2020 15:18:55 -0500 Subject: [PATCH] electron: only allow browser-window MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. Fixes https://bugs.eclipse.org/bugs/show_bug.cgi?id=551747 Signed-off-by: Paul Maréchal --- .../src/generator/frontend-generator.ts | 25 ++++++- packages/core/package.json | 5 ++ .../src/electron-common/electron-token.ts | 26 ++++++++ .../electron-token-backend-contribution.ts | 47 +++++++++++++ .../token/electron-token-backend-module.ts | 37 +++++++++++ .../electron-token-messaging-contribution.ts | 41 ++++++++++++ .../token/electron-token-validator.ts | 66 +++++++++++++++++++ .../messaging/messaging-backend-module.ts | 4 +- .../node/messaging/messaging-contribution.ts | 20 ++++-- .../src/node/messaging/messaging-service.ts | 2 + yarn.lock | 7 +- 11 files changed, 270 insertions(+), 10 deletions(-) create mode 100644 packages/core/src/electron-common/electron-token.ts create mode 100644 packages/core/src/electron-node/token/electron-token-backend-contribution.ts create mode 100644 packages/core/src/electron-node/token/electron-token-backend-module.ts create mode 100644 packages/core/src/electron-node/token/electron-token-messaging-contribution.ts create mode 100644 packages/core/src/electron-node/token/electron-token-validator.ts diff --git a/dev-packages/application-manager/src/generator/frontend-generator.ts b/dev-packages/application-manager/src/generator/frontend-generator.ts index c386866d803f3..6556fa4d3fc88 100644 --- a/dev-packages/application-manager/src/generator/frontend-generator.ts +++ b/dev-packages/application-manager/src/generator/frontend-generator.ts @@ -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)} @@ -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'}; @@ -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) { @@ -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); + } + }); } }; @@ -322,6 +338,7 @@ 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) => { @@ -329,7 +346,9 @@ app.on('ready', () => { 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); }); diff --git a/packages/core/package.json b/packages/core/package.json index fd784c7afe611..4d0016d4574d6 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -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", @@ -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", @@ -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": [ diff --git a/packages/core/src/electron-common/electron-token.ts b/packages/core/src/electron-common/electron-token.ts new file mode 100644 index 0000000000000..4ebde485a14a7 --- /dev/null +++ b/packages/core/src/electron-common/electron-token.ts @@ -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; +}; diff --git a/packages/core/src/electron-node/token/electron-token-backend-contribution.ts b/packages/core/src/electron-node/token/electron-token-backend-contribution.ts new file mode 100644 index 0000000000000..cbfe0c0fea45a --- /dev/null +++ b/packages/core/src/electron-node/token/electron-token-backend-contribution.ts @@ -0,0 +1,47 @@ +/******************************************************************************** + * 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 express = require('express'); +import { injectable, inject } from 'inversify'; +import { BackendApplicationContribution } from '../../node'; +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); + } + } + +} diff --git a/packages/core/src/electron-node/token/electron-token-backend-module.ts b/packages/core/src/electron-node/token/electron-token-backend-module.ts new file mode 100644 index 0000000000000..37d2079aeffd4 --- /dev/null +++ b/packages/core/src/electron-node/token/electron-token-backend-module.ts @@ -0,0 +1,37 @@ +/******************************************************************************** + * 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 { BackendApplicationContribution, MessagingService } from '../../node'; +import { MessagingContribution } from '../../node/messaging/messaging-contribution'; +import { ElectronSecurityToken } from '../../electron-common/electron-token'; +import { ElectronTokenBackendContribution } from './electron-token-backend-contribution'; +import { ElectronMessagingContribution } from './electron-token-messaging-contribution'; +import { ElectronTokenValidator } from './electron-token-validator'; + +export default new ContainerModule((bind, unbind, isBound, rebind) => { + bind>(Promise).toDynamicValue(async () => + JSON.parse(process.env[ElectronSecurityToken]!) + ).inSingletonScope().whenTargetNamed(ElectronSecurityToken); + + bind(ElectronTokenValidator).toSelf().inSingletonScope(); + bind(ElectronTokenBackendContribution).toSelf().inSingletonScope(); + for (const contribution of [ElectronTokenBackendContribution, ElectronTokenValidator]) { + bind(BackendApplicationContribution).toService(contribution); + } + + rebind(MessagingService.Identifier).to(ElectronMessagingContribution).inSingletonScope(); +}); diff --git a/packages/core/src/electron-node/token/electron-token-messaging-contribution.ts b/packages/core/src/electron-node/token/electron-token-messaging-contribution.ts new file mode 100644 index 0000000000000..df013a9660a82 --- /dev/null +++ b/packages/core/src/electron-node/token/electron-token-messaging-contribution.ts @@ -0,0 +1,41 @@ +/******************************************************************************** + * 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 net from 'net'; +import * as http from 'http'; +import { injectable, inject } from 'inversify'; +import { MessagingContribution } from '../../node/messaging/messaging-contribution'; +import { ElectronTokenValidator } from './electron-token-validator'; + +/** + * 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; + + protected handleHttpUpgrade(request: http.IncomingMessage, socket: net.Socket, head: Buffer): void { + if (this.tokenValidator.allowRequest(request)) { + super.handleHttpUpgrade(request, socket, head); + } else { + console.error(`refused a websocket connection: ${request.connection.remoteAddress}`); + socket.destroy(); // kill connection, client will take that as a "no". + } + } + +} diff --git a/packages/core/src/electron-node/token/electron-token-validator.ts b/packages/core/src/electron-node/token/electron-token-validator.ts new file mode 100644 index 0000000000000..c1a4158f91af8 --- /dev/null +++ b/packages/core/src/electron-node/token/electron-token-validator.ts @@ -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` can be undefined while the value is being resolved. + */ + protected electronSecurityToken: ElectronSecurityToken | undefined; + + async onStart(): Promise { + 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; + } + +} diff --git a/packages/core/src/node/messaging/messaging-backend-module.ts b/packages/core/src/node/messaging/messaging-backend-module.ts index e0edd7fd854d8..99ccf7d1fb245 100644 --- a/packages/core/src/node/messaging/messaging-backend-module.ts +++ b/packages/core/src/node/messaging/messaging-backend-module.ts @@ -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); }); diff --git a/packages/core/src/node/messaging/messaging-contribution.ts b/packages/core/src/node/messaging/messaging-contribution.ts index 944778efec06b..ccf04f3ac4b8d 100644 --- a/packages/core/src/node/messaging/messaging-contribution.ts +++ b/packages/core/src/node/messaging/messaging-contribution.ts @@ -16,6 +16,7 @@ import * as ws from 'ws'; import * as url from 'url'; +import * as net from 'net'; import * as http from 'http'; import * as https from 'https'; import { injectable, inject, named, postConstruct, interfaces } from 'inversify'; @@ -46,6 +47,7 @@ export class MessagingContribution implements BackendApplicationContribution, Me @inject(ContributionProvider) @named(MessagingService.Contribution) protected readonly contributions: ContributionProvider; + protected webSocketServer: ws.Server | undefined; protected readonly wsHandlers = new MessagingContribution.ConnectionHandlers(); protected readonly channelHandlers = new MessagingContribution.ConnectionHandlers(); @@ -81,23 +83,24 @@ export class MessagingContribution implements BackendApplicationContribution, Me protected checkAliveTimeout = 30000; onStart(server: http.Server | https.Server): void { - const wss = new ws.Server({ - server, + this.webSocketServer = new ws.Server({ + noServer: true, perMessageDeflate: { // don't compress if a message is less than 256kb threshold: 256 * 1024 } }); + server.on('upgrade', this.handleHttpUpgrade.bind(this)); interface CheckAliveWS extends ws { alive: boolean; } - wss.on('connection', (socket: CheckAliveWS, request) => { + this.webSocketServer.on('connection', (socket: CheckAliveWS, request) => { socket.alive = true; socket.on('pong', () => socket.alive = true); this.handleConnection(socket, request); }); setInterval(() => { - wss.clients.forEach((socket: CheckAliveWS) => { + this.webSocketServer!.clients.forEach((socket: CheckAliveWS) => { if (socket.alive === false) { socket.terminate(); return; @@ -108,6 +111,15 @@ export class MessagingContribution implements BackendApplicationContribution, Me }, this.checkAliveTimeout); } + /** + * Route HTTP upgrade requests to the WebSocket server. + */ + protected handleHttpUpgrade(request: http.IncomingMessage, socket: net.Socket, head: Buffer): void { + this.webSocketServer!.handleUpgrade(request, socket, head, client => { + this.webSocketServer!.emit('connection', client, request); + }); + } + protected handleConnection(socket: ws, request: http.IncomingMessage): void { const pathname = request.url && url.parse(request.url).pathname; if (pathname && !this.wsHandlers.route(pathname, socket)) { diff --git a/packages/core/src/node/messaging/messaging-service.ts b/packages/core/src/node/messaging/messaging-service.ts index 919cc505f576f..68dd461abe828 100644 --- a/packages/core/src/node/messaging/messaging-service.ts +++ b/packages/core/src/node/messaging/messaging-service.ts @@ -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 } diff --git a/yarn.lock b/yarn.lock index 92fc8d6638686..7780ca583e36e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" @@ -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==