From c74dbf449ec8087ef34650a533aa7fb506422241 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Wed, 3 Jun 2026 14:04:38 -0700 Subject: [PATCH 1/2] Block programmatic webview click and keypress re-dispatch events in some cases Should disable these entirely for notebook webviews as I don't think there's a valid use case there For now I'm keeping it enabled for extension webviews as this re-dispatch is the only way I know for a full screen iframe to not eat all keypresses. If needed we can add something on the webview vscode api object that should allow this to be even safer --- .../api/browser/mainThreadWebviews.ts | 1 + .../view/renderers/backLayerWebView.ts | 1 + .../contrib/webview/browser/pre/index.html | 22 ++++++++++++------- .../contrib/webview/browser/webview.ts | 13 +++++++++++ .../contrib/webview/browser/webviewElement.ts | 14 +++++++++--- .../webview/browser/webviewMessages.d.ts | 19 ++++++++-------- .../browser/webviewEditorInputSerializer.ts | 1 + 7 files changed, 51 insertions(+), 20 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadWebviews.ts b/src/vs/workbench/api/browser/mainThreadWebviews.ts index 51fb615d502..0d599713236 100644 --- a/src/vs/workbench/api/browser/mainThreadWebviews.ts +++ b/src/vs/workbench/api/browser/mainThreadWebviews.ts @@ -152,6 +152,7 @@ export function reviveWebviewContentOptions(webviewOptions: extHostProtocol.IWeb return { allowScripts: webviewOptions.enableScripts, allowForms: webviewOptions.enableForms, + forwardUntrustedKeypressEvents: true, // This is always enabled for extensions enableCommandUris: webviewOptions.enableCommandUris, localResourceRoots: Array.isArray(webviewOptions.localResourceRoots) ? webviewOptions.localResourceRoots.map(r => URI.revive(r)) : undefined, portMapping: webviewOptions.portMapping, diff --git a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts index d3c4ea3f178..1de54375c42 100644 --- a/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts +++ b/src/vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView.ts @@ -1205,6 +1205,7 @@ export class BackLayerWebView extends Themable { contentOptions: { allowMultipleAPIAcquire: true, allowScripts: true, + forwardUntrustedKeypressEvents: false, localResourceRoots: this.localResourceRootsCache, }, extension: undefined, diff --git a/src/vs/workbench/contrib/webview/browser/pre/index.html b/src/vs/workbench/contrib/webview/browser/pre/index.html index 8b9850b3077..1b722167644 100644 --- a/src/vs/workbench/contrib/webview/browser/pre/index.html +++ b/src/vs/workbench/contrib/webview/browser/pre/index.html @@ -5,7 +5,7 @@ + content="default-src 'none'; script-src 'sha256-nXjtuhBilO++r8hfxl5VjEScSmdm07wDAk6jw228DgM=' 'self'; frame-src 'self'; style-src 'unsafe-inline';"> { - if (!event?.view?.document) { + if (!event.isTrusted || !event?.view?.document) { return; } @@ -606,7 +606,8 @@ e.preventDefault(); } - hostMessaging.postMessage('did-keydown', { + /** @type {import('../webviewMessages').FromWebviewMessage['did-keydown']} */ + const data = { key: e.key, keyCode: e.keyCode, code: e.code, @@ -614,14 +615,17 @@ altKey: e.altKey, ctrlKey: e.ctrlKey, metaKey: e.metaKey, - repeat: e.repeat - }); + repeat: e.repeat, + isTrusted: e.isTrusted + }; + hostMessaging.postMessage('did-keydown', data); }; /** * @param {KeyboardEvent} e */ const handleInnerKeyup = (e) => { - hostMessaging.postMessage('did-keyup', { + /** @type {import('../webviewMessages').FromWebviewMessage['did-keyup']} */ + const data = { key: e.key, keyCode: e.keyCode, code: e.code, @@ -629,8 +633,10 @@ altKey: e.altKey, ctrlKey: e.ctrlKey, metaKey: e.metaKey, - repeat: e.repeat - }); + repeat: e.repeat, + isTrusted: e.isTrusted + }; + hostMessaging.postMessage('did-keyup', data); }; /** diff --git a/src/vs/workbench/contrib/webview/browser/webview.ts b/src/vs/workbench/contrib/webview/browser/webview.ts index 9c990d657df..f8b6c143665 100644 --- a/src/vs/workbench/contrib/webview/browser/webview.ts +++ b/src/vs/workbench/contrib/webview/browser/webview.ts @@ -123,6 +123,18 @@ export interface WebviewContentOptions { */ readonly allowForms?: boolean; + /** + * Should untrusted key events from the webview be forwarded to the workbench? Defaults to false. + * + * This blocks keypress events that are triggered by scripts. Webviews should not allow untrusted scripts + * to be run, so this is more of a defense-in-depth measure. + * + * There are valid reasons to enable this, such when a webview embeds an iframe. In those cases, keypress events would + * normally be eaten by the iframe, so the inner iframe needs to forward keypress event to the parent webview, + * which then needs to forward them to the workbench. + */ + readonly forwardUntrustedKeypressEvents?: boolean; + /** * Set of root paths from which the webview can load local resources. */ @@ -149,6 +161,7 @@ export function areWebviewContentOptionsEqual(a: WebviewContentOptions, b: Webvi a.allowMultipleAPIAcquire === b.allowMultipleAPIAcquire && a.allowScripts === b.allowScripts && a.allowForms === b.allowForms + && a.forwardUntrustedKeypressEvents === b.forwardUntrustedKeypressEvents && equals(a.localResourceRoots, b.localResourceRoots, isEqual) && equals(a.portMapping, b.portMapping, (a, b) => a.extensionHostPort === b.extensionHostPort && a.webviewPort === b.webviewPort) && areEnableCommandUrisEqual(a, b) diff --git a/src/vs/workbench/contrib/webview/browser/webviewElement.ts b/src/vs/workbench/contrib/webview/browser/webviewElement.ts index 17fa9d1d90b..6bca383c22c 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewElement.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewElement.ts @@ -247,9 +247,6 @@ export class WebviewElement extends Disposable implements IWebviewElement, Webvi })); this._register(this.on('did-keydown', (data) => { - // Electron: workaround for https://github.com/electron/electron/issues/14258 - // We have to detect keyboard events in the and dispatch them to our - // keybinding service because these events do not bubble to the parent window anymore. this.handleKeyEvent('keydown', data); })); @@ -713,7 +710,18 @@ export class WebviewElement extends Disposable implements IWebviewElement, Webvi } } + private shouldForwardKeyEvent(event: KeyEvent): boolean { + return event.isTrusted || !!this._content.options.forwardUntrustedKeypressEvents; + } + private handleKeyEvent(type: 'keydown' | 'keyup', event: KeyEvent) { + if (!this.shouldForwardKeyEvent(event)) { + return; + } + + // Electron: workaround for https://github.com/electron/electron/issues/14258 + // We have to detect keyboard events in the and dispatch them to our + // keybinding service because these events do not bubble to the parent window anymore. // Create a fake KeyboardEvent from the data provided const emulatedKeyboardEvent = new KeyboardEvent(type, event); // Force override the target diff --git a/src/vs/workbench/contrib/webview/browser/webviewMessages.d.ts b/src/vs/workbench/contrib/webview/browser/webviewMessages.d.ts index 33977290557..5ae6734308e 100644 --- a/src/vs/workbench/contrib/webview/browser/webviewMessages.d.ts +++ b/src/vs/workbench/contrib/webview/browser/webviewMessages.d.ts @@ -7,18 +7,19 @@ import type { IMouseWheelEvent } from '../../../../base/browser/mouseEvent.js'; import type { WebviewStyles } from './webview.js'; type KeyEvent = { - key: string; - keyCode: number; - code: string; - shiftKey: boolean; - altKey: boolean; - ctrlKey: boolean; - metaKey: boolean; - repeat: boolean; + readonly key: string; + readonly keyCode: number; + readonly code: string; + readonly shiftKey: boolean; + readonly altKey: boolean; + readonly ctrlKey: boolean; + readonly metaKey: boolean; + readonly repeat: boolean; + readonly isTrusted: boolean; } type WebViewDragEvent = { - shiftKey: boolean; + readonly shiftKey: boolean; } export type FromWebviewMessage = { diff --git a/src/vs/workbench/contrib/webviewPanel/browser/webviewEditorInputSerializer.ts b/src/vs/workbench/contrib/webviewPanel/browser/webviewEditorInputSerializer.ts index 9e10a658dda..d0603af35e0 100644 --- a/src/vs/workbench/contrib/webviewPanel/browser/webviewEditorInputSerializer.ts +++ b/src/vs/workbench/contrib/webviewPanel/browser/webviewEditorInputSerializer.ts @@ -184,6 +184,7 @@ export function restoreWebviewOptions(options: SerializedWebviewOptions): Webvie export function restoreWebviewContentOptions(options: SerializedWebviewOptions): WebviewContentOptions { return { ...options, + forwardUntrustedKeypressEvents: options.forwardUntrustedKeypressEvents ?? true, // default to true for backwards compatibility localResourceRoots: options.localResourceRoots?.map(uri => reviveUri(uri)), }; } From 86d15f68f9172439f546f4cfd8c91a66ca043ad8 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Wed, 3 Jun 2026 16:41:41 -0700 Subject: [PATCH 2/2] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/vs/workbench/contrib/webview/browser/webview.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/webview/browser/webview.ts b/src/vs/workbench/contrib/webview/browser/webview.ts index f8b6c143665..f950b7b5a64 100644 --- a/src/vs/workbench/contrib/webview/browser/webview.ts +++ b/src/vs/workbench/contrib/webview/browser/webview.ts @@ -129,8 +129,8 @@ export interface WebviewContentOptions { * This blocks keypress events that are triggered by scripts. Webviews should not allow untrusted scripts * to be run, so this is more of a defense-in-depth measure. * - * There are valid reasons to enable this, such when a webview embeds an iframe. In those cases, keypress events would - * normally be eaten by the iframe, so the inner iframe needs to forward keypress event to the parent webview, + * There are valid reasons to enable this, such as when a webview embeds an iframe. In those cases, keyboard events can + * be eaten by the iframe, so the inner iframe needs to forward keyboard events to the parent webview, * which then needs to forward them to the workbench. */ readonly forwardUntrustedKeypressEvents?: boolean;