From a307f85220c00dcdf455ab73d34169d35265ac87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20Mar=C3=A9chal?= Date: Mon, 7 Jun 2021 13:46:53 -0400 Subject: [PATCH] mini-browser, webview: warn if unsecure Add a new `FrontendApplicationConfiguration` field `securityWarnings` that drives the binding of guards in different modules, as well as adding/removing preferences. When enabled, these modules will do checks for known configuration issues that may cause security vulnerabilities. When disabled, applications will run like they used to, skipping checks. Check for unsecure host patterns when deploying `mini-browser` and `webview` content. `{{hostname}}` is known to cause vulnerabilities in applications, so we currently check for those by default. New preferences: `mini-browser.previewFile.preventUnsecure: 'ask' | 'alwaysOpen' | 'alwaysPrevent'` Theia will prompt the user before loading the local content into the preview iframe. You can either open, prevent, always open, or always prevent. `mini-browser.warnIfUnsecure: boolean` Theia will prompt a warning upon starting the frontend if the configured host pattern is unsecure. `webview.warnIfUnsecure: boolean` Theia will prompt a warning upon starting the frontend if the configured host pattern is unsecure. --- .../src/application-props.ts | 10 +- .../environment/mini-browser-environment.ts | 30 +++-- .../src/browser/location-mapper-service.ts | 13 +- .../src/browser/mini-browser-configuration.ts | 23 ++++ .../browser/mini-browser-frontend-module.ts | 20 +++- .../src/browser/mini-browser-guard.ts | 113 ++++++++++++++++++ .../src/browser/mini-browser-preferences.ts | 51 ++++++++ .../electron-mini-browser-environment.ts | 2 +- .../browser/plugin-ext-frontend-module.ts | 8 ++ .../browser/webview/webview-environment.ts | 9 +- .../src/main/browser/webview/webview-guard.ts | 69 +++++++++++ .../browser/webview/webview-preferences.ts | 26 ++-- 12 files changed, 352 insertions(+), 22 deletions(-) create mode 100644 packages/mini-browser/src/browser/mini-browser-configuration.ts create mode 100644 packages/mini-browser/src/browser/mini-browser-guard.ts create mode 100644 packages/mini-browser/src/browser/mini-browser-preferences.ts create mode 100644 packages/plugin-ext/src/main/browser/webview/webview-guard.ts diff --git a/dev-packages/application-package/src/application-props.ts b/dev-packages/application-package/src/application-props.ts index 1c78feacccb0b..cc8af24c2a64f 100644 --- a/dev-packages/application-package/src/application-props.ts +++ b/dev-packages/application-package/src/application-props.ts @@ -82,7 +82,8 @@ export namespace ApplicationProps { config: { applicationName: 'Eclipse Theia', defaultTheme: 'dark', - defaultIconTheme: 'none' + defaultIconTheme: 'none', + securityWarnings: true, } }, generator: { @@ -122,6 +123,13 @@ export interface FrontendApplicationConfig extends ApplicationConfig { */ readonly applicationName: string; + /** + * Control if security checks will be bound and executed in your application. + * + * Defaults to `true`. + */ + readonly securityWarnings?: boolean + /** * Electron specific configuration. */ diff --git a/packages/mini-browser/src/browser/environment/mini-browser-environment.ts b/packages/mini-browser/src/browser/environment/mini-browser-environment.ts index c22f747a4d792..af08cbbb09981 100644 --- a/packages/mini-browser/src/browser/environment/mini-browser-environment.ts +++ b/packages/mini-browser/src/browser/environment/mini-browser-environment.ts @@ -14,20 +14,28 @@ * 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 { inject, injectable, postConstruct } from '@theia/core/shared/inversify'; -import { MiniBrowserEndpoint } from '../../common/mini-browser-endpoint'; +import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; +import { inject, injectable, optional, postConstruct } from '@theia/core/shared/inversify'; import { v4 } from 'uuid'; +import { MiniBrowserEndpoint } from '../../common/mini-browser-endpoint'; +import { MiniBrowserGuard } from '../mini-browser-guard'; +import { MiniBrowserConfiguration } from '../mini-browser-configuration'; /** - * 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; - protected _hostPattern: string; + + @inject(MiniBrowserGuard) @optional() + protected miniBrowserGuard?: MiniBrowserGuard; + + @inject(MiniBrowserConfiguration) + protected miniBrowserConfiguration: MiniBrowserConfiguration; @inject(EnvVariablesServer) protected readonly environment: EnvVariablesServer; @@ -35,17 +43,23 @@ export class MiniBrowserEnvironment implements FrontendApplicationContribution { @postConstruct() protected postConstruct(): void { this._hostPatternPromise = this.environment.getValue(MiniBrowserEndpoint.HOST_PATTERN_ENV) - .then(envVar => envVar?.value || MiniBrowserEndpoint.HOST_PATTERN_DEFAULT); + .then(envVar => envVar?.value || MiniBrowserEndpoint.HOST_PATTERN_DEFAULT) + .then(pattern => this.miniBrowserConfiguration.hostPattern = pattern); + if (this.miniBrowserGuard) { + this._hostPatternPromise.then( + pattern => this.miniBrowserGuard!.onSetHostPattern(pattern) + ); + } } async onStart(): Promise { - this._hostPattern = await this._hostPatternPromise; + await this._hostPatternPromise; } getEndpoint(uuid: string, hostname?: string): Endpoint { return new Endpoint({ path: MiniBrowserEndpoint.PATH, - host: this._hostPattern + host: this.miniBrowserConfiguration.hostPattern! .replace('{{uuid}}', uuid) .replace('{{hostname}}', hostname || this.getDefaultHostname()), }); diff --git a/packages/mini-browser/src/browser/location-mapper-service.ts b/packages/mini-browser/src/browser/location-mapper-service.ts index a20bdf1c8c8d1..5fd9df4a7db33 100644 --- a/packages/mini-browser/src/browser/location-mapper-service.ts +++ b/packages/mini-browser/src/browser/location-mapper-service.ts @@ -14,12 +14,13 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { inject, injectable, named } from '@theia/core/shared/inversify'; +import { inject, injectable, named, optional } from '@theia/core/shared/inversify'; import URI from '@theia/core/lib/common/uri'; import { Endpoint } from '@theia/core/lib/browser'; import { MaybePromise, Prioritizeable } from '@theia/core/lib/common/types'; import { ContributionProvider } from '@theia/core/lib/common/contribution-provider'; import { MiniBrowserEnvironment } from './environment/mini-browser-environment'; +import { MiniBrowserGuard } from './mini-browser-guard'; /** * Contribution for the `LocationMapperService`. @@ -128,14 +129,17 @@ export class LocationWithoutSchemeMapper implements LocationMapper { @injectable() export class FileLocationMapper implements LocationMapper { + @inject(MiniBrowserGuard) @optional() + protected miniBrowserGuard?: MiniBrowserGuard; + @inject(MiniBrowserEnvironment) - protected readonly miniBrowserEnvironment: MiniBrowserEnvironment; + protected miniBrowserEnvironment: MiniBrowserEnvironment; canHandle(location: string): MaybePromise { return location.startsWith('file://') ? 1 : 0; } - map(location: string): MaybePromise { + async map(location: string): Promise { 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}.`); @@ -144,6 +148,9 @@ export class FileLocationMapper implements LocationMapper { if (rawLocation.charAt(0) === '/') { rawLocation = rawLocation.substr(1); } + if (this.miniBrowserGuard) { + await this.miniBrowserGuard.onFileLocationMap(rawLocation); + } return this.miniBrowserEnvironment.getRandomEndpoint().getRestUrl().resolve(rawLocation).toString(); } diff --git a/packages/mini-browser/src/browser/mini-browser-configuration.ts b/packages/mini-browser/src/browser/mini-browser-configuration.ts new file mode 100644 index 0000000000000..d7c3b60463990 --- /dev/null +++ b/packages/mini-browser/src/browser/mini-browser-configuration.ts @@ -0,0 +1,23 @@ +/******************************************************************************** + * 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 + ********************************************************************************/ + +export const MiniBrowserConfiguration = Symbol('MiniBrowserConfiguration'); +export interface MiniBrowserConfiguration { + /** + * The host pattern used to serve mini-browser content. + */ + hostPattern?: string +} diff --git a/packages/mini-browser/src/browser/mini-browser-frontend-module.ts b/packages/mini-browser/src/browser/mini-browser-frontend-module.ts index f3af38aabcdd6..feeefac4c8dd2 100644 --- a/packages/mini-browser/src/browser/mini-browser-frontend-module.ts +++ b/packages/mini-browser/src/browser/mini-browser-frontend-module.ts @@ -18,6 +18,8 @@ import '../../src/browser/style/index.css'; import { ContainerModule } from '@theia/core/shared/inversify'; import URI from '@theia/core/lib/common/uri'; +import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider'; +import { createPreferenceProxy, PreferenceService, PreferenceContribution } from '@theia/core/lib/browser'; import { OpenHandler } from '@theia/core/lib/browser/opener-service'; import { WidgetFactory } from '@theia/core/lib/browser/widget-manager'; import { bindContributionProvider } from '@theia/core/lib/common/contribution-provider'; @@ -39,8 +41,22 @@ import { LocationMapper, LocationWithoutSchemeMapper, } from './location-mapper-service'; +import { MiniBrowserPreferences, MiniBrowserPreferencesSchema } from './mini-browser-preferences'; +import { MiniBrowserGuard } from './mini-browser-guard'; +import { MiniBrowserConfiguration } from './mini-browser-configuration'; + +const frontendConfig = FrontendApplicationConfigProvider.get(); export default new ContainerModule(bind => { + bind(MiniBrowserConfiguration).toConstantValue({}); + bind(PreferenceContribution).toConstantValue({ schema: MiniBrowserPreferencesSchema }); + bind(MiniBrowserPreferences).toDynamicValue( + ctx => createPreferenceProxy(ctx.container.get(PreferenceService), MiniBrowserPreferencesSchema) + ).inSingletonScope(); + + if (frontendConfig.securityWarnings) { + bind(MiniBrowserGuard).toSelf().inSingletonScope(); + } bind(MiniBrowserContent).toSelf(); bind(MiniBrowserContentFactory).toFactory(context => (props: MiniBrowserProps) => { @@ -77,5 +93,7 @@ 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(); }); diff --git a/packages/mini-browser/src/browser/mini-browser-guard.ts b/packages/mini-browser/src/browser/mini-browser-guard.ts new file mode 100644 index 0000000000000..7d4022f357383 --- /dev/null +++ b/packages/mini-browser/src/browser/mini-browser-guard.ts @@ -0,0 +1,113 @@ +/******************************************************************************** + * 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 { PreferenceService, PreferenceScope } from '@theia/core/lib/browser'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { MiniBrowserPreferences, IMiniBrowserPreferences } from './mini-browser-preferences'; +import { MiniBrowserConfiguration } from './mini-browser-configuration'; + +/** + * Checks for known security issues with the mini-browser. + * Can be controlled through preferences. + */ +@injectable() +export class MiniBrowserGuard { + + @inject(MessageService) + protected messageService: MessageService; + + @inject(PreferenceService) + protected preferenceService: PreferenceService; + + @inject(MiniBrowserConfiguration) + protected miniBrowserConfiguration: MiniBrowserConfiguration; + + @inject(MiniBrowserPreferences) + protected miniBrowserPreferences: MiniBrowserPreferences; + + async onSetHostPattern(hostPattern: string): Promise { + if (this.miniBrowserPreferences['mini-browser.warnIfUnsecure']) { + if (this.isHostPatternUnsecure(hostPattern)) { + this.messageService.warn( + '`mini-browser` is currently configured to serve `file:` resources on the same origin as the application, this is known to be unsecure. ' + + `Current pattern: \`${hostPattern}\``, + { timeout: 5000 }, + /* actions: */ 'Ok', 'Don\'t show again', + ).then(action => { + if (action === 'Don\'t show again') { + this.setMiniBrowserPreference('mini-browser.warnIfUnsecure', false); + } + }); + } + } + } + + /** + * Will throw if the location should not be opened, according to the current configurations. + */ + async onFileLocationMap(location: string): Promise { + if (this.isHostPatternUnsecure(this.miniBrowserConfiguration.hostPattern!)) { + if (this.miniBrowserPreferences['mini-browser.previewFile.preventUnsecure'] === 'alwaysPrevent') { + throw this.preventOpeningLocation(location); + } + if (this.miniBrowserPreferences['mini-browser.previewFile.preventUnsecure'] === 'ask') { + await this.askOpenFileUnsecurely(location); + } + } + } + + protected isHostPatternUnsecure(hostPattern: string): boolean { + return hostPattern === '{{hostname}}'; + } + + protected async askOpenFileUnsecurely(location: string): Promise { + const action = await this.messageService.warn( + 'You are about to open a local file with the same origin as this application, this unsecure and the displayed document might access this application services. ' + + `File: \`${location}\``, + /* actions: */ 'Open', 'Always Open', 'Prevent', 'Always Prevent' + ); + switch (action) { + case 'Always Prevent': + this.setMiniBrowserPreference('mini-browser.previewFile.preventUnsecure', 'alwaysPrevent'); + case 'Prevent': + case undefined: + throw this.preventOpeningLocation(location); + case 'Always Open': + this.setMiniBrowserPreference('mini-browser.previewFile.preventUnsecure', 'alwaysPrevent'); + case 'Open': + return; + } + } + + protected preventOpeningLocation(location: string): Error { + const message = `Prevented opening ${location}.`; + this.messageService.warn( + `${message} See the \`mini-browser.previewFile.preventUnsecure\` preference to control this behavior.`, + { timeout: 10_000 }, + /* actions: */ 'Ok' + ); + return new Error(message); + } + + protected setMiniBrowserPreference( + preference: K, + value: IMiniBrowserPreferences[K], + scope: PreferenceScope = PreferenceScope.User + ): void { + this.preferenceService.set(preference, value, scope); + } +} diff --git a/packages/mini-browser/src/browser/mini-browser-preferences.ts b/packages/mini-browser/src/browser/mini-browser-preferences.ts new file mode 100644 index 0000000000000..93b792bf35d3d --- /dev/null +++ b/packages/mini-browser/src/browser/mini-browser-preferences.ts @@ -0,0 +1,51 @@ +/******************************************************************************** + * 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 { PreferenceSchema, PreferenceProxy } from '@theia/core/lib/browser'; +import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider'; + +const frontendConfig = FrontendApplicationConfigProvider.get(); + +export const MiniBrowserPreferencesSchema: PreferenceSchema = { + properties: {} +}; + +if (frontendConfig.securityWarnings) { + MiniBrowserPreferencesSchema.properties['mini-browser.previewFile.preventUnsecure'] = { + scope: 'application', + description: 'What to do when you open a resource with the mini-browser in an unsecure manner.', + enum: [ + 'ask', + 'alwaysOpen', + 'alwaysPrevent', + ], + default: 'ask' + }; + MiniBrowserPreferencesSchema.properties['mini-browser.warnIfUnsecure'] = { + scope: 'application', + type: 'boolean', + description: 'Warns users that the mini-browser is currently deployed unsecurely.', + default: true, + }; +} + +export interface IMiniBrowserPreferences { + 'mini-browser.previewFile.preventUnsecure'?: 'ask' | 'alwaysOpen' | 'alwaysPrevent' + 'mini-browser.warnIfUnsecure'?: boolean +} + +export const MiniBrowserPreferences = Symbol('GitPreferences'); +export type MiniBrowserPreferences = PreferenceProxy; diff --git a/packages/mini-browser/src/electron-browser/environment/electron-mini-browser-environment.ts b/packages/mini-browser/src/electron-browser/environment/electron-mini-browser-environment.ts index a4c5eb5fc854b..7fa7760dd5316 100644 --- a/packages/mini-browser/src/electron-browser/environment/electron-mini-browser-environment.ts +++ b/packages/mini-browser/src/electron-browser/environment/electron-mini-browser-environment.ts @@ -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) diff --git a/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts b/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts index 2082841d0c160..ed9be05f86f5b 100644 --- a/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts +++ b/packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts @@ -19,6 +19,7 @@ import '../../../src/main/browser/style/index.css'; import '../../../src/main/browser/style/comments.css'; import { ContainerModule } from '@theia/core/shared/inversify'; +import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider'; import { FrontendApplicationContribution, WidgetFactory, bindViewContribution, ViewContainerIdentifier, ViewContainer, createTreeContainer, TreeImpl, TreeWidget, TreeModelImpl, LabelProviderContribution @@ -75,6 +76,9 @@ 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 { WebviewGuard } from './webview/webview-guard'; + +const frontendConfig = FrontendApplicationConfigProvider.get(); export default new ContainerModule((bind, unbind, isBound, rebind) => { @@ -159,6 +163,10 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { } })).inSingletonScope(); + if (frontendConfig.securityWarnings) { + bind(WebviewGuard).toSelf().inSingletonScope(); + } + bindWebviewPreferences(bind); bind(WebviewEnvironment).toSelf().inSingletonScope(); bind(WebviewThemeDataProvider).toSelf().inSingletonScope(); diff --git a/packages/plugin-ext/src/main/browser/webview/webview-environment.ts b/packages/plugin-ext/src/main/browser/webview/webview-environment.ts index e56175c295cdc..04dd996ada7eb 100644 --- a/packages/plugin-ext/src/main/browser/webview/webview-environment.ts +++ b/packages/plugin-ext/src/main/browser/webview/webview-environment.ts @@ -14,17 +14,21 @@ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 ********************************************************************************/ -import { injectable, inject, postConstruct } from '@theia/core/shared/inversify'; +import { injectable, inject, postConstruct, optional } from '@theia/core/shared/inversify'; import { Endpoint } from '@theia/core/lib/browser/endpoint'; import { Deferred } from '@theia/core/lib/common/promise-util'; import { EnvVariablesServer } from '@theia/core/lib/common/env-variables'; import URI from '@theia/core/lib/common/uri'; import { WebviewExternalEndpoint } from '../../common/webview-protocol'; import { environment } from '@theia/core/shared/@theia/application-package/lib/environment'; +import { WebviewGuard } from './webview-guard'; @injectable() export class WebviewEnvironment { + @inject(WebviewGuard) @optional() + protected webviewGuard?: WebviewGuard; + @inject(EnvVariablesServer) protected readonly environments: EnvVariablesServer; @@ -41,6 +45,9 @@ export class WebviewEnvironment { endpointPattern = variable && variable.value || WebviewExternalEndpoint.defaultPattern; } const { host } = new Endpoint(); + if (this.webviewGuard) { + await this.webviewGuard.onSetHostPattern(endpointPattern); + } this.externalEndpointHost.resolve(endpointPattern.replace('{{hostname}}', host)); } catch (e) { this.externalEndpointHost.reject(e); diff --git a/packages/plugin-ext/src/main/browser/webview/webview-guard.ts b/packages/plugin-ext/src/main/browser/webview/webview-guard.ts new file mode 100644 index 0000000000000..2dae89df29554 --- /dev/null +++ b/packages/plugin-ext/src/main/browser/webview/webview-guard.ts @@ -0,0 +1,69 @@ +/******************************************************************************** + * 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 { PreferenceScope, PreferenceService } from '@theia/core/lib/browser'; +import { MaybePromise } from '@theia/core/lib/common'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { WebviewConfiguration, WebviewPreferences } from './webview-preferences'; + +/** + * Checks for known security issues with webviews. + * Can be controlled through preferences. + * + * You can unbind this component from your application to remove the checks. + */ +@injectable() +export class WebviewGuard { + + @inject(MessageService) + protected messageService: MessageService; + + @inject(PreferenceService) + protected preferenceService: PreferenceService; + + @inject(WebviewPreferences) + protected webviewPreferences: WebviewPreferences; + + onSetHostPattern(hostPattern: string): MaybePromise { + if (this.webviewPreferences['webview.warnIfUnsecure']) { + if (this.isHostPatternUnsecure(hostPattern)) { + this.messageService.warn( + 'Webviews are currently configured to serve on the same origin as the application, this is known to be unsecure. ' + + `Current pattern: \`${hostPattern}\``, + { timeout: 5000 }, + /* actions: */ 'Ok', 'Don\'t show again', + ).then(action => { + if (action === 'Don\'t show again') { + this.setWebviewPreference('webview.warnIfUnsecure', false); + } + }); + } + } + } + + protected isHostPatternUnsecure(hostPattern: string): boolean { + return hostPattern === '{{hostname}}'; + } + + protected setWebviewPreference( + preference: K, + value: WebviewConfiguration[K], + scope: PreferenceScope = PreferenceScope.User + ): void { + this.preferenceService.set(preference, value, scope); + } +} diff --git a/packages/plugin-ext/src/main/browser/webview/webview-preferences.ts b/packages/plugin-ext/src/main/browser/webview/webview-preferences.ts index c15689d35da73..16a23d44e7a58 100644 --- a/packages/plugin-ext/src/main/browser/webview/webview-preferences.ts +++ b/packages/plugin-ext/src/main/browser/webview/webview-preferences.ts @@ -15,6 +15,7 @@ ********************************************************************************/ import { interfaces } from '@theia/core/shared/inversify'; +import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider'; import { createPreferenceProxy, PreferenceProxy, @@ -23,20 +24,32 @@ import { PreferenceSchema } from '@theia/core/lib/browser/preferences'; +const frontendConfig = FrontendApplicationConfigProvider.get(); + export const WebviewConfigSchema: PreferenceSchema = { - 'type': 'object', - 'properties': { + type: 'object', + properties: { 'webview.trace': { - 'type': 'string', - 'enum': ['off', 'on', 'verbose'], - 'description': 'Controls communication tracing with webviews.', - 'default': 'off' + type: 'string', + enum: ['off', 'on', 'verbose'], + description: 'Controls communication tracing with webviews.', + default: 'off' } } }; +if (frontendConfig.securityWarnings) { + WebviewConfigSchema.properties['webview.warnIfUnsecure'] = { + scope: 'application', + type: 'boolean', + description: 'Warns users that webviews are currently deployed unsecurely.', + default: true, + }; +} + export interface WebviewConfiguration { 'webview.trace': 'off' | 'on' | 'verbose' + 'webview.warnIfUnsecure'?: boolean } export const WebviewPreferences = Symbol('WebviewPreferences'); @@ -51,6 +64,5 @@ export function bindWebviewPreferences(bind: interfaces.Bind): void { const preferences = ctx.container.get(PreferenceService); return createWebviewPreferences(preferences); }); - bind(PreferenceContribution).toConstantValue({ schema: WebviewConfigSchema }); }