Encode webview parent origin in authority

Fixes #144703
This commit is contained in:
Matt Bierner 2022-03-08 11:28:59 -08:00
parent fa6ee6ce91
commit c569182d08
No known key found for this signature in database
GPG key ID: 099C331567E11888
7 changed files with 67 additions and 25 deletions

View file

@ -27,7 +27,7 @@
"licenseFileName": "LICENSE.txt",
"reportIssueUrl": "https://github.com/microsoft/vscode/issues/new",
"urlProtocol": "code-oss",
"webviewContentExternalBaseUrlTemplate": "https://{{uuid}}.vscode-webview.net/insider/93a2a2fa12dd3ae0629eec01c05a28cb60ac1c4b/out/vs/workbench/contrib/webview/browser/pre/",
"webviewContentExternalBaseUrlTemplate": "https://{{uuid}}.vscode-webview.net/insider/181b43c0e2949e36ecb623d8cc6de29d4fa2bae8/out/vs/workbench/contrib/webview/browser/pre/",
"builtInExtensions": [
{
"name": "ms-vscode.references-view",

View file

@ -22,7 +22,6 @@ const searchParams = new URL(location.toString()).searchParams;
const ID = searchParams.get('id');
const onElectron = searchParams.get('platform') === 'electron';
const expectedWorkerVersion = parseInt(searchParams.get('swVersion'));
const parentOrigin = searchParams.get('parentOrigin');
/**
* Use polling to track focus of main webview and iframes within the webview
@ -309,8 +308,43 @@ const hostMessaging = new class HostMessaging {
handlers.push(handler);
}
signalReady() {
window.parent.postMessage({ target: ID, channel: 'webview-ready', data: {} }, parentOrigin, [this.channel.port2]);
async signalReady() {
const start = (/** @type {string} */ parentOrigin) => {
window.parent.postMessage({ target: ID, channel: 'webview-ready', data: {} }, parentOrigin, [this.channel.port2]);
};
const parentOrigin = searchParams.get('parentOrigin');
const id = searchParams.get('id');
const hostname = location.hostname;
if (!crypto.subtle) {
// cannot validate, not running in a secure context
throw new Error(`Cannot validate in current context!`);
}
// Here the `parentOriginHash()` function from `src/vs/workbench/common/webview.ts` is inlined
// compute a sha-256 composed of `parentOrigin` and `salt` converted to base 32
let parentOriginHash;
try {
const strData = JSON.stringify({ parentOrigin, salt: id });
const encoder = new TextEncoder();
const arrData = encoder.encode(strData);
const hash = await crypto.subtle.digest('sha-256', arrData);
const hashArray = Array.from(new Uint8Array(hash));
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
// sha256 has 256 bits, so we need at most ceil(lg(2^256-1)/lg(32)) = 52 chars to represent it in base 32
parentOriginHash = BigInt(`0x${hashHex}`).toString(32).padStart(52, '0');
} catch (err) {
throw err instanceof Error ? err : new Error(String(err));
}
if (hostname === parentOriginHash || hostname.startsWith(parentOriginHash + '.')) {
// validation succeeded!
return start(parentOrigin);
}
throw new Error(`Expected '${parentOriginHash}' as hostname or subdomain!`);
}
}();

View file

