Skip to content

Commit

Permalink
mini-browser,webview: warn if unsecure
Browse files Browse the repository at this point in the history
Add security warnings to the mini-browser and webviews when modifying
the host patterns. You can disable those warnings by setting
`warnOnPotentiallyInsecureHostPattern: false` in your application's
`package.json` file, as frontend/backend configurations.
  • Loading branch information
paul-marechal committed Jun 17, 2021
1 parent c92e822 commit a073736
Show file tree
Hide file tree
Showing 15 changed files with 307 additions and 33 deletions.
16 changes: 12 additions & 4 deletions packages/mini-browser/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,20 @@

The `@theia/mini-browser` extension provides a browser widget with the corresponding backend endpoints.

### Environment Variables
## Environment Variables

- `THEIA_MINI_BROWSER_HOST_PATTERN`
- A string pattern possibly containing `{{hostname}}` which will be replaced. This is the host for which the `mini-browser` will serve.
- It is a good practice to host the `mini-browser` handlers on a sub-domain as it is more secure.
- Defaults to `{{uuid}}.mini-browser.{{hostname}}`.

A string pattern possibly containing `{{uuid}}` and `{{hostname}}` which will be replaced. This is the host for which the `mini-browser` will serve.
It is a good practice to host the `mini-browser` handlers on a sub-domain as it is more secure.
Defaults to `{{uuid}}.mini-browser.{{hostname}}`.

## Security Warnings

- Potentially Insecure Host Pattern

When you change the host pattern via `THEIA_MINI_BROWSER_HOST_PATTERN` a warning will be emitted both from the frontend and from the backend.
You can disable those warnings by setting `warnOnPotentiallyInsecureHostPattern: false` in the appropriate application configurations in your application's `package.json`.

## Additional Information

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,35 +14,51 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
import { Endpoint, FrontendApplicationContribution } from '@theia/core/lib/browser';
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
import { environment } from '@theia/core/shared/@theia/application-package/lib/environment';
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
import { MiniBrowserEndpoint } from '../../common/mini-browser-endpoint';
import { v4 } from 'uuid';
import { MiniBrowserEndpoint } from '../../common/mini-browser-endpoint';

