Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: port window-setup to use ctx bridge instead of being run in the main world #23302

Merged
merged 4 commits into from Apr 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 1 addition & 3 deletions filenames.auto.gni
Expand Up @@ -177,11 +177,8 @@ auto_filenames = {
isolated_bundle_deps = [
"lib/common/electron-binding-setup.ts",
"lib/isolated_renderer/init.js",
"lib/renderer/ipc-renderer-internal-utils.ts",
"lib/renderer/ipc-renderer-internal.ts",
"lib/renderer/web-view/web-view-constants.ts",
"lib/renderer/web-view/web-view-element.ts",
"lib/renderer/window-setup.ts",
"package.json",
"tsconfig.electron.json",
"tsconfig.json",
Expand All @@ -191,6 +188,7 @@ auto_filenames = {
"lib/common/electron-binding-setup.ts",
"lib/common/webpack-globals-provider.ts",
"lib/content_script/init.js",
"lib/renderer/api/context-bridge.ts",
"lib/renderer/chrome-api.ts",
"lib/renderer/extensions/event.ts",
"lib/renderer/extensions/i18n.ts",
Expand Down
5 changes: 5 additions & 0 deletions lib/browser/api/web-contents.js
Expand Up @@ -429,6 +429,11 @@ WebContents.prototype._init = function () {
const referrer = { url: '', policy: 'default' };
internalWindowOpen(event, url, referrer, frameName, disposition, options);
});

const prefs = this.getWebPreferences() || {};
if (prefs.webviewTag && prefs.contextIsolation) {
electron.deprecate.log('Security Warning: A WebContents was just created with both webviewTag and contextIsolation enabled. This combination is fundamentally less secure and effectively bypasses the protections of contextIsolation. We strongly recommend you move away from webviews to OOPIF or BrowserView in order for your app to be more secure');
}
}

this.on('login', (event, ...args) => {
Expand Down
12 changes: 0 additions & 12 deletions lib/isolated_renderer/init.js
Expand Up @@ -6,22 +6,10 @@ process.electronBinding = require('@electron/internal/common/electron-binding-se

const v8Util = process.electronBinding('v8_util');

// The `lib/renderer/ipc-renderer-internal.js` module looks for the ipc object in the
// "ipc-internal" hidden value
v8Util.setHiddenValue(global, 'ipc-internal', v8Util.getHiddenValue(isolatedWorld, 'ipc-internal'));

const webViewImpl = v8Util.getHiddenValue(isolatedWorld, 'web-view-impl');

if (webViewImpl) {
// Must setup the WebView element in main world.
const { setupWebView } = require('@electron/internal/renderer/web-view/web-view-element');
setupWebView(v8Util, webViewImpl);
}

const isolatedWorldArgs = v8Util.getHiddenValue(isolatedWorld, 'isolated-world-args');

if (isolatedWorldArgs) {
const { guestInstanceId, isHiddenPage, openerId, usesNativeWindowOpen } = isolatedWorldArgs;
const { windowSetup } = require('@electron/internal/renderer/window-setup');
windowSetup(guestInstanceId, openerId, isHiddenPage, usesNativeWindowOpen);
}
15 changes: 15 additions & 0 deletions lib/renderer/api/context-bridge.ts
Expand Up @@ -18,3 +18,18 @@ const contextBridge = {
if (!binding._debugGCMaps) delete contextBridge.debugGC;

export default contextBridge;

export const internalContextBridge = {
contextIsolationEnabled,
overrideGlobalValueFromIsolatedWorld: (keys: string[], value: any) => {
return binding._overrideGlobalValueFromIsolatedWorld(keys, value, false);
},
overrideGlobalValueWithDynamicPropsFromIsolatedWorld: (keys: string[], value: any) => {
return binding._overrideGlobalValueFromIsolatedWorld(keys, value, true);
},
overrideGlobalPropertyFromIsolatedWorld: (keys: string[], getter: Function, setter?: Function) => {
return binding._overrideGlobalPropertyFromIsolatedWorld(keys, getter, setter || null);
},
isInMainWorld: () => binding._isCalledFromMainWorld() as boolean,
isInIsolatedWorld: () => binding._isCalledFromIsolatedWorld() as boolean
};
119 changes: 96 additions & 23 deletions lib/renderer/window-setup.ts
@@ -1,19 +1,24 @@
import { ipcRendererInternal } from '@electron/internal/renderer/ipc-renderer-internal';
import * as ipcRendererUtils from '@electron/internal/renderer/ipc-renderer-internal-utils';
import { internalContextBridge } from '@electron/internal/renderer/api/context-bridge';

// This file implements the following APIs:
// - window.history.back()
// - window.history.forward()
// - window.history.go()
// - window.history.length
const { contextIsolationEnabled, isInIsolatedWorld } = internalContextBridge;
const shouldUseContextBridge = contextIsolationEnabled && isInIsolatedWorld();

// This file implements the following APIs over the ctx bridge:
// - window.open()
// - window.opener.blur()
// - window.opener.close()
// - window.opener.eval()
// - window.opener.focus()
// - window.opener.location
// - window.opener.print()
// - window.opener.closed
// - window.opener.postMessage()
// - window.history.back()
// - window.history.forward()
// - window.history.go()
// - window.history.length
// - window.prompt()
// - document.hidden
// - document.visibilityState
Expand All @@ -30,13 +35,13 @@ const toString = (value: any) => {

const windowProxies = new Map<number, BrowserWindowProxy>();

const getOrCreateProxy = (guestId: number) => {
const getOrCreateProxy = (guestId: number): SafelyBoundBrowserWindowProxy => {
let proxy = windowProxies.get(guestId);
if (proxy == null) {
proxy = new BrowserWindowProxy(guestId);
windowProxies.set(guestId, proxy);
}
return proxy;
return proxy.getSafe();
};

const removeProxy = (guestId: number) => {
Expand Down Expand Up @@ -64,6 +69,8 @@ class LocationProxy {
*/
private static ProxyProperty<T> (target: LocationProxy, propertyKey: LocationProperties) {
Object.defineProperty(target, propertyKey, {
enumerable: true,
configurable: true,
get: function (this: LocationProxy): T | string {
const guestURL = this.getGuestURL();
const value = guestURL ? guestURL[propertyKey] : '';
Expand All @@ -82,6 +89,30 @@ class LocationProxy {
});
}

public getSafe = () => {
const that = this;
return {
get href () { return that.href; },
set href (newValue) { that.href = newValue; },
get hash () { return that.hash; },
set hash (newValue) { that.hash = newValue; },
get host () { return that.host; },
set host (newValue) { that.host = newValue; },
get hostname () { return that.hostname; },
set hostname (newValue) { that.hostname = newValue; },
get origin () { return that.origin; },
set origin (newValue) { that.origin = newValue; },
get pathname () { return that.pathname; },
set pathname (newValue) { that.pathname = newValue; },
get port () { return that.port; },
set port (newValue) { that.port = newValue; },
get protocol () { return that.protocol; },
set protocol (newValue) { that.protocol = newValue; },
get search () { return that.search; },
set search (newValue) { that.search = newValue; }
};
}

constructor (guestId: number) {
// eslint will consider the constructor "useless"
// unless we assign them in the body. It's fine, that's what
Expand Down Expand Up @@ -114,6 +145,17 @@ class LocationProxy {
}
}

interface SafelyBoundBrowserWindowProxy {
location: WindowProxy['location'];
blur: WindowProxy['blur'];
close: WindowProxy['close'];
eval: typeof eval; // eslint-disable-line no-eval
focus: WindowProxy['focus'];
print: WindowProxy['print'];
postMessage: WindowProxy['postMessage'];
closed: boolean;
}

class BrowserWindowProxy {
public closed: boolean = false

Expand All @@ -124,7 +166,7 @@ class BrowserWindowProxy {
// so for now, we'll have to make do with an "any" in the mix.
// https://github.com/Microsoft/TypeScript/issues/2521
public get location (): LocationProxy | any {
return this._location;
return this._location.getSafe();
}
public set location (url: string | any) {
url = resolveURL(url, this.location.href);
Expand All @@ -141,27 +183,48 @@ class BrowserWindowProxy {
});
}

public close () {
public getSafe = (): SafelyBoundBrowserWindowProxy => {
const that = this;
return {
postMessage: this.postMessage,
blur: this.blur,
close: this.close,
focus: this.focus,
print: this.print,
eval: this.eval,
get location () {
return that.location;
},
set location (url: string | any) {
that.location = url;
},
get closed () {
return that.closed;
}
};
}

public close = () => {
this._invokeWindowMethod('destroy');
}

public focus () {
public focus = () => {
this._invokeWindowMethod('focus');
}

public blur () {
public blur = () => {
this._invokeWindowMethod('blur');
}

public print () {
public print = () => {
this._invokeWebContentsMethod('print');
}

public postMessage (message: any, targetOrigin: string) {
public postMessage = (message: any, targetOrigin: string) => {
ipcRendererInternal.invoke('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_POSTMESSAGE', this.guestId, message, toString(targetOrigin), window.location.origin);
}

public eval (code: string) {
public eval = (code: string) => {
this._invokeWebContentsMethod('executeJavaScript', code);
}

Expand All @@ -182,9 +245,11 @@ export const windowSetup = (
window.close = function () {
ipcRendererInternal.sendSync('ELECTRON_BROWSER_WINDOW_CLOSE');
};
if (shouldUseContextBridge) internalContextBridge.overrideGlobalValueFromIsolatedWorld(['close'], window.close);
}

if (!usesNativeWindowOpen) {
// TODO(MarshallOfSound): Make compatible with ctx isolation without hole-punch
// Make the browser window or guest view emit "new-window" event.
(window as any).open = function (url?: string, frameName?: string, features?: string) {
if (url != null && url !== '') {
Expand All @@ -197,16 +262,19 @@ export const windowSetup = (
return null;
}
};
if (shouldUseContextBridge) internalContextBridge.overrideGlobalValueWithDynamicPropsFromIsolatedWorld(['open'], window.open);
}

if (openerId != null) {
window.opener = getOrCreateProxy(openerId);
if (shouldUseContextBridge) internalContextBridge.overrideGlobalValueWithDynamicPropsFromIsolatedWorld(['opener'], window.opener);
}

// But we do not support prompt().
window.prompt = function () {
throw new Error('prompt() is and will not be supported.');
};
if (shouldUseContextBridge) internalContextBridge.overrideGlobalValueFromIsolatedWorld(['prompt'], window.prompt);

if (!usesNativeWindowOpen || openerId != null) {
ipcRendererInternal.on('ELECTRON_GUEST_WINDOW_POSTMESSAGE', function (
Expand All @@ -233,20 +301,25 @@ export const windowSetup = (
window.history.back = function () {
ipcRendererInternal.send('ELECTRON_NAVIGATION_CONTROLLER_GO_BACK');
};
if (shouldUseContextBridge) internalContextBridge.overrideGlobalValueFromIsolatedWorld(['history', 'back'], window.history.back);

window.history.forward = function () {
ipcRendererInternal.send('ELECTRON_NAVIGATION_CONTROLLER_GO_FORWARD');
};
if (shouldUseContextBridge) internalContextBridge.overrideGlobalValueFromIsolatedWorld(['history', 'forward'], window.history.forward);

window.history.go = function (offset: number) {
ipcRendererInternal.send('ELECTRON_NAVIGATION_CONTROLLER_GO_TO_OFFSET', +offset);
};
if (shouldUseContextBridge) internalContextBridge.overrideGlobalValueFromIsolatedWorld(['history', 'go'], window.history.go);

const getHistoryLength = () => ipcRendererInternal.sendSync('ELECTRON_NAVIGATION_CONTROLLER_LENGTH');
Object.defineProperty(window.history, 'length', {
get: function () {
return ipcRendererInternal.sendSync('ELECTRON_NAVIGATION_CONTROLLER_LENGTH');
}
get: getHistoryLength,
set () {}
});
// TODO(MarshallOfSound): Fix so that the internal context bridge can override a non-configurable property
// if (shouldUseContextBridge) internalContextBridge.overrideGlobalPropertyFromIsolatedWorld(['history', 'length'], getHistoryLength);
}

if (guestInstanceId != null) {
Expand All @@ -268,16 +341,16 @@ export const windowSetup = (
});

// Make document.hidden and document.visibilityState return the correct value.
const getDocumentHidden = () => cachedVisibilityState !== 'visible';
Object.defineProperty(document, 'hidden', {
get: function () {
return cachedVisibilityState !== 'visible';
}
get: getDocumentHidden
});
if (shouldUseContextBridge) internalContextBridge.overrideGlobalPropertyFromIsolatedWorld(['document', 'hidden'], getDocumentHidden);

const getDocumentVisibilityState = () => cachedVisibilityState;
Object.defineProperty(document, 'visibilityState', {
get: function () {
return cachedVisibilityState;
}
get: getDocumentVisibilityState
});
if (shouldUseContextBridge) internalContextBridge.overrideGlobalPropertyFromIsolatedWorld(['document', 'visibilityState'], getDocumentVisibilityState);
}
};