@ -30,6 +30,7 @@ import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remot
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { ITunnelService } from 'vs/platform/tunnel/common/tunnel';
import { WebviewPortMappingManager } from 'vs/platform/webview/common/webviewPortMapping';
import { parentOriginHash } from 'vs/workbench/browser/webview';
import { asWebviewUri, decodeAuthority, webviewGenericCspSource, webviewRootResourceAuthority } from 'vs/workbench/common/webview';
import { loadLocalResource, WebviewResourceResponse } from 'vs/workbench/contrib/webview/browser/resourceLoading';
import { WebviewThemeDataProvider } from 'vs/workbench/contrib/webview/browser/themeing';
@ -99,7 +100,10 @@ namespace WebviewState {
export class WebviewElement extends Disposable implements IWebview, WebviewFindDelegate {
public readonly id: string;
protected readonly iframeId: string;
private readonly iframeId: string;
private readonly encodedWebviewOriginPromise: Promise<string>;
private encodedWebviewOrigin: string | undefined;
protected get platform(): string { return 'browser'; }
@ -144,6 +148,8 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD
protected readonly _webviewFindWidget: WebviewFindWidget | undefined;
public readonly checkImeCompletionState = true;
private _disposed = false;
constructor(
id: string,
private readonly options: WebviewOptions,
@ -166,6 +172,7 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD
this.id = id;
this.iframeId = generateUuid();
this.encodedWebviewOriginPromise = parentOriginHash(window.origin, this.iframeId).then(id => this.encodedWebviewOrigin = id);
this.content = {
html: '',
@ -183,11 +190,11 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD
const subscription = this._register(addDisposableListener(window, 'message', (e: MessageEvent) => {
if (e?.data?.target !== this.iframeId) {
if (!this.encodedWebviewOrigin || e?.data?.target !== this.iframeId) {
return;
}
if (e.origin !== this.webviewContentOrigin) {
if (e.origin !== this.webviewContentOrigin(this.encodedWebviewOrigin)) {
console.log(`Skipped renderer receiving message due to mismatched origins: ${e.origin} ${this.webviewContentOrigin}`);
return;
}
@ -342,10 +349,16 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD
this.styledFindWidget();
}
this.initElement(extension, options);
this.encodedWebviewOriginPromise.then(encodedWebviewOrigin => {
if (!this._disposed) {
this.initElement(encodedWebviewOrigin, extension, options);
}
});
}
override dispose(): void {
this._disposed = true;
this.element?.remove();
this._element = undefined;
@ -425,7 +438,7 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD
return element;
}
private initElement(extension: WebviewExtensionDescription | undefined, options: WebviewOptions) {
private initElement(encodedWebviewOrigin: string, extension: WebviewExtensionDescription | undefined, options: WebviewOptions) {
// The extensionId and purpose in the URL are used for filtering in js-debug:
const params: { [key: string]: string } = {
id: this.iframeId,
@ -449,7 +462,7 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD
// Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=1754872
const fileName = isFirefox ? 'index-no-csp.html' : 'index.html';
this.element!.setAttribute('src', `${this.webviewContentEndpoint}/${fileName}?${queryString}`);
this.element!.setAttribute('src', `${this.webviewContentEndpoint(encodedWebviewOrigin)}/${fileName}?${queryString}`);
}
public mountTo(parent: HTMLElement) {
@ -463,22 +476,17 @@ export class WebviewElement extends Disposable implements IWebview, WebviewFindD
parent.appendChild(this.element);
}
protected get webviewContentEndpoint(): string {
const endpoint = this._environmentService.webviewExternalEndpoint!.replace('{{uuid}}', this.id);
protected webviewContentEndpoint(encodedWebviewOrigin: string): string {
const endpoint = this._environmentService.webviewExternalEndpoint!.replace('{{uuid}}', encodedWebviewOrigin);
if (endpoint[endpoint.length - 1] === '/') {
return endpoint.slice(0, endpoint.length - 1);
}
return endpoint;
}
private _webviewContentOrigin?: string;
private get webviewContentOrigin(): string {
if (!this._webviewContentOrigin) {
const uri = URI.parse(this.webviewContentEndpoint);
this._webviewContentOrigin = uri.scheme + '://' + uri.authority.toLowerCase();
}
return this._webviewContentOrigin;
private webviewContentOrigin(encodedWebviewOrigin: string): string {
const uri = URI.parse(this.webviewContentEndpoint(encodedWebviewOrigin));
return uri.scheme + '://' + uri.authority.toLowerCase();
}
private doPostMessage(channel: string, data?: any, transferable: Transferable[] = []): void {

View file

@ -92,8 +92,8 @@ export class ElectronWebviewElement extends WebviewElement {
}
}
protected override get webviewContentEndpoint(): string {
return `${Schemas.vscodeWebview}://${this.iframeId}`;
protected override webviewContentEndpoint(iframeId: string): string {
return `${Schemas.vscodeWebview}://${iframeId}`;
}
protected override streamToBuffer(stream: VSBufferReadableStream): Promise<ArrayBufferLike> {

View file

@ -182,7 +182,7 @@ export class BrowserWorkbenchEnvironmentService implements IBrowserWorkbenchEnvi
const webviewExternalEndpointCommit = this.payload?.get('webviewExternalEndpointCommit');
return endpoint
.replace('{{commit}}', webviewExternalEndpointCommit ?? this.productService.commit ?? '93a2a2fa12dd3ae0629eec01c05a28cb60ac1c4b')
.replace('{{commit}}', webviewExternalEndpointCommit ?? this.productService.commit ?? '181b43c0e2949e36ecb623d8cc6de29d4fa2bae8')
.replace('{{quality}}', (webviewExternalEndpointCommit ? 'insider' : this.productService.quality) ?? 'insider');
}

View file

@ -282,7 +282,7 @@ async function launchBrowser(options: LaunchOptions, endpoint: string) {
}
});
const payloadParam = `[["enableProposedApi",""],["webviewExternalEndpointCommit","93a2a2fa12dd3ae0629eec01c05a28cb60ac1c4b"],["skipWelcome","true"]]`;
const payloadParam = `[["enableProposedApi",""],["webviewExternalEndpointCommit","181b43c0e2949e36ecb623d8cc6de29d4fa2bae8"],["skipWelcome","true"]]`;
await measureAndLog(page.goto(`${endpoint}&folder=${URI.file(workspacePath!).path}&payload=${payloadParam}`), 'page.goto()', logger);
return { browser, context, page };

View file

@ -65,7 +65,7 @@ async function runTestsInBrowser(browserType: BrowserType, endpoint: url.UrlWith
const testExtensionUri = url.format({ pathname: URI.file(path.resolve(optimist.argv.extensionDevelopmentPath)).path, protocol, host, slashes: true });
const testFilesUri = url.format({ pathname: URI.file(path.resolve(optimist.argv.extensionTestsPath)).path, protocol, host, slashes: true });
const payloadParam = `[["extensionDevelopmentPath","${testExtensionUri}"],["extensionTestsPath","${testFilesUri}"],["enableProposedApi",""],["webviewExternalEndpointCommit","93a2a2fa12dd3ae0629eec01c05a28cb60ac1c4b"],["skipWelcome","true"]]`;
const payloadParam = `[["extensionDevelopmentPath","${testExtensionUri}"],["extensionTestsPath","${testFilesUri}"],["enableProposedApi",""],["webviewExternalEndpointCommit","181b43c0e2949e36ecb623d8cc6de29d4fa2bae8"],["skipWelcome","true"]]`;
if (path.extname(testWorkspacePath) === '.code-workspace') {
await page.goto(`${endpoint.href}&workspace=${testWorkspacePath}&payload=${payloadParam}`);