/**
* Fetch values from the backend's environment.
* Fetch values from the backend's environment and caches them locally.
* Helps with deploying various mini-browser endpoints.
*/
@injectable()
export class MiniBrowserEnvironment implements FrontendApplicationContribution {

protected _hostPatternPromise: Promise<string>;
protected _hostPattern: string;
protected _hostPattern?: string;

@inject(EnvVariablesServer)
protected readonly environment: EnvVariablesServer;
protected environment: EnvVariablesServer;

@postConstruct()
protected postConstruct(): void {
this._hostPatternPromise = this.environment.getValue(MiniBrowserEndpoint.HOST_PATTERN_ENV)
.then(envVar => envVar?.value || MiniBrowserEndpoint.HOST_PATTERN_DEFAULT);
this._hostPatternPromise = this.getHostPattern()
.then(pattern => this._hostPattern = pattern);
}

get hostPatternPromise(): Promise<string> {
return this._hostPatternPromise;
}

get hostPattern(): string | undefined {
return this._hostPattern;
}

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

/**
* Throws if `hostPatternPromise` is not yet resolved.
*/
getEndpoint(uuid: string, hostname?: string): Endpoint {
if (this._hostPattern === undefined) {
throw new Error('MiniBrowserEnvironment is not finished initializing');
}
return new Endpoint({
path: MiniBrowserEndpoint.PATH,
host: this._hostPattern
Expand All @@ -51,10 +67,20 @@ export class MiniBrowserEnvironment implements FrontendApplicationContribution {
});
}

/**
* Throws if `hostPatternPromise` is not yet resolved.
*/
getRandomEndpoint(): Endpoint {
return this.getEndpoint(v4());
}

protected async getHostPattern(): Promise<string> {
return environment.electron.is()
? MiniBrowserEndpoint.HOST_PATTERN_DEFAULT
: this.environment.getValue(MiniBrowserEndpoint.HOST_PATTERN_ENV)
.then(envVar => envVar?.value || MiniBrowserEndpoint.HOST_PATTERN_DEFAULT);
}

protected getDefaultHostname(): string {
return self.location.host;
}
Expand Down
4 changes: 2 additions & 2 deletions packages/mini-browser/src/browser/location-mapper-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,13 +129,13 @@ export class LocationWithoutSchemeMapper implements LocationMapper {
export class FileLocationMapper implements LocationMapper {

@inject(MiniBrowserEnvironment)
protected readonly miniBrowserEnvironment: MiniBrowserEnvironment;
protected miniBrowserEnvironment: MiniBrowserEnvironment;

canHandle(location: string): MaybePromise<number> {
return location.startsWith('file://') ? 1 : 0;
}

map(location: string): MaybePromise<string> {
async map(location: string): Promise<string> {
const uri = new URI(location);
if (uri.scheme !== 'file') {
throw new Error(`Only URIs with 'file' scheme can be mapped to an URL. URI was: ${uri}.`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ import {
LocationMapper,
LocationWithoutSchemeMapper,
} from './location-mapper-service';
import { MiniBrowserFrontendSecurityWarnings } from './mini-browser-frontend-security-warnings';

export default new ContainerModule(bind => {

bind(MiniBrowserContent).toSelf();
bind(MiniBrowserContentFactory).toFactory(context => (props: MiniBrowserProps) => {
const { container } = context;
Expand Down Expand Up @@ -77,5 +77,10 @@ export default new ContainerModule(bind => {
bind(LocationMapper).toService(LocationWithoutSchemeMapper);
bind(LocationMapperService).toSelf().inSingletonScope();

bind(MiniBrowserService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, MiniBrowserServicePath)).inSingletonScope();
bind(MiniBrowserService).toDynamicValue(
ctx => WebSocketConnectionProvider.createProxy(ctx.container, MiniBrowserServicePath)
).inSingletonScope();

bind(MiniBrowserFrontendSecurityWarnings).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(MiniBrowserFrontendSecurityWarnings);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/********************************************************************************
* Copyright (C) 2021 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 { MessageService } from '@theia/core';
import { FrontendApplicationContribution } from '@theia/core/lib/browser';
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
import { inject, injectable } from '@theia/core/shared/inversify';
import { MiniBrowserEnvironment } from './environment/mini-browser-environment';
import { MiniBrowserEndpoint } from '../common/mini-browser-endpoint';

@injectable()
export class MiniBrowserFrontendSecurityWarnings implements FrontendApplicationContribution {

@inject(MessageService)
protected messageService: MessageService;

@inject(MiniBrowserEnvironment)
protected miniBrowserEnvironment: MiniBrowserEnvironment;

initialize(): void {
this.checkHostPattern();
}

protected async checkHostPattern(): Promise<void> {
if (FrontendApplicationConfigProvider.get()['warnOnPotentiallyInsecureHostPattern'] === false) {
return;
}
const hostPattern = await this.miniBrowserEnvironment.hostPatternPromise;
if (hostPattern !== MiniBrowserEndpoint.HOST_PATTERN_DEFAULT) {
this.messageService.warn(`\
The mini-browser endpoint's host pattern has been changed to \`${hostPattern}\`, changing this pattern can lead to security vulnerabilities. \
See \`@theia/mini-browser/README.md\` for more information.`
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export class ElectronMiniBrowserEnvironment extends MiniBrowserEnvironment {

protected getDefaultHostname(): string {
const query = self.location.search
.substr(1)
.substr(1) // remove leading `?`
.split('&')
.map(entry => entry
.split('=', 2)
Expand Down
3 changes: 3 additions & 0 deletions packages/mini-browser/src/node/mini-browser-backend-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { MiniBrowserService, MiniBrowserServicePath } from '../common/mini-brows
import { MiniBrowserEndpoint, MiniBrowserEndpointHandler, HtmlHandler, ImageHandler, PdfHandler, SvgHandler } from './mini-browser-endpoint';
import { WsRequestValidatorContribution } from '@theia/core/lib/node/ws-request-validators';
import { MiniBrowserWsRequestValidator } from './mini-browser-ws-validator';
import { MiniBrowserBackendSecurityWarnings } from './mini-browser-backend-security-warnings';

export default new ContainerModule(bind => {
bind(MiniBrowserEndpoint).toSelf().inSingletonScope();
Expand All @@ -35,4 +36,6 @@ export default new ContainerModule(bind => {
bind(MiniBrowserEndpointHandler).to(ImageHandler).inSingletonScope();
bind(MiniBrowserEndpointHandler).to(PdfHandler).inSingletonScope();
bind(MiniBrowserEndpointHandler).to(SvgHandler).inSingletonScope();
bind(MiniBrowserBackendSecurityWarnings).toSelf().inSingletonScope();
bind(BackendApplicationContribution).toService(MiniBrowserBackendSecurityWarnings);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/********************************************************************************
* Copyright (C) 2021 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 { BackendApplicationContribution } from '@theia/core/lib/node';
import { BackendApplicationConfigProvider } from '@theia/core/lib/node/backend-application-config-provider';
import { injectable } from '@theia/core/shared/inversify';
import { MiniBrowserEndpoint } from '../common/mini-browser-endpoint';

@injectable()
export class MiniBrowserBackendSecurityWarnings implements BackendApplicationContribution {

initialize(): void {
this.checkHostPattern();
}

protected async checkHostPattern(): Promise<void> {
if (BackendApplicationConfigProvider.get()['warnOnPotentiallyInsecureHostPattern'] === false) {
return;
}
const envHostPattern = process.env[MiniBrowserEndpoint.HOST_PATTERN_ENV];
if (envHostPattern && envHostPattern !== MiniBrowserEndpoint.HOST_PATTERN_DEFAULT) {
console.warn(`\
MINI BROWSER SECURITY WARNING
Changing the @theia/mini-browser host pattern can lead to security vulnerabilities.
Current pattern: "${envHostPattern}"
Please read @theia/mini-browser/README.md for more information.
`
);
}
}
}
18 changes: 18 additions & 0 deletions packages/plugin-ext/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,24 @@

The `@theia/plugin-ext` extension contributes functionality for the `plugin` API.

## Implementation

The implementation is inspired from: https://blog.mattbierner.com/vscode-webview-web-learnings/.

## Environment Variables

- `THEIA_WEBVIEW_ENDPOINT_PATTERN`

A string pattern possibly containing `{{uuid}}` and `{{hostname}}` which will be replaced. This is the host for which the `webviews` will be served on.
It is a good practice to host the `webview` handlers on a sub-domain as it is more secure.
Defaults to `{{uuid}}.webview.{{hostname}}`.

## Security Warnings

- Potentially Insecure Host Pattern

When you change the host pattern via `THEIA_WEBVIEW_ENDPOINT_PATTERN` a warning will be emitted both from the frontend and from the backend.
You can disable those warnings by setting `warnOnPotentiallyInsecureHostPattern: false` in the appropriate application configurations in your application's `package.json`.

## Additional Information

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ import { CustomEditorWidgetFactory } from '../browser/custom-editors/custom-edit
import { CustomEditorWidget } from './custom-editors/custom-editor-widget';
import { CustomEditorService } from './custom-editors/custom-editor-service';
import { UndoRedoService } from './custom-editors/undo-redo-service';
import { WebviewFrontendSecurityWarnings } from './webview/webview-frontend-security-warnings';

export default new ContainerModule((bind, unbind, isBound, rebind) => {

Expand Down Expand Up @@ -226,4 +227,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(CommentingRangeDecorator).toSelf().inSingletonScope();
bind(CommentsContribution).toSelf().inSingletonScope();
bind(CommentsContextKeyService).toSelf().inSingletonScope();

bind(WebviewFrontendSecurityWarnings).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(WebviewFrontendSecurityWarnings);
});
Original file line number Diff line number Diff line change
Expand Up @@ -25,28 +25,29 @@ import { environment } from '@theia/core/shared/@theia/application-package/lib/e
@injectable()
export class WebviewEnvironment {

@inject(EnvVariablesServer)
protected readonly environments: EnvVariablesServer;
protected _hostPatternPromise: Promise<string>;

protected readonly externalEndpointHost = new Deferred<string>();

@inject(EnvVariablesServer)
protected readonly environments: EnvVariablesServer;

@postConstruct()
protected async init(): Promise<void> {
this._hostPatternPromise = this.getHostPattern();
try {
let endpointPattern;
if (environment.electron.is()) {
endpointPattern = WebviewExternalEndpoint.defaultPattern;
} else {
const variable = await this.environments.getValue(WebviewExternalEndpoint.pattern);
endpointPattern = variable && variable.value || WebviewExternalEndpoint.defaultPattern;
}
const endpointPattern = await this.hostPatternPromise;
const { host } = new Endpoint();
this.externalEndpointHost.resolve(endpointPattern.replace('{{hostname}}', host));
} catch (e) {
this.externalEndpointHost.reject(e);
}
}

get hostPatternPromise(): Promise<string> {
return this._hostPatternPromise;
}

async externalEndpointUrl(): Promise<URI> {
const host = await this.externalEndpointHost.promise;
return new Endpoint({
Expand All @@ -67,4 +68,10 @@ export class WebviewEnvironment {
return (await this.externalEndpointUrl()).withPath('').withQuery('').withFragment('').toString(true).replace('{{uuid}}', '*');
}

protected async getHostPattern(): Promise<string> {
return environment.electron.is()
? WebviewExternalEndpoint.defaultPattern
: this.environments.getValue(WebviewExternalEndpoint.pattern)
.then(variable => variable?.value || WebviewExternalEndpoint.defaultPattern);
}
}
Loading

0 comments on commit a073736

Please sign in to comment.