Allow loading webview outside of file: origin (#41698)

* Allow loading webview outside of file: origin

**Problem**
Webviews are currently always loaded from a file on the disk. This results in the webview running in the file origin, potentially allowing it to access any file on disk. If a webview fails to sanitize workspace or remote input, untrusted code could potentially access files on the user's system.

**Fix**
Add a new option to serve the webview out of a "data:" uri instead. This prevents access to `file://` resources.

In order to allow webviews to still load resources from disk, add a new protocol called `vscode-core-resource://` that only allows access to resources inside of the vscode directory.

Moves extension pages and our release notes to use this new option. These already are pretty locked down. We cannot move the htmlpreview command to use this option as it would break a huge number of existing extensions, however the new webview API will always have this new option enabled.

* Shorted protocol name
This commit is contained in:
Matt Bierner
2018-01-16 12:50:14 -08:00
committed by GitHub
parent 42de6cc82f
commit cbf9ae23ed
6 changed files with 95 additions and 36 deletions
+10 -2
View File
@@ -126,8 +126,16 @@ export class CodeApplication {
}
});
const isValidWebviewSource = (source: string) =>
!source || (URI.parse(source.toLowerCase()).toString() as any).startsWith(URI.file(this.environmentService.appRoot.toLowerCase()).toString());
const isValidWebviewSource = (source: string): boolean => {
if (!source) {
return false;
}
if (source === 'data:text/html;charset=utf-8,%3C%21DOCTYPE%20html%3E%0D%0A%3Chtml%20lang%3D%22en%22%20style%3D%22width%3A%20100%25%3B%20height%3A%20100%25%22%3E%0D%0A%3Chead%3E%0D%0A%09%3Ctitle%3EVirtual%20Document%3C%2Ftitle%3E%0D%0A%3C%2Fhead%3E%0D%0A%3Cbody%20style%3D%22margin%3A%200%3B%20overflow%3A%20hidden%3B%20width%3A%20100%25%3B%20height%3A%20100%25%22%3E%0D%0A%3C%2Fbody%3E%0D%0A%3C%2Fhtml%3E') {
return true;
}
const srcUri: any = URI.parse(source.toLowerCase()).toString();
return srcUri.startsWith(URI.file(this.environmentService.appRoot.toLowerCase()).toString());
};
app.on('web-contents-created', (_event: any, contents) => {
contents.on('will-attach-webview', (event: Electron.Event, webPreferences, params) => {
@@ -52,6 +52,7 @@ import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/edi
import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { Color } from 'vs/base/common/color';
import { WorkbenchTree, IListService } from 'vs/platform/list/browser/listService';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
/** A context key that is set when an extension editor webview has focus. */
export const KEYBINDING_CONTEXT_EXTENSIONEDITOR_WEBVIEW_FOCUS = new RawContextKey<boolean>('extensionEditorWebviewFocus', undefined);
@@ -62,13 +63,17 @@ export const KEYBINDING_CONTEXT_EXTENSIONEDITOR_FIND_WIDGET_INPUT_FOCUSED = new
/** A context key that is set when the find widget find input in extension editor webview is not focused. */
export const KEYBINDING_CONTEXT_EXTENSIONEDITOR_FIND_WIDGET_INPUT_NOT_FOCUSED: ContextKeyExpr = KEYBINDING_CONTEXT_EXTENSIONEDITOR_FIND_WIDGET_INPUT_FOCUSED.toNegated();
function renderBody(body: string): string {
function renderBody(
body: string,
environmentService: IEnvironmentService
): string {
const styleSheetPath = require.toUrl('./media/markdown.css').replace('file://' + environmentService.appRoot, 'vscode-core-resource://');
return `<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src https: data:; media-src https:; script-src 'none'; style-src file:; child-src 'none'; frame-src 'none';">
<link rel="stylesheet" type="text/css" href="${require.toUrl('./media/markdown.css')}">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src https: data:; media-src https:; script-src 'none'; style-src vscode-core-resource:; child-src 'none'; frame-src 'none';">
<link rel="stylesheet" type="text/css" href="${styleSheetPath}">
</head>
<body>
<a id="scroll-to-top" role="button" aria-label="scroll to top" href="#"><span class="icon"></span></a>
@@ -194,7 +199,9 @@ export class ExtensionEditor extends BaseEditor {
@IPartService private partService: IPartService,
@IContextViewService private contextViewService: IContextViewService,
@IContextKeyService private contextKeyService: IContextKeyService,
@IExtensionTipsService private extensionTipsService: IExtensionTipsService
@IExtensionTipsService private extensionTipsService: IExtensionTipsService,
@IEnvironmentService private environmentService: IEnvironmentService
) {
super(ExtensionEditor.ID, telemetryService, themeService);
this.disposables = [];
@@ -408,12 +415,12 @@ export class ExtensionEditor extends BaseEditor {
private openMarkdown(content: TPromise<string>, noContentCopy: string) {
return this.loadContents(() => content
.then(marked.parse)
.then(renderBody)
.then(content => renderBody(content, this.environmentService))
.then(removeEmbeddedSVGs)
.then<void>(body => {
const allowedBadgeProviders = this.extensionsWorkbenchService.allowedBadgeProviders;
const webViewOptions = allowedBadgeProviders.length > 0 ? { allowScripts: false, allowSvgs: false, svgWhiteList: allowedBadgeProviders } : undefined;
this.activeWebview = new WebView(this.content, this.partService.getContainer(Parts.EDITOR_PART), this.contextViewService, this.contextKey, this.findInputFocusContextKey, webViewOptions);
const webViewOptions = allowedBadgeProviders.length > 0 ? { allowScripts: false, allowSvgs: false, svgWhiteList: allowedBadgeProviders } : {};
this.activeWebview = new WebView(this.content, this.partService.getContainer(Parts.EDITOR_PART), this.environmentService, this.contextViewService, this.contextKey, this.findInputFocusContextKey, webViewOptions, false);
const removeLayoutParticipant = arrays.insert(this.layoutParticipants, this.activeWebview);
this.contentDisposables.push(toDisposable(removeLayoutParticipant));
@@ -25,6 +25,7 @@ import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import Webview, { WebviewOptions } from './webview';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { WebviewEditor } from './webviewEditor';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
/**
@@ -46,13 +47,14 @@ export class HtmlPreviewPart extends WebviewEditor {
constructor(
@ITelemetryService telemetryService: ITelemetryService,
@ITextModelService private textModelResolverService: ITextModelService,
@IThemeService themeService: IThemeService,
@IOpenerService private readonly openerService: IOpenerService,
@IPartService private partService: IPartService,
@IStorageService storageService: IStorageService,
@IContextViewService private _contextViewService: IContextViewService,
@IContextKeyService contextKeyService: IContextKeyService
@IContextKeyService contextKeyService: IContextKeyService,
@ITextModelService private readonly textModelResolverService: ITextModelService,
@IOpenerService private readonly openerService: IOpenerService,
@IPartService private readonly partService: IPartService,
@IContextViewService private readonly _contextViewService: IContextViewService,
@IEnvironmentService private readonly _environmentService: IEnvironmentService
) {
super(HtmlPreviewPart.ID, telemetryService, themeService, storageService, contextKeyService);
}
@@ -84,7 +86,7 @@ export class HtmlPreviewPart extends WebviewEditor {
webviewOptions = this.input.options;
}
this._webview = new Webview(this.content, this.partService.getContainer(Parts.EDITOR_PART), this._contextViewService, this.contextKey, this.findInputFocusContextKey, webviewOptions);
this._webview = new Webview(this.content, this.partService.getContainer(Parts.EDITOR_PART), this._environmentService, this._contextViewService, this.contextKey, this.findInputFocusContextKey, webviewOptions, true);
if (this.input && this.input instanceof HtmlInput) {
const state = this.loadViewState(this.input.getResource());
this.scrollYPercentage = state ? state.scrollYPercentage : 0;
@@ -264,7 +264,7 @@
contentWindow.addEventListener('scroll', handleInnerScroll);
pendingMessages.forEach(function (data) {
contentWindow.postMessage(data, document.location.origin);
contentWindow.postMessage(data, '*');
});
pendingMessages = [];
}
@@ -303,7 +303,7 @@
} else {
const target = getActiveFrame();
if (target) {
target.contentWindow.postMessage(data, document.location.origin);
target.contentWindow.postMessage(data, '*');
}
}
});
+47 -10
View File
@@ -14,6 +14,9 @@ import { ITheme, LIGHT, DARK } from 'vs/platform/theme/common/themeService';
import { WebviewFindWidget } from './webviewFindWidget';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { IContextKey } from 'vs/platform/contextkey/common/contextkey';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { normalize, join } from 'vs/base/common/paths';
import { startsWith } from 'vs/base/common/strings';
export interface WebviewElementFindInPageOptions {
forward?: boolean;
@@ -39,8 +42,6 @@ export interface WebviewOptions {
}
export default class Webview {
private static index: number = 0;
private readonly _webview: Electron.WebviewTag;
private _ready: Promise<this>;
private _disposables: IDisposable[] = [];
@@ -55,13 +56,15 @@ export default class Webview {
constructor(
private readonly parent: HTMLElement,
private readonly _styleElement: Element,
@IContextViewService private readonly _contextViewService: IContextViewService,
private readonly _environmentService: IEnvironmentService,
private readonly _contextViewService: IContextViewService,
private readonly _contextKey: IContextKey<boolean>,
private readonly _findInputContextKey: IContextKey<boolean>,
private _options: WebviewOptions = {},
private _options: WebviewOptions,
useSameOriginForRoot: boolean
) {
this._webview = document.createElement('webview');
this._webview.setAttribute('partition', this._options.allowSvgs ? 'webview' : `webview${Webview.index++}`);
this._webview.setAttribute('partition', this._options.allowSvgs ? 'webview' : `webview${Date.now()}`);
// disable auxclick events (see https://developers.google.com/web/updates/2016/10/auxclick)
this._webview.setAttribute('disableblinkfeatures', 'Auxclick');
@@ -75,7 +78,7 @@ export default class Webview {
this._webview.style.outline = '0';
this._webview.preload = require.toUrl('./webview-pre.js');
this._webview.src = require.toUrl('./webview.html');
this._webview.src = useSameOriginForRoot ? require.toUrl('./webview.html') : 'data:text/html;charset=utf-8,%3C%21DOCTYPE%20html%3E%0D%0A%3Chtml%20lang%3D%22en%22%20style%3D%22width%3A%20100%25%3B%20height%3A%20100%25%22%3E%0D%0A%3Chead%3E%0D%0A%09%3Ctitle%3EVirtual%20Document%3C%2Ftitle%3E%0D%0A%3C%2Fhead%3E%0D%0A%3Cbody%20style%3D%22margin%3A%200%3B%20overflow%3A%20hidden%3B%20width%3A%20100%25%3B%20height%3A%20100%25%22%3E%0D%0A%3C%2Fbody%3E%0D%0A%3C%2Fhtml%3E';
this._ready = new Promise<this>(resolve => {
const subscription = addDisposableListener(this._webview, 'ipc-message', (event) => {
@@ -89,9 +92,24 @@ export default class Webview {
});
});
if (!useSameOriginForRoot) {
let loaded = false;
this._disposables.push(addDisposableListener(this._webview, 'did-start-loading', () => {
if (loaded) {
return;
}
loaded = true;
const contents = this._webview.getWebContents();
if (contents && !contents.isDestroyed()) {
registerFileProtocol(contents, 'vscode-core-resource', this._environmentService.appRoot);
}
}));
}
if (!this._options.allowSvgs) {
let loaded = false;
const subscription = addDisposableListener(this._webview, 'did-start-loading', () => {
this._disposables.push(addDisposableListener(this._webview, 'did-start-loading', () => {
if (loaded) {
return;
}
@@ -124,9 +142,7 @@ export default class Webview {
}
return callback({ cancel: false, responseHeaders: details.responseHeaders });
});
});
this._disposables.push(subscription);
}));
}
this._disposables.push(
@@ -397,3 +413,24 @@ export default class Webview {
this._webviewFindWidget.showPreviousFindTerm();
}
}
function registerFileProtocol(
contents: Electron.WebContents,
protocol: string,
root: string
) {
contents.session.protocol.registerFileProtocol(protocol, (request, callback: any) => {
const requestPath = URI.parse(request.url).path;
const normalizedPath = normalize(join(root, requestPath));
if (startsWith(normalizedPath, root)) {
callback({ path: normalizedPath });
} else {
callback({ error: 'Cannot load resource outside of protocol root' });
}
}, (error) => {
if (error) {
console.error('Failed to register protocol ' + protocol);
}
});
}
@@ -29,14 +29,19 @@ import { onUnexpectedError } from 'vs/base/common/errors';
import { addGAParameters } from 'vs/platform/telemetry/node/telemetryNodeUtils';
import { generateTokensCSSForColorMap } from 'vs/editor/common/modes/supports/tokenization';
function renderBody(body: string, css: string): string {
function renderBody(
body: string,
css: string,
environmentService: IEnvironmentService
): string {
const styleSheetPath = require.toUrl('./media/markdown.css').replace('file://' + environmentService.appRoot, 'vscode-core-resource://');
return `<!DOCTYPE html>
<html>
<head>
<base href="https://code.visualstudio.com/raw/">
<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src https: data:; media-src https:; script-src 'none'; style-src file: https: 'unsafe-inline'; child-src 'none'; frame-src 'none';">
<link rel="stylesheet" type="text/css" href="${require.toUrl('./media/markdown.css')}">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src https: data:; media-src https:; script-src 'none'; style-src vscode-core-resource: https: 'unsafe-inline'; child-src 'none'; frame-src 'none';">
<link rel="stylesheet" type="text/css" href="${styleSheetPath}">
<style>${css}</style>
</head>
<body>${body}</body>
@@ -52,14 +57,14 @@ export class ReleaseNotesEditor extends WebviewEditor {
constructor(
@ITelemetryService telemetryService: ITelemetryService,
@IEnvironmentService private environmentService: IEnvironmentService,
@IThemeService protected themeService: IThemeService,
@IStorageService storageService: IStorageService,
@IContextKeyService contextKeyService: IContextKeyService,
@IEnvironmentService private environmentService: IEnvironmentService,
@IOpenerService private openerService: IOpenerService,
@IModeService private modeService: IModeService,
@IPartService private partService: IPartService,
@IStorageService storageService: IStorageService,
@IContextViewService private _contextViewService: IContextViewService,
@IContextKeyService contextKeyService: IContextKeyService
@IContextViewService private _contextViewService: IContextViewService
) {
super(ReleaseNotesEditor.ID, telemetryService, themeService, storageService, contextKeyService);
}
@@ -99,8 +104,8 @@ export class ReleaseNotesEditor extends WebviewEditor {
const colorMap = TokenizationRegistry.getColorMap();
const css = generateTokensCSSForColorMap(colorMap);
const body = renderBody(marked(text, { renderer }), css);
this._webview = new WebView(this.content, this.partService.getContainer(Parts.EDITOR_PART), this._contextViewService, this.contextKey, this.findInputFocusContextKey);
const body = renderBody(marked(text, { renderer }), css, this.environmentService);
this._webview = new WebView(this.content, this.partService.getContainer(Parts.EDITOR_PART), this.environmentService, this._contextViewService, this.contextKey, this.findInputFocusContextKey, {}, false);
if (this.input && this.input instanceof ReleaseNotesInput) {
const state = this.loadViewState(this.input.version);
if (state) {