Skip to content

Commit a307f85

Browse files
committed
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.
1 parent 2880525 commit a307f85

12 files changed

+352
-22
lines changed

dev-packages/application-package/src/application-props.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,8 @@ export namespace ApplicationProps {
8282
config: {
8383
applicationName: 'Eclipse Theia',
8484
defaultTheme: 'dark',
85-
defaultIconTheme: 'none'
85+
defaultIconTheme: 'none',
86+
securityWarnings: true,
8687
}
8788
},
8889
generator: {
@@ -122,6 +123,13 @@ export interface FrontendApplicationConfig extends ApplicationConfig {
122123
*/
123124
readonly applicationName: string;
124125

126+
/**
127+
* Control if security checks will be bound and executed in your application.
128+
*
129+
* Defaults to `true`.
130+
*/
131+
readonly securityWarnings?: boolean
132+
125133
/**
126134
* Electron specific configuration.
127135
*/

packages/mini-browser/src/browser/environment/mini-browser-environment.ts

+22-8
Original file line numberDiff line numberDiff line change
@@ -14,38 +14,52 @@
1414
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
1515
********************************************************************************/
1616

17-
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
1817
import { Endpoint, FrontendApplicationContribution } from '@theia/core/lib/browser';
19-
import { inject, injectable, postConstruct } from '@theia/core/shared/inversify';
20-
import { MiniBrowserEndpoint } from '../../common/mini-browser-endpoint';
18+
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
19+
import { inject, injectable, optional, postConstruct } from '@theia/core/shared/inversify';
2120
import { v4 } from 'uuid';
21+
import { MiniBrowserEndpoint } from '../../common/mini-browser-endpoint';
22+
import { MiniBrowserGuard } from '../mini-browser-guard';
23+
import { MiniBrowserConfiguration } from '../mini-browser-configuration';
2224

2325
/**
24-
* Fetch values from the backend's environment.
26+
* Fetch values from the backend's environment and caches them locally.
27+
* Helps with deploying various mini-browser endpoints.
2528
*/
2629
@injectable()
2730
export class MiniBrowserEnvironment implements FrontendApplicationContribution {
2831

2932
protected _hostPatternPromise: Promise<string>;
30-
protected _hostPattern: string;
33+
34+
@inject(MiniBrowserGuard) @optional()
35+
protected miniBrowserGuard?: MiniBrowserGuard;
36+
37+
@inject(MiniBrowserConfiguration)
38+
protected miniBrowserConfiguration: MiniBrowserConfiguration;
3139

3240
@inject(EnvVariablesServer)
3341
protected readonly environment: EnvVariablesServer;
3442

3543
@postConstruct()
3644
protected postConstruct(): void {
3745
this._hostPatternPromise = this.environment.getValue(MiniBrowserEndpoint.HOST_PATTERN_ENV)
38-
.then(envVar => envVar?.value || MiniBrowserEndpoint.HOST_PATTERN_DEFAULT);
46+
.then(envVar => envVar?.value || MiniBrowserEndpoint.HOST_PATTERN_DEFAULT)
47+
.then(pattern => this.miniBrowserConfiguration.hostPattern = pattern);
48+
if (this.miniBrowserGuard) {
49+
this._hostPatternPromise.then(
50+
pattern => this.miniBrowserGuard!.onSetHostPattern(pattern)
51+
);
52+
}
3953
}
4054

4155
async onStart(): Promise<void> {
42-
this._hostPattern = await this._hostPatternPromise;
56+
await this._hostPatternPromise;
4357
}
4458

4559
getEndpoint(uuid: string, hostname?: string): Endpoint {
4660
return new Endpoint({
4761
path: MiniBrowserEndpoint.PATH,
48-
host: this._hostPattern
62+
host: this.miniBrowserConfiguration.hostPattern!
4963
.replace('{{uuid}}', uuid)
5064
.replace('{{hostname}}', hostname || this.getDefaultHostname()),
5165
});

packages/mini-browser/src/browser/location-mapper-service.ts

+10-3
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,13 @@
1414
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
1515
********************************************************************************/
1616

17-
import { inject, injectable, named } from '@theia/core/shared/inversify';
17+
import { inject, injectable, named, optional } from '@theia/core/shared/inversify';
1818
import URI from '@theia/core/lib/common/uri';
1919
import { Endpoint } from '@theia/core/lib/browser';
2020
import { MaybePromise, Prioritizeable } from '@theia/core/lib/common/types';
2121
import { ContributionProvider } from '@theia/core/lib/common/contribution-provider';
2222
import { MiniBrowserEnvironment } from './environment/mini-browser-environment';
23+
import { MiniBrowserGuard } from './mini-browser-guard';
2324

2425
/**
2526
* Contribution for the `LocationMapperService`.
@@ -128,14 +129,17 @@ export class LocationWithoutSchemeMapper implements LocationMapper {
128129
@injectable()
129130
export class FileLocationMapper implements LocationMapper {
130131

132+
@inject(MiniBrowserGuard) @optional()
133+
protected miniBrowserGuard?: MiniBrowserGuard;
134+
131135
@inject(MiniBrowserEnvironment)
132-
protected readonly miniBrowserEnvironment: MiniBrowserEnvironment;
136+
protected miniBrowserEnvironment: MiniBrowserEnvironment;
133137

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

138-
map(location: string): MaybePromise<string> {
142+
async map(location: string): Promise<string> {
139143
const uri = new URI(location);
140144
if (uri.scheme !== 'file') {
141145
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 {
144148
if (rawLocation.charAt(0) === '/') {
145149
rawLocation = rawLocation.substr(1);
146150
}
151+
if (this.miniBrowserGuard) {
152+
await this.miniBrowserGuard.onFileLocationMap(rawLocation);
153+
}
147154
return this.miniBrowserEnvironment.getRandomEndpoint().getRestUrl().resolve(rawLocation).toString();
148155
}
149156

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/********************************************************************************
2+
* Copyright (C) 2021 Ericsson and others.
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Public License v. 2.0 which is available at
6+
* http://www.eclipse.org/legal/epl-2.0.
7+
*
8+
* This Source Code may also be made available under the following Secondary
9+
* Licenses when the conditions for such availability set forth in the Eclipse
10+
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
11+
* with the GNU Classpath Exception which is available at
12+
* https://www.gnu.org/software/classpath/license.html.
13+
*
14+
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15+
********************************************************************************/
16+
17+
export const MiniBrowserConfiguration = Symbol('MiniBrowserConfiguration');
18+
export interface MiniBrowserConfiguration {
19+
/**
20+
* The host pattern used to serve mini-browser content.
21+
*/
22+
hostPattern?: string
23+
}

packages/mini-browser/src/browser/mini-browser-frontend-module.ts

+19-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import '../../src/browser/style/index.css';
1818

1919
import { ContainerModule } from '@theia/core/shared/inversify';
2020
import URI from '@theia/core/lib/common/uri';
21+
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
22+
import { createPreferenceProxy, PreferenceService, PreferenceContribution } from '@theia/core/lib/browser';
2123
import { OpenHandler } from '@theia/core/lib/browser/opener-service';
2224
import { WidgetFactory } from '@theia/core/lib/browser/widget-manager';
2325
import { bindContributionProvider } from '@theia/core/lib/common/contribution-provider';
@@ -39,8 +41,22 @@ import {
3941
LocationMapper,
4042
LocationWithoutSchemeMapper,
4143
} from './location-mapper-service';
44+
import { MiniBrowserPreferences, MiniBrowserPreferencesSchema } from './mini-browser-preferences';
45+
import { MiniBrowserGuard } from './mini-browser-guard';
46+
import { MiniBrowserConfiguration } from './mini-browser-configuration';
47+
48+
const frontendConfig = FrontendApplicationConfigProvider.get();
4249

4350
export default new ContainerModule(bind => {
51+
bind<MiniBrowserConfiguration>(MiniBrowserConfiguration).toConstantValue({});
52+
bind(PreferenceContribution).toConstantValue({ schema: MiniBrowserPreferencesSchema });
53+
bind(MiniBrowserPreferences).toDynamicValue(
54+
ctx => createPreferenceProxy(ctx.container.get(PreferenceService), MiniBrowserPreferencesSchema)
55+
).inSingletonScope();
56+
57+
if (frontendConfig.securityWarnings) {
58+
bind(MiniBrowserGuard).toSelf().inSingletonScope();
59+
}
4460

4561
bind(MiniBrowserContent).toSelf();
4662
bind(MiniBrowserContentFactory).toFactory(context => (props: MiniBrowserProps) => {
@@ -77,5 +93,7 @@ export default new ContainerModule(bind => {
7793
bind(LocationMapper).toService(LocationWithoutSchemeMapper);
7894
bind(LocationMapperService).toSelf().inSingletonScope();
7995

80-
bind(MiniBrowserService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, MiniBrowserServicePath)).inSingletonScope();
96+
bind(MiniBrowserService).toDynamicValue(
97+
ctx => WebSocketConnectionProvider.createProxy(ctx.container, MiniBrowserServicePath)
98+
).inSingletonScope();
8199
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/********************************************************************************
2+
* Copyright (C) 2021 Ericsson and others.
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Public License v. 2.0 which is available at
6+
* http://www.eclipse.org/legal/epl-2.0.
7+
*
8+
* This Source Code may also be made available under the following Secondary
9+
* Licenses when the conditions for such availability set forth in the Eclipse
10+
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
11+
* with the GNU Classpath Exception which is available at
12+
* https://www.gnu.org/software/classpath/license.html.
13+
*
14+
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15+
********************************************************************************/
16+
17+
import { MessageService } from '@theia/core';
18+
import { PreferenceService, PreferenceScope } from '@theia/core/lib/browser';
19+
import { inject, injectable } from '@theia/core/shared/inversify';
20+
import { MiniBrowserPreferences, IMiniBrowserPreferences } from './mini-browser-preferences';
21+
import { MiniBrowserConfiguration } from './mini-browser-configuration';
22+
23+
/**
24+
* Checks for known security issues with the mini-browser.
25+
* Can be controlled through preferences.
26+
*/
27+
@injectable()
28+
export class MiniBrowserGuard {
29+
30+
@inject(MessageService)
31+
protected messageService: MessageService;
32+
33+
@inject(PreferenceService)
34+
protected preferenceService: PreferenceService;
35+
36+
@inject(MiniBrowserConfiguration)
37+
protected miniBrowserConfiguration: MiniBrowserConfiguration;
38+
39+
@inject(MiniBrowserPreferences)
40+
protected miniBrowserPreferences: MiniBrowserPreferences;
41+
42+
async onSetHostPattern(hostPattern: string): Promise<void> {
43+
if (this.miniBrowserPreferences['mini-browser.warnIfUnsecure']) {
44+
if (this.isHostPatternUnsecure(hostPattern)) {
45+
this.messageService.warn(
46+
'`mini-browser` is currently configured to serve `file:` resources on the same origin as the application, this is known to be unsecure. ' +
47+
`Current pattern: \`${hostPattern}\``,
48+
{ timeout: 5000 },
49+
/* actions: */ 'Ok', 'Don\'t show again',
50+
).then(action => {
51+
if (action === 'Don\'t show again') {
52+
this.setMiniBrowserPreference('mini-browser.warnIfUnsecure', false);
53+
}
54+
});
55+
}
56+
}
57+
}
58+
59+
/**
60+
* Will throw if the location should not be opened, according to the current configurations.
61+
*/
62+
async onFileLocationMap(location: string): Promise<void> {
63+
if (this.isHostPatternUnsecure(this.miniBrowserConfiguration.hostPattern!)) {
64+
if (this.miniBrowserPreferences['mini-browser.previewFile.preventUnsecure'] === 'alwaysPrevent') {
65+
throw this.preventOpeningLocation(location);
66+
}
67+
if (this.miniBrowserPreferences['mini-browser.previewFile.preventUnsecure'] === 'ask') {
68+
await this.askOpenFileUnsecurely(location);
69+
}
70+
}
71+
}
72+
73+
protected isHostPatternUnsecure(hostPattern: string): boolean {
74+
return hostPattern === '{{hostname}}';
75+
}
76+
77+
protected async askOpenFileUnsecurely(location: string): Promise<void> {
78+
const action = await this.messageService.warn(
79+
'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. ' +
80+
`File: \`${location}\``,
81+
/* actions: */ 'Open', 'Always Open', 'Prevent', 'Always Prevent'
82+
);
83+
switch (action) {
84+
case 'Always Prevent':
85+
this.setMiniBrowserPreference('mini-browser.previewFile.preventUnsecure', 'alwaysPrevent');
86+
case 'Prevent':
87+
case undefined:
88+
throw this.preventOpeningLocation(location);
89+
case 'Always Open':
90+
this.setMiniBrowserPreference('mini-browser.previewFile.preventUnsecure', 'alwaysPrevent');
91+
case 'Open':
92+
return;
93+
}
94+
}
95+
96+
protected preventOpeningLocation(location: string): Error {
97+
const message = `Prevented opening ${location}.`;
98+
this.messageService.warn(
99+
`${message} See the \`mini-browser.previewFile.preventUnsecure\` preference to control this behavior.`,
100+
{ timeout: 10_000 },
101+
/* actions: */ 'Ok'
102+
);
103+
return new Error(message);
104+
}
105+
106+
protected setMiniBrowserPreference<K extends keyof IMiniBrowserPreferences>(
107+
preference: K,
108+
value: IMiniBrowserPreferences[K],
109+
scope: PreferenceScope = PreferenceScope.User
110+
): void {
111+
this.preferenceService.set(preference, value, scope);
112+
}
113+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/********************************************************************************
2+
* Copyright (C) 2021 Ericsson and others.
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Public License v. 2.0 which is available at
6+
* http://www.eclipse.org/legal/epl-2.0.
7+
*
8+
* This Source Code may also be made available under the following Secondary
9+
* Licenses when the conditions for such availability set forth in the Eclipse
10+
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
11+
* with the GNU Classpath Exception which is available at
12+
* https://www.gnu.org/software/classpath/license.html.
13+
*
14+
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15+
********************************************************************************/
16+
17+
import { PreferenceSchema, PreferenceProxy } from '@theia/core/lib/browser';
18+
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
19+
20+
const frontendConfig = FrontendApplicationConfigProvider.get();
21+
22+
export const MiniBrowserPreferencesSchema: PreferenceSchema = {
23+
properties: {}
24+
};
25+
26+
if (frontendConfig.securityWarnings) {
27+
MiniBrowserPreferencesSchema.properties['mini-browser.previewFile.preventUnsecure'] = {
28+
scope: 'application',
29+
description: 'What to do when you open a resource with the mini-browser in an unsecure manner.',
30+
enum: [
31+
'ask',
32+
'alwaysOpen',
33+
'alwaysPrevent',
34+
],
35+
default: 'ask'
36+
};
37+
MiniBrowserPreferencesSchema.properties['mini-browser.warnIfUnsecure'] = {
38+
scope: 'application',
39+
type: 'boolean',
40+
description: 'Warns users that the mini-browser is currently deployed unsecurely.',
41+
default: true,
42+
};
43+
}
44+
45+
export interface IMiniBrowserPreferences {
46+
'mini-browser.previewFile.preventUnsecure'?: 'ask' | 'alwaysOpen' | 'alwaysPrevent'
47+
'mini-browser.warnIfUnsecure'?: boolean
48+
}
49+
50+
export const MiniBrowserPreferences = Symbol('GitPreferences');
51+
export type MiniBrowserPreferences = PreferenceProxy<IMiniBrowserPreferences>;

packages/mini-browser/src/electron-browser/environment/electron-mini-browser-environment.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export class ElectronMiniBrowserEnvironment extends MiniBrowserEnvironment {
4040

4141
protected getDefaultHostname(): string {
4242
const query = self.location.search
43-
.substr(1)
43+
.substr(1) // remove leading `?`
4444
.split('&')
4545
.map(entry => entry
4646
.split('=', 2)

packages/plugin-ext/src/main/browser/plugin-ext-frontend-module.ts

+8
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import '../../../src/main/browser/style/index.css';
1919
import '../../../src/main/browser/style/comments.css';
2020

2121
import { ContainerModule } from '@theia/core/shared/inversify';
22+
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
2223
import {
2324
FrontendApplicationContribution, WidgetFactory, bindViewContribution,
2425
ViewContainerIdentifier, ViewContainer, createTreeContainer, TreeImpl, TreeWidget, TreeModelImpl, LabelProviderContribution
@@ -75,6 +76,9 @@ import { CustomEditorWidgetFactory } from '../browser/custom-editors/custom-edit
7576
import { CustomEditorWidget } from './custom-editors/custom-editor-widget';
7677
import { CustomEditorService } from './custom-editors/custom-editor-service';
7778
import { UndoRedoService } from './custom-editors/undo-redo-service';
79+
import { WebviewGuard } from './webview/webview-guard';
80+
81+
const frontendConfig = FrontendApplicationConfigProvider.get();
7882

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

@@ -159,6 +163,10 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
159163
}
160164
})).inSingletonScope();
161165

166+
if (frontendConfig.securityWarnings) {
167+
bind(WebviewGuard).toSelf().inSingletonScope();
168+
}
169+
162170
bindWebviewPreferences(bind);
163171
bind(WebviewEnvironment).toSelf().inSingletonScope();
164172
bind(WebviewThemeDataProvider).toSelf().inSingletonScope();

0 commit comments

Comments
 (0)