diff --git a/docs/api/context-bridge.md b/docs/api/context-bridge.md index 5d62220bd6070..6699d5ef59fdf 100644 --- a/docs/api/context-bridge.md +++ b/docs/api/context-bridge.md @@ -46,11 +46,17 @@ The `contextBridge` module has the following methods: * `apiKey` string - The key to inject the API onto `window` with. The API will be accessible on `window[apiKey]`. * `api` any - Your API, more information on what this API can be and how it works is available below. +### `contextBridge.exposeInIsolatedWorld(worldId, apiKey, api)` + +* `worldId` Integer - The ID of the world to inject the API into. This has to be an existing world. 0 is the default world, 999 is the world used by Electrons contextIsolation feature. Chrome extensions reserve the range of IDs in [1 << 20, 1 << 29). You can provide any integer here. +* `apiKey` string - The key to inject the API onto `window` with. The API will be accessible on `window[apiKey]`. +* `api` any - Your API, more information on what this API can be and how it works is available below. + ## Usage ### API -The `api` provided to [`exposeInMainWorld`](#contextbridgeexposeinmainworldapikey-api) must be a `Function`, `string`, `number`, `Array`, `boolean`, or an object +The `api` provided to [`exposeInMainWorld`](#contextbridgeexposeinmainworldapikey-api) and [`exposeInIsolatedWorld`](#contextbridgeexposeinisolatedworldworldid-apikey-api) must be a `Function`, `string`, `number`, `Array`, `boolean`, or an object whose keys are strings and values are a `Function`, `string`, `number`, `Array`, `boolean`, or another nested object that meets the same conditions. `Function` values are proxied to the other context and all other values are **copied** and **frozen**. Any data / primitives sent in @@ -84,6 +90,32 @@ contextBridge.exposeInMainWorld( ) ``` +An example of API exposed in an isolated world is shown below: + +```javascript + +const { contextBridge, webFrame } = require('electron') + +webFrame.setIsolatedWorldInfo(1005, { + name: 'Isolated World' +}) + +contextBridge.exposeInIsolatedWorld( + 1005, + 'electron', + { + doThing: () => ipcRenderer.send('do-a-thing') + } +) + +// To call this API in isolated world, you can use `executeJavaScriptInIsolatedWorld` +webFrame.executeJavaScriptInIsolatedWorld(1005, [ + { + code: 'window.electron.doThing()' + } +]) +``` + ### API Functions `Function` values that you bind through the `contextBridge` are proxied through Electron to ensure that contexts remain isolated. This diff --git a/lib/renderer/api/context-bridge.ts b/lib/renderer/api/context-bridge.ts index dd545767c93b1..99b133e6c3ac0 100644 --- a/lib/renderer/api/context-bridge.ts +++ b/lib/renderer/api/context-bridge.ts @@ -7,7 +7,11 @@ const checkContextIsolationEnabled = () => { const contextBridge: Electron.ContextBridge = { exposeInMainWorld: (key: string, api: any) => { checkContextIsolationEnabled(); - return binding.exposeAPIInMainWorld(key, api); + return binding.exposeAPIInWorld(0, key, api); + }, + exposeInIsolatedWorld: (worldId: number, key: string, api: any) => { + checkContextIsolationEnabled(); + return binding.exposeAPIInWorld(worldId, key, api); } }; diff --git a/shell/renderer/api/electron_api_context_bridge.cc b/shell/renderer/api/electron_api_context_bridge.cc index 22f8c0adada0d..5ee9bf164ea50 100644 --- a/shell/renderer/api/electron_api_context_bridge.cc +++ b/shell/renderer/api/electron_api_context_bridge.cc @@ -560,50 +560,57 @@ v8::MaybeLocal CreateProxyForAPI( } } -void ExposeAPIInMainWorld(v8::Isolate* isolate, - const std::string& key, - v8::Local api, - gin_helper::Arguments* args) { - TRACE_EVENT1("electron", "ContextBridge::ExposeAPIInMainWorld", "key", key); +void ExposeAPIInWorld(v8::Isolate* isolate, + const int world_id, + const std::string& key, + v8::Local api, + gin_helper::Arguments* args) { + TRACE_EVENT2("electron", "ContextBridge::ExposeAPIInWorld", "key", key, + "worldId", world_id); auto* render_frame = GetRenderFrame(isolate->GetCurrentContext()->Global()); CHECK(render_frame); auto* frame = render_frame->GetWebFrame(); CHECK(frame); + v8::Local main_context = frame->MainWorldScriptContext(); - gin_helper::Dictionary global(main_context->GetIsolate(), - main_context->Global()); + v8::Local context = + world_id == 0 ? main_context + : frame->GetScriptContextFromWorldId(isolate, world_id); + + gin_helper::Dictionary properties(context->GetIsolate(), context->Global()); - if (global.Has(key)) { + if (properties.Has(key)) { args->ThrowError( "Cannot bind an API on top of an existing property on the window " "object"); return; } - v8::Local isolated_context = frame->GetScriptContextFromWorldId( - args->isolate(), WorldIDs::ISOLATED_WORLD_ID); + v8::Local electron_isolated_context = + frame->GetScriptContextFromWorldId(args->isolate(), + WorldIDs::ISOLATED_WORLD_ID); { context_bridge::ObjectCache object_cache; v8::Context::Scope main_context_scope(main_context); v8::MaybeLocal maybe_proxy = PassValueToOtherContext( - isolated_context, main_context, api, &object_cache, false, 0); + electron_isolated_context, context, api, &object_cache, false, 0); if (maybe_proxy.IsEmpty()) return; auto proxy = maybe_proxy.ToLocalChecked(); if (base::FeatureList::IsEnabled(features::kContextBridgeMutability)) { - global.Set(key, proxy); + isolated.Set(key, proxy); return; } if (proxy->IsObject() && !proxy->IsTypedArray() && - !DeepFreeze(proxy.As(), main_context)) + !DeepFreeze(proxy.As(), electron_isolated_context)) return; - global.SetReadOnlyNonConfigurable(key, proxy); + isolated.SetReadOnlyNonConfigurable(key, proxy); } } @@ -716,7 +723,7 @@ void Initialize(v8::Local exports, void* priv) { v8::Isolate* isolate = context->GetIsolate(); gin_helper::Dictionary dict(isolate, exports); - dict.SetMethod("exposeAPIInMainWorld", &electron::api::ExposeAPIInMainWorld); + dict.SetMethod("exposeAPIInWorld", &electron::api::ExposeAPIInWorld); dict.SetMethod("_overrideGlobalValueFromIsolatedWorld", &electron::api::OverrideGlobalValueFromIsolatedWorld); dict.SetMethod("_overrideGlobalPropertyFromIsolatedWorld", diff --git a/spec-main/api-context-bridge-spec.ts b/spec-main/api-context-bridge-spec.ts index dc3601dbf8841..2eab03827186f 100644 --- a/spec-main/api-context-bridge-spec.ts +++ b/spec-main/api-context-bridge-spec.ts @@ -62,17 +62,27 @@ describe('contextBridge', () => { const generateTests = (useSandbox: boolean) => { describe(`with sandbox=${useSandbox}`, () => { - const makeBindingWindow = async (bindingCreator: Function) => { - const preloadContent = `const renderer_1 = require('electron'); + const makeBindingWindow = async (bindingCreator: Function, worldId: number = 0) => { + const preloadContentForMainContext = `const renderer_1 = require('electron'); ${useSandbox ? '' : `require('v8').setFlagsFromString('--expose_gc'); const gc=require('vm').runInNewContext('gc'); renderer_1.contextBridge.exposeInMainWorld('GCRunner', { run: () => gc() });`} (${bindingCreator.toString()})();`; + + const preloadContentForIsolatedContext = `const renderer_1 = require('electron'); + ${useSandbox ? '' : `require('v8').setFlagsFromString('--expose_gc'); + renderer_1.webFrame.setIsolatedWorldInfo(${worldId}, { + name: "Isolated World" + }); + renderer_1.contextBridge.exposeInIsolatedWorld(1004, 'GCRunner', { + run: () => gc() + });`} + (${bindingCreator.toString()})();`; const tmpDir = await fs.mkdtemp(path.resolve(os.tmpdir(), 'electron-spec-preload-')); dir = tmpDir; - await fs.writeFile(path.resolve(tmpDir, 'preload.js'), preloadContent); + await fs.writeFile(path.resolve(tmpDir, 'preload.js'), worldId === 0 ? preloadContentForMainContext : preloadContentForIsolatedContext); w = new BrowserWindow({ show: false, webPreferences: { @@ -86,8 +96,9 @@ describe('contextBridge', () => { await w.loadURL(`http://127.0.0.1:${(server.address() as AddressInfo).port}`); }; - const callWithBindings = (fn: Function) => - w.webContents.executeJavaScript(`(${fn.toString()})(window)`); + const callWithBindings = (fn: Function, worldId: number = 0) => + worldId === 0 ? w.webContents.executeJavaScript(`(${fn.toString()})(window)`) + : w.webContents.executeJavaScriptInIsolatedWorld(worldId, [{ code: `(${fn.toString()})(window)` }]); ; const getGCInfo = async (): Promise<{ trackedValues: number; @@ -114,6 +125,16 @@ describe('contextBridge', () => { expect(result).to.equal(123); }); + it('should proxy numbers', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInIsolatedWorld(1004, 'example', 123); + }, 1004); + const result = await callWithBindings((root: any) => { + return root.example; + }, 1004); + expect(result).to.equal(123); + }); + it('should make global properties read-only', async () => { await makeBindingWindow(() => { contextBridge.exposeInMainWorld('example', 123); @@ -150,6 +171,19 @@ describe('contextBridge', () => { expect(result).to.equal(123); }); + it('should make properties unwriteable', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInIsolatedWorld(1004, 'example', { + myNumber: 123 + }); + }, 1004); + const result = await callWithBindings((root: any) => { + root.example.myNumber = 456; + return root.example.myNumber; + }, 1004); + expect(result).to.equal(123); + }); + it('should proxy strings', async () => { await makeBindingWindow(() => { contextBridge.exposeInMainWorld('example', 'my-words'); @@ -248,6 +282,18 @@ describe('contextBridge', () => { expect(result).to.equal(true); }); + it('should proxy nested booleans', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInIsolatedWorld(1004, 'example', { + myBool: true + }); + }, 1004); + const result = await callWithBindings((root: any) => { + return root.example.myBool; + }, 1004); + expect(result).to.equal(true); + }); + it('should proxy promises and resolve with the correct value', async () => { await makeBindingWindow(() => { contextBridge.exposeInMainWorld('example',