Browse Source

Merge pull request #8348 from electron/isolated-world

Add context isolation option to windows and webview tags
Kevin Sawicki 8 years ago
parent
commit
feac8685f4

+ 7 - 1
atom/browser/web_contents_preferences.cc

@@ -119,6 +119,12 @@ void WebContentsPreferences::AppendExtraCommandLineSwitches(
       LOG(ERROR) << "preload url must be file:// protocol.";
   }
 
+  // Run Electron APIs and preload script in isolated world
+  bool isolated;
+  if (web_preferences.GetBoolean(options::kContextIsolation, &isolated) &&
+      isolated)
+    command_line->AppendSwitch(switches::kContextIsolation);
+
   // --background-color.
   std::string color;
   if (web_preferences.GetString(options::kBackgroundColor, &color))
@@ -190,7 +196,7 @@ void WebContentsPreferences::AppendExtraCommandLineSwitches(
   if (window) {
     bool visible = window->IsVisible() && !window->IsMinimized();
     if (!visible)  // Default state is visible.
-      command_line->AppendSwitch("hidden-page");
+      command_line->AppendSwitch(switches::kHiddenPage);
   }
 
   // Use frame scheduling for offscreen renderers.

+ 14 - 9
atom/common/options_switches.cc

@@ -99,7 +99,10 @@ const char kPreloadURL[] = "preloadURL";
 // Enable the node integration.
 const char kNodeIntegration[] = "nodeIntegration";
 
-// Instancd ID of guest WebContents.
+// Enable context isolation of Electron APIs and preload script
+const char kContextIsolation[] = "contextIsolation";
+
+// Instance ID of guest WebContents.
 const char kGuestInstanceID[] = "guestInstanceId";
 
 // Web runtime features.
@@ -158,14 +161,16 @@ const char kCipherSuiteBlacklist[] = "cipher-suite-blacklist";
 const char kAppUserModelId[] = "app-user-model-id";
 
 // The command line switch versions of the options.
-const char kBackgroundColor[] = "background-color";
-const char kZoomFactor[]      = "zoom-factor";
-const char kPreloadScript[]   = "preload";
-const char kPreloadURL[]      = "preload-url";
-const char kNodeIntegration[] = "node-integration";
-const char kGuestInstanceID[] = "guest-instance-id";
-const char kOpenerID[]        = "opener-id";
-const char kScrollBounce[]    = "scroll-bounce";
+const char kBackgroundColor[]  = "background-color";
+const char kZoomFactor[]       = "zoom-factor";
+const char kPreloadScript[]    = "preload";
+const char kPreloadURL[]       = "preload-url";
+const char kNodeIntegration[]  = "node-integration";
+const char kContextIsolation[] = "context-isolation";
+const char kGuestInstanceID[]  = "guest-instance-id";
+const char kOpenerID[]         = "opener-id";
+const char kScrollBounce[]     = "scroll-bounce";
+const char kHiddenPage[]       = "hidden-page";
 
 // Widevine options
 // Path to Widevine CDM binaries.

+ 3 - 0
atom/common/options_switches.h

@@ -54,6 +54,7 @@ extern const char kZoomFactor[];
 extern const char kPreloadScript[];
 extern const char kPreloadURL[];
 extern const char kNodeIntegration[];
+extern const char kContextIsolation[];
 extern const char kGuestInstanceID[];
 extern const char kExperimentalFeatures[];
 extern const char kExperimentalCanvasFeatures[];
@@ -86,9 +87,11 @@ extern const char kZoomFactor[];
 extern const char kPreloadScript[];
 extern const char kPreloadURL[];
 extern const char kNodeIntegration[];
+extern const char kContextIsolation[];
 extern const char kGuestInstanceID[];
 extern const char kOpenerID[];
 extern const char kScrollBounce[];
+extern const char kHiddenPage[];
 
 extern const char kWidevineCdmPath[];
 extern const char kWidevineCdmVersion[];

+ 2 - 1
atom/renderer/atom_render_view_observer.cc

@@ -76,6 +76,7 @@ AtomRenderViewObserver::AtomRenderViewObserver(
     content::RenderView* render_view,
     AtomRendererClient* renderer_client)
     : content::RenderViewObserver(render_view),
+      renderer_client_(renderer_client),
       document_created_(false) {
   // Initialise resource for directory listing.
   net::NetModule::SetResourceProvider(NetResourceProvider);
@@ -93,7 +94,7 @@ void AtomRenderViewObserver::EmitIPCEvent(blink::WebFrame* frame,
   v8::Isolate* isolate = blink::mainThreadIsolate();
   v8::HandleScope handle_scope(isolate);
 
-  v8::Local<v8::Context> context = frame->mainWorldScriptContext();
+  v8::Local<v8::Context> context = renderer_client_->GetContext(frame, isolate);
   v8::Context::Scope context_scope(context);
 
   // Only emit IPC event for context with node integration.

+ 2 - 0
atom/renderer/atom_render_view_observer.h

@@ -40,6 +40,8 @@ class AtomRenderViewObserver : public content::RenderViewObserver {
                         const base::string16& channel,
                         const base::ListValue& args);
 
+  AtomRendererClient* renderer_client_;
+
   // Whether the document object has been created.
   bool document_created_;
 

+ 95 - 9
atom/renderer/atom_renderer_client.cc

@@ -7,6 +7,8 @@
 #include <string>
 #include <vector>
 
+#include "atom_natives.h"  // NOLINT: This file is generated with js2c
+
 #include "atom/common/api/api_messages.h"
 #include "atom/common/api/atom_bindings.h"
 #include "atom/common/api/event_emitter_caller.h"
@@ -14,6 +16,7 @@
 #include "atom/common/native_mate_converters/value_converter.h"
 #include "atom/common/node_bindings.h"
 #include "atom/common/options_switches.h"
+#include "atom/renderer/api/atom_api_renderer_ipc.h"
 #include "atom/renderer/atom_render_view_observer.h"
 #include "atom/renderer/content_settings_observer.h"
 #include "atom/renderer/guest_view_container.h"
@@ -57,6 +60,17 @@ namespace atom {
 
 namespace {
 
+enum World {
+  MAIN_WORLD = 0,
+  // Use a high number far away from 0 to not collide with any other world
+  // IDs created internally by Chrome.
+  ISOLATED_WORLD = 999
+};
+
+enum ExtensionGroup {
+  MAIN_GROUP = 1
+};
+
 // Helper class to forward the messages to the client.
 class AtomRenderFrameObserver : public content::RenderFrameObserver {
  public:
@@ -64,7 +78,6 @@ class AtomRenderFrameObserver : public content::RenderFrameObserver {
                           AtomRendererClient* renderer_client)
       : content::RenderFrameObserver(frame),
         render_frame_(frame),
-        world_id_(-1),
         renderer_client_(renderer_client) {}
 
   // content::RenderFrameObserver:
@@ -72,19 +85,82 @@ class AtomRenderFrameObserver : public content::RenderFrameObserver {
     renderer_client_->DidClearWindowObject(render_frame_);
   }
 
+  void CreateIsolatedWorldContext() {
+    // This maps to the name shown in the context combo box in the Console tab
+    // of the dev tools.
+    render_frame_->GetWebFrame()->setIsolatedWorldHumanReadableName(
+        World::ISOLATED_WORLD,
+        blink::WebString::fromUTF8("Electron Isolated Context"));
+
+    blink::WebScriptSource source("void 0");
+    render_frame_->GetWebFrame()->executeScriptInIsolatedWorld(
+        World::ISOLATED_WORLD, &source, 1, ExtensionGroup::MAIN_GROUP);
+  }
+
+  void SetupMainWorldOverrides(v8::Handle<v8::Context> context) {
+    // Setup window overrides in the main world context
+    v8::Isolate* isolate = context->GetIsolate();
+
+    // Wrap the bundle into a function that receives the binding object as
+    // an argument.
+    std::string bundle(node::isolated_bundle_native,
+        node::isolated_bundle_native + sizeof(node::isolated_bundle_native));
+    std::string wrapper = "(function (binding) {\n" + bundle + "\n})";
+    auto script = v8::Script::Compile(
+        mate::ConvertToV8(isolate, wrapper)->ToString());
+    auto func = v8::Handle<v8::Function>::Cast(
+        script->Run(context).ToLocalChecked());
+
+    auto binding = v8::Object::New(isolate);
+    api::Initialize(binding, v8::Null(isolate), context, nullptr);
+
+    // Pass in CLI flags needed to setup window
+    base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
+    mate::Dictionary dict(isolate, binding);
+    if (command_line->HasSwitch(switches::kGuestInstanceID))
+      dict.Set(options::kGuestInstanceID,
+               command_line->GetSwitchValueASCII(switches::kGuestInstanceID));
+    if (command_line->HasSwitch(switches::kOpenerID))
+      dict.Set(options::kOpenerID,
+               command_line->GetSwitchValueASCII(switches::kOpenerID));
+    dict.Set("hiddenPage", command_line->HasSwitch(switches::kHiddenPage));
+
+    v8::Local<v8::Value> args[] = { binding };
+    ignore_result(func->Call(context, v8::Null(isolate), 1, args));
+  }
+
+  bool IsMainWorld(int world_id) {
+    return world_id == World::MAIN_WORLD;
+  }
+
+  bool IsIsolatedWorld(int world_id) {
+    return world_id == World::ISOLATED_WORLD;
+  }
+
+  bool ShouldNotifyClient(int world_id) {
+    if (renderer_client_->isolated_world() && render_frame_->IsMainFrame())
+      return IsIsolatedWorld(world_id);
+    else
+      return IsMainWorld(world_id);
+  }
+
   void DidCreateScriptContext(v8::Handle<v8::Context> context,
                               int extension_group,
                               int world_id) override {
-    if (world_id_ != -1 && world_id_ != world_id)
-      return;
-    world_id_ = world_id;
-    renderer_client_->DidCreateScriptContext(context, render_frame_);
+    if (ShouldNotifyClient(world_id))
+      renderer_client_->DidCreateScriptContext(context, render_frame_);
+
+    if (renderer_client_->isolated_world() && IsMainWorld(world_id)
+        && render_frame_->IsMainFrame()) {
+      CreateIsolatedWorldContext();
+      SetupMainWorldOverrides(context);
+    }
   }
+
   void WillReleaseScriptContext(v8::Local<v8::Context> context,
                                 int world_id) override {
-    if (world_id_ != world_id)
-      return;
-    renderer_client_->WillReleaseScriptContext(context, render_frame_);
+    if (ShouldNotifyClient(world_id))
+      renderer_client_->WillReleaseScriptContext(context, render_frame_);
   }
 
   void OnDestruct() override {
@@ -93,7 +169,6 @@ class AtomRenderFrameObserver : public content::RenderFrameObserver {
 
  private:
   content::RenderFrame* render_frame_;
-  int world_id_;
   AtomRendererClient* renderer_client_;
 
   DISALLOW_COPY_AND_ASSIGN(AtomRenderFrameObserver);
@@ -133,6 +208,8 @@ std::vector<std::string> ParseSchemesCLISwitch(const char* switch_name) {
 AtomRendererClient::AtomRendererClient()
     : node_bindings_(NodeBindings::Create(false)),
       atom_bindings_(new AtomBindings) {
+  isolated_world_ = base::CommandLine::ForCurrentProcess()->HasSwitch(
+      switches::kContextIsolation);
   // Parse --standard-schemes=scheme1,scheme2
   std::vector<std::string> standard_schemes_list =
       ParseSchemesCLISwitch(switches::kStandardSchemes);
@@ -336,4 +413,13 @@ void AtomRendererClient::AddSupportedKeySystems(
   AddChromeKeySystems(key_systems);
 }
 
+v8::Local<v8::Context> AtomRendererClient::GetContext(
+    blink::WebFrame* frame, v8::Isolate* isolate) {
+  if (isolated_world())
+    return frame->worldScriptContext(
+        isolate, World::ISOLATED_WORLD, ExtensionGroup::MAIN_GROUP);
+  else
+    return frame->mainWorldScriptContext();
+}
+
 }  // namespace atom

+ 6 - 0
atom/renderer/atom_renderer_client.h

@@ -27,6 +27,11 @@ class AtomRendererClient : public content::ContentRendererClient {
   void WillReleaseScriptContext(
       v8::Handle<v8::Context> context, content::RenderFrame* render_frame);
 
+  // Get the context that the Electron API is running in.
+  v8::Local<v8::Context> GetContext(
+      blink::WebFrame* frame, v8::Isolate* isolate);
+  bool isolated_world() { return isolated_world_; }
+
  private:
   enum NodeIntegration {
     ALL,
@@ -64,6 +69,7 @@ class AtomRendererClient : public content::ContentRendererClient {
   std::unique_ptr<NodeBindings> node_bindings_;
   std::unique_ptr<AtomBindings> atom_bindings_;
   std::unique_ptr<PreferencesManager> preferences_manager_;
+  bool isolated_world_;
 
   DISALLOW_COPY_AND_ASSIGN(AtomRendererClient);
 };

+ 16 - 0
docs/api/browser-window.md

@@ -282,6 +282,21 @@ It creates a new `BrowserWindow` with native properties as set by the `options`.
       [offscreen rendering tutorial](../tutorial/offscreen-rendering.md) for
       more details.
     * `sandbox` Boolean (optional) - Whether to enable Chromium OS-level sandbox.
+    * `contextIsolation` Boolean (optional) - Whether to run Electron APIs and
+      the specified `preload` script in a separate JavaScript context. Defaults
+      to `false`. The context that the `preload` script runs in will still
+      have full access to the `document` and `window` globals but it will use
+      its own set of JavaScript builtins (`Array`, `Object`, `JSON`, etc.)
+      and will be isolated from any changes made to the global environment
+      by the loaded page. The Electron API will only be available in the
+      `preload` script and not the loaded page. This option should be used when
+      loading potentially untrusted remote content to ensure the loaded content
+      cannot tamper with the `preload` script and any Electron APIs being used.
+      This option uses the same technique used by [Chrome Content Scripts][chrome-content-scripts].
+      You can access this context in the dev tools by selecting the
+      'Electron Isolated Context' entry in the combo box at the top of the
+      Console tab. **Note:** This option is currently experimental and may
+      change or be removed in future Electron releases.
 
 When setting minimum or maximum window size with `minWidth`/`maxWidth`/
 `minHeight`/`maxHeight`, it only constrains the users. It won't prevent you from
@@ -1254,3 +1269,4 @@ will remove the vibrancy effect on the window.
 [quick-look]: https://en.wikipedia.org/wiki/Quick_Look
 [vibrancy-docs]: https://developer.apple.com/reference/appkit/nsvisualeffectview?language=objc
 [window-levels]: https://developer.apple.com/reference/appkit/nswindow/1664726-window_levels
+[chrome-content-scripts]: https://developer.chrome.com/extensions/content_scripts#execution-environment

+ 3 - 1
docs/tutorial/security.md

@@ -55,7 +55,9 @@ This is not bulletproof, but at the least, you should attempt the following:
 
 * Only display secure (https) content
 * Disable the Node integration in all renderers that display remote content
-  (using `webPreferences`)
+  (setting `nodeIntegration` to `false` in `webPreferences`)
+* Enable context isolation in all rendererers that display remote content
+  (setting `contextIsolation` to `true` in `webPreferences`)
 * Do not disable `webSecurity`. Disabling it will disable the same-origin policy.
 * Define a [`Content-Security-Policy`](http://www.html5rocks.com/en/tutorials/security/content-security-policy/)
 , and use restrictive rules (i.e. `script-src 'self'`)

+ 22 - 2
electron.gyp

@@ -433,7 +433,7 @@
       ],
       'actions': [
         {
-          'action_name': 'atom_browserify',
+          'action_name': 'atom_browserify_sandbox',
           'inputs': [
             '<@(browserify_entries)',
           ],
@@ -450,7 +450,26 @@
             '-o',
             '<@(_outputs)',
           ],
-        }
+        },
+        {
+          'action_name': 'atom_browserify_isolated_context',
+          'inputs': [
+            '<@(isolated_context_browserify_entries)',
+          ],
+          'outputs': [
+            '<(js2c_input_dir)/isolated_bundle.js',
+          ],
+          'action': [
+            'npm',
+            'run',
+            '--silent',
+            'browserify',
+            '--',
+            'lib/isolated_renderer/init.js',
+            '-o',
+            '<@(_outputs)',
+          ],
+        },
       ],
     },  # target atom_browserify
     {
@@ -467,6 +486,7 @@
             # List all input files that should trigger a rebuild with js2c
             '<@(js2c_sources)',
             '<(js2c_input_dir)/preload_bundle.js',
+            '<(js2c_input_dir)/isolated_bundle.js',
           ],
           'outputs': [
             '<(SHARED_INTERMEDIATE_DIR)/atom_natives.h',

+ 5 - 0
filenames.gypi

@@ -56,6 +56,7 @@
       'lib/renderer/init.js',
       'lib/renderer/inspector.js',
       'lib/renderer/override.js',
+      'lib/renderer/window-setup.js',
       'lib/renderer/web-view/guest-view-internal.js',
       'lib/renderer/web-view/web-view.js',
       'lib/renderer/web-view/web-view-attributes.js',
@@ -76,6 +77,10 @@
       'lib/renderer/api/ipc-renderer-setup.js',
       'lib/sandboxed_renderer/init.js',
     ],
+    'isolated_context_browserify_entries': [
+      'lib/renderer/window-setup.js',
+      'lib/isolated_renderer/init.js',
+    ],
     'js2c_sources': [
       'lib/common/asar.js',
       'lib/common/asar_init.js',

+ 3 - 2
lib/browser/api/browser-window.js

@@ -26,7 +26,7 @@ BrowserWindow.prototype._init = function () {
       width: 800,
       height: 600
     }
-    ipcMain.emit('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_OPEN',
+    ipcMain.emit('ELECTRON_GUEST_WINDOW_MANAGER_INTERNAL_WINDOW_OPEN',
                  event, url, frameName, disposition,
                  options, additionalFeatures, postData)
   })
@@ -56,7 +56,8 @@ BrowserWindow.prototype._init = function () {
       height: height || 600,
       webContents: webContents
     }
-    ipcMain.emit('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_OPEN', event, url, frameName, disposition, options)
+    ipcMain.emit('ELECTRON_GUEST_WINDOW_MANAGER_INTERNAL_WINDOW_OPEN',
+                 event, url, frameName, disposition, options)
   })
 
   // window.resizeTo(...)

+ 72 - 2
lib/browser/guest-window-manager.js

@@ -2,6 +2,7 @@
 
 const {BrowserWindow, ipcMain, webContents} = require('electron')
 const {isSameOrigin} = process.atomBinding('v8_util')
+const parseFeaturesString = require('../common/parse-features-string')
 
 const hasProp = {}.hasOwnProperty
 const frameToGuest = {}
@@ -47,6 +48,11 @@ const mergeBrowserWindowOptions = function (embedder, options) {
     options.webPreferences.nodeIntegration = false
   }
 
+  // Enable context isolation on child window if enable on parent window
+  if (embedder.getWebPreferences().contextIsolation === true) {
+    options.webPreferences.contextIsolation = true
+  }
+
   // Sets correct openerId here to give correct options to 'new-window' event handler
   options.webPreferences.openerId = embedder.id
 
@@ -171,8 +177,68 @@ const canAccessWindow = function (sender, target) {
          isSameOrigin(sender.getURL(), target.getURL())
 }
 
-// Routed window.open messages.
-ipcMain.on('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_OPEN', function (event, url, frameName,
+// Routed window.open messages with raw options
+ipcMain.on('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_OPEN', (event, url, frameName, features) => {
+  if (url == null || url === '') url = 'about:blank'
+  if (frameName == null) frameName = ''
+  if (features == null) features = ''
+
+  const options = {}
+
+  const ints = ['x', 'y', 'width', 'height', 'minWidth', 'maxWidth', 'minHeight', 'maxHeight', 'zoomFactor']
+  const webPreferences = ['zoomFactor', 'nodeIntegration', 'preload']
+  const disposition = 'new-window'
+
+  // Used to store additional features
+  const additionalFeatures = []
+
+  // Parse the features
+  parseFeaturesString(features, function (key, value) {
+    if (value === undefined) {
+      additionalFeatures.push(key)
+    } else {
+      if (webPreferences.includes(key)) {
+        if (options.webPreferences == null) {
+          options.webPreferences = {}
+        }
+        options.webPreferences[key] = value
+      } else {
+        options[key] = value
+      }
+    }
+  })
+  if (options.left) {
+    if (options.x == null) {
+      options.x = options.left
+    }
+  }
+  if (options.top) {
+    if (options.y == null) {
+      options.y = options.top
+    }
+  }
+  if (options.title == null) {
+    options.title = frameName
+  }
+  if (options.width == null) {
+    options.width = 800
+  }
+  if (options.height == null) {
+    options.height = 600
+  }
+
+  for (const name of ints) {
+    if (options[name] != null) {
+      options[name] = parseInt(options[name], 10)
+    }
+  }
+
+  ipcMain.emit('ELECTRON_GUEST_WINDOW_MANAGER_INTERNAL_WINDOW_OPEN', event,
+               url, frameName, disposition, options, additionalFeatures)
+})
+
+// Routed window.open messages with fully parsed options
+ipcMain.on('ELECTRON_GUEST_WINDOW_MANAGER_INTERNAL_WINDOW_OPEN', function (event, url, frameName,
                                                                   disposition, options,
                                                                   additionalFeatures, postData) {
   options = mergeBrowserWindowOptions(event.sender, options)
@@ -224,6 +290,10 @@ ipcMain.on('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_METHOD', function (event, guest
 })
 
 ipcMain.on('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_POSTMESSAGE', function (event, guestId, message, targetOrigin, sourceOrigin) {
+  if (targetOrigin == null) {
+    targetOrigin = '*'
+  }
+
   const guestContents = webContents.fromId(guestId)
   if (guestContents == null) return
 

+ 26 - 0
lib/isolated_renderer/init.js

@@ -0,0 +1,26 @@
+/* global binding */
+
+'use strict'
+
+const {send, sendSync} = binding
+const {parse} = JSON
+
+const ipcRenderer = {
+  send (...args) {
+    return send('ipc-message', args)
+  },
+
+  sendSync (...args) {
+    return parse(sendSync('ipc-message-sync', args))
+  },
+
+  // No-ops since events aren't received
+  on () {},
+  once () {}
+}
+
+let {guestInstanceId, hiddenPage, openerId} = binding
+if (guestInstanceId != null) guestInstanceId = parseInt(guestInstanceId)
+if (openerId != null) openerId = parseInt(openerId)
+
+require('../renderer/window-setup')(ipcRenderer, guestInstanceId, openerId, hiddenPage)

+ 3 - 239
lib/renderer/override.js

@@ -1,244 +1,8 @@
 'use strict'
 
 const {ipcRenderer} = require('electron')
-const parseFeaturesString = require('../common/parse-features-string')
 
-const {defineProperty} = Object
+const {guestInstanceId, openerId} = process
+const hiddenPage = process.argv.includes('--hidden-page')
 
-// Helper function to resolve relative url.
-const a = window.top.document.createElement('a')
-const resolveURL = function (url) {
-  a.href = url
-  return a.href
-}
-
-// Window object returned by "window.open".
-const BrowserWindowProxy = (function () {
-  BrowserWindowProxy.proxies = {}
-
-  BrowserWindowProxy.getOrCreate = function (guestId) {
-    let proxy = this.proxies[guestId]
-    if (proxy == null) {
-      proxy = new BrowserWindowProxy(guestId)
-      this.proxies[guestId] = proxy
-    }
-    return proxy
-  }
-
-  BrowserWindowProxy.remove = function (guestId) {
-    delete this.proxies[guestId]
-  }
-
-  function BrowserWindowProxy (guestId1) {
-    defineProperty(this, 'guestId', {
-      configurable: false,
-      enumerable: true,
-      writeable: false,
-      value: guestId1
-    })
-
-    this.closed = false
-    ipcRenderer.once('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_CLOSED_' + this.guestId, () => {
-      BrowserWindowProxy.remove(this.guestId)
-      this.closed = true
-    })
-  }
-
-  BrowserWindowProxy.prototype.close = function () {
-    ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_CLOSE', this.guestId)
-  }
-
-  BrowserWindowProxy.prototype.focus = function () {
-    ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_METHOD', this.guestId, 'focus')
-  }
-
-  BrowserWindowProxy.prototype.blur = function () {
-    ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_METHOD', this.guestId, 'blur')
-  }
-
-  BrowserWindowProxy.prototype.print = function () {
-    ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD', this.guestId, 'print')
-  }
-
-  defineProperty(BrowserWindowProxy.prototype, 'location', {
-    get: function () {
-      return ipcRenderer.sendSync('ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD_SYNC', this.guestId, 'getURL')
-    },
-    set: function (url) {
-      url = resolveURL(url)
-      return ipcRenderer.sendSync('ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD_SYNC', this.guestId, 'loadURL', url)
-    }
-  })
-
-  BrowserWindowProxy.prototype.postMessage = function (message, targetOrigin) {
-    if (targetOrigin == null) {
-      targetOrigin = '*'
-    }
-    ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_POSTMESSAGE', this.guestId, message, targetOrigin, window.location.origin)
-  }
-
-  BrowserWindowProxy.prototype['eval'] = function (...args) {
-    ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD', this.guestId, 'executeJavaScript', ...args)
-  }
-
-  return BrowserWindowProxy
-})()
-
-if (process.guestInstanceId == null) {
-  // Override default window.close.
-  window.close = function () {
-    ipcRenderer.sendSync('ELECTRON_BROWSER_WINDOW_CLOSE')
-  }
-}
-
-// Make the browser window or guest view emit "new-window" event.
-window.open = function (url, frameName, features) {
-  let guestId, j, len1, name, options, additionalFeatures
-  if (frameName == null) {
-    frameName = ''
-  }
-  if (features == null) {
-    features = ''
-  }
-  options = {}
-
-  const ints = ['x', 'y', 'width', 'height', 'minWidth', 'maxWidth', 'minHeight', 'maxHeight', 'zoomFactor']
-  const webPreferences = ['zoomFactor', 'nodeIntegration', 'preload']
-  const disposition = 'new-window'
-
-  // Used to store additional features
-  additionalFeatures = []
-
-  // Parse the features
-  parseFeaturesString(features, function (key, value) {
-    if (value === undefined) {
-      additionalFeatures.push(key)
-    } else {
-      if (webPreferences.includes(key)) {
-        if (options.webPreferences == null) {
-          options.webPreferences = {}
-        }
-        options.webPreferences[key] = value
-      } else {
-        options[key] = value
-      }
-    }
-  })
-  if (options.left) {
-    if (options.x == null) {
-      options.x = options.left
-    }
-  }
-  if (options.top) {
-    if (options.y == null) {
-      options.y = options.top
-    }
-  }
-  if (options.title == null) {
-    options.title = frameName
-  }
-  if (options.width == null) {
-    options.width = 800
-  }
-  if (options.height == null) {
-    options.height = 600
-  }
-
-  // Resolve relative urls.
-  if (url == null || url === '') {
-    url = 'about:blank'
-  } else {
-    url = resolveURL(url)
-  }
-  for (j = 0, len1 = ints.length; j < len1; j++) {
-    name = ints[j]
-    if (options[name] != null) {
-      options[name] = parseInt(options[name], 10)
-    }
-  }
-  guestId = ipcRenderer.sendSync('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_OPEN', url, frameName, disposition, options, additionalFeatures)
-  if (guestId) {
-    return BrowserWindowProxy.getOrCreate(guestId)
-  } else {
-    return null
-  }
-}
-
-window.alert = function (message, title) {
-  ipcRenderer.sendSync('ELECTRON_BROWSER_WINDOW_ALERT', message, title)
-}
-
-window.confirm = function (message, title) {
-  return ipcRenderer.sendSync('ELECTRON_BROWSER_WINDOW_CONFIRM', message, title)
-}
-
-// But we do not support prompt().
-window.prompt = function () {
-  throw new Error('prompt() is and will not be supported.')
-}
-
-if (process.openerId != null) {
-  window.opener = BrowserWindowProxy.getOrCreate(process.openerId)
-}
-
-ipcRenderer.on('ELECTRON_GUEST_WINDOW_POSTMESSAGE', function (event, sourceId, message, sourceOrigin) {
-  // Manually dispatch event instead of using postMessage because we also need to
-  // set event.source.
-  event = document.createEvent('Event')
-  event.initEvent('message', false, false)
-  event.data = message
-  event.origin = sourceOrigin
-  event.source = BrowserWindowProxy.getOrCreate(sourceId)
-  window.dispatchEvent(event)
-})
-
-// Forward history operations to browser.
-const sendHistoryOperation = function (...args) {
-  ipcRenderer.send('ELECTRON_NAVIGATION_CONTROLLER', ...args)
-}
-
-const getHistoryOperation = function (...args) {
-  return ipcRenderer.sendSync('ELECTRON_SYNC_NAVIGATION_CONTROLLER', ...args)
-}
-
-window.history.back = function () {
-  sendHistoryOperation('goBack')
-}
-
-window.history.forward = function () {
-  sendHistoryOperation('goForward')
-}
-
-window.history.go = function (offset) {
-  sendHistoryOperation('goToOffset', offset)
-}
-
-defineProperty(window.history, 'length', {
-  get: function () {
-    return getHistoryOperation('length')
-  }
-})
-
-// The initial visibilityState.
-let cachedVisibilityState = process.argv.includes('--hidden-page') ? 'hidden' : 'visible'
-
-// Subscribe to visibilityState changes.
-ipcRenderer.on('ELECTRON_RENDERER_WINDOW_VISIBILITY_CHANGE', function (event, visibilityState) {
-  if (cachedVisibilityState !== visibilityState) {
-    cachedVisibilityState = visibilityState
-    document.dispatchEvent(new Event('visibilitychange'))
-  }
-})
-
-// Make document.hidden and document.visibilityState return the correct value.
-defineProperty(document, 'hidden', {
-  get: function () {
-    return cachedVisibilityState !== 'visible'
-  }
-})
-
-defineProperty(document, 'visibilityState', {
-  get: function () {
-    return cachedVisibilityState
-  }
-})
+require('./window-setup')(ipcRenderer, guestInstanceId, openerId, hiddenPage)

+ 192 - 0
lib/renderer/window-setup.js

@@ -0,0 +1,192 @@
+// This file should have no requires since it is used by the isolated context
+// preload bundle. Instead arguments should be passed in for everything it
+// needs.
+
+// This file implements the following APIs:
+// - window.alert()
+// - window.confirm()
+// - window.history.back()
+// - window.history.forward()
+// - window.history.go()
+// - window.history.length
+// - window.open()
+// - window.opener.blur()
+// - window.opener.close()
+// - window.opener.eval()
+// - window.opener.focus()
+// - window.opener.location
+// - window.opener.print()
+// - window.opener.postMessage()
+// - window.prompt()
+// - document.hidden
+// - document.visibilityState
+
+'use strict'
+
+const {defineProperty} = Object
+
+// Helper function to resolve relative url.
+const a = window.top.document.createElement('a')
+const resolveURL = function (url) {
+  a.href = url
+  return a.href
+}
+
+const windowProxies = {}
+
+const getOrCreateProxy = (ipcRenderer, guestId) => {
+  let proxy = windowProxies[guestId]
+  if (proxy == null) {
+    proxy = new BrowserWindowProxy(ipcRenderer, guestId)
+    windowProxies[guestId] = proxy
+  }
+  return proxy
+}
+
+const removeProxy = (guestId) => {
+  delete windowProxies[guestId]
+}
+
+function BrowserWindowProxy (ipcRenderer, guestId) {
+  this.closed = false
+
+  defineProperty(this, 'location', {
+    get: function () {
+      return ipcRenderer.sendSync('ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD_SYNC', guestId, 'getURL')
+    },
+    set: function (url) {
+      url = resolveURL(url)
+      return ipcRenderer.sendSync('ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD_SYNC', guestId, 'loadURL', url)
+    }
+  })
+
+  ipcRenderer.once(`ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_CLOSED_${guestId}`, () => {
+    removeProxy(guestId)
+    this.closed = true
+  })
+
+  this.close = () => {
+    ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_CLOSE', guestId)
+  }
+
+  this.focus = () => {
+    ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_METHOD', guestId, 'focus')
+  }
+
+  this.blur = () => {
+    ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_METHOD', guestId, 'blur')
+  }
+
+  this.print = () => {
+    ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD', guestId, 'print')
+  }
+
+  this.postMessage = (message, targetOrigin) => {
+    ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_POSTMESSAGE', guestId, message, targetOrigin, window.location.origin)
+  }
+
+  this.eval = (...args) => {
+    ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD', guestId, 'executeJavaScript', ...args)
+  }
+}
+
+// Forward history operations to browser.
+const sendHistoryOperation = function (ipcRenderer, ...args) {
+  ipcRenderer.send('ELECTRON_NAVIGATION_CONTROLLER', ...args)
+}
+
+const getHistoryOperation = function (ipcRenderer, ...args) {
+  return ipcRenderer.sendSync('ELECTRON_SYNC_NAVIGATION_CONTROLLER', ...args)
+}
+
+module.exports = (ipcRenderer, guestInstanceId, openerId, hiddenPage) => {
+  if (guestInstanceId == null) {
+    // Override default window.close.
+    window.close = function () {
+      ipcRenderer.sendSync('ELECTRON_BROWSER_WINDOW_CLOSE')
+    }
+  }
+
+  // Make the browser window or guest view emit "new-window" event.
+  window.open = function (url, frameName, features) {
+    if (url != null && url !== '') {
+      url = resolveURL(url)
+    }
+    const guestId = ipcRenderer.sendSync('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_OPEN', url, frameName, features)
+    if (guestId != null) {
+      return getOrCreateProxy(ipcRenderer, guestId)
+    } else {
+      return null
+    }
+  }
+
+  window.alert = function (message, title) {
+    ipcRenderer.sendSync('ELECTRON_BROWSER_WINDOW_ALERT', message, title)
+  }
+
+  window.confirm = function (message, title) {
+    return ipcRenderer.sendSync('ELECTRON_BROWSER_WINDOW_CONFIRM', message, title)
+  }
+
+  // But we do not support prompt().
+  window.prompt = function () {
+    throw new Error('prompt() is and will not be supported.')
+  }
+
+  if (openerId != null) {
+    window.opener = getOrCreateProxy(ipcRenderer, openerId)
+  }
+
+  ipcRenderer.on('ELECTRON_GUEST_WINDOW_POSTMESSAGE', function (event, sourceId, message, sourceOrigin) {
+    // Manually dispatch event instead of using postMessage because we also need to
+    // set event.source.
+    event = document.createEvent('Event')
+    event.initEvent('message', false, false)
+    event.data = message
+    event.origin = sourceOrigin
+    event.source = getOrCreateProxy(ipcRenderer, sourceId)
+    window.dispatchEvent(event)
+  })
+
+  window.history.back = function () {
+    sendHistoryOperation(ipcRenderer, 'goBack')
+  }
+
+  window.history.forward = function () {
+    sendHistoryOperation(ipcRenderer, 'goForward')
+  }
+
+  window.history.go = function (offset) {
+    sendHistoryOperation(ipcRenderer, 'goToOffset', offset)
+  }
+
+  defineProperty(window.history, 'length', {
+    get: function () {
+      return getHistoryOperation(ipcRenderer, 'length')
+    }
+  })
+
+  // The initial visibilityState.
+  let cachedVisibilityState = hiddenPage ? 'hidden' : 'visible'
+
+  // Subscribe to visibilityState changes.
+  ipcRenderer.on('ELECTRON_RENDERER_WINDOW_VISIBILITY_CHANGE', function (event, visibilityState) {
+    if (cachedVisibilityState !== visibilityState) {
+      cachedVisibilityState = visibilityState
+      document.dispatchEvent(new Event('visibilitychange'))
+    }
+  })
+
+  // Make document.hidden and document.visibilityState return the correct value.
+  defineProperty(document, 'hidden', {
+    get: function () {
+      return cachedVisibilityState !== 'visible'
+    }
+  })
+
+  defineProperty(document, 'visibilityState', {
+    get: function () {
+      return cachedVisibilityState
+    }
+  })
+}

+ 1 - 1
script/lib/config.py

@@ -9,7 +9,7 @@ import sys
 BASE_URL = os.getenv('LIBCHROMIUMCONTENT_MIRROR') or \
     'https://s3.amazonaws.com/github-janky-artifacts/libchromiumcontent'
 LIBCHROMIUMCONTENT_COMMIT = os.getenv('LIBCHROMIUMCONTENT_COMMIT') or \
-    '2c8173b64b7fbc50e7190a6982e6db6b3eda0582'
+    'f14fb5fb9cb3c3a57a2ac1a9725fd9373ef043d2'
 
 PLATFORM = {
   'cygwin': 'win32',

+ 63 - 0
spec/api-browser-window-spec.js

@@ -1835,6 +1835,69 @@ describe('BrowserWindow module', function () {
     })
   })
 
+  describe('contextIsolation option', () => {
+    const expectedContextData = {
+      preloadContext: {
+        preloadProperty: 'number',
+        pageProperty: 'undefined',
+        typeofRequire: 'function',
+        typeofProcess: 'object',
+        typeofArrayPush: 'function',
+        typeofFunctionApply: 'function'
+      },
+      pageContext: {
+        preloadProperty: 'undefined',
+        pageProperty: 'string',
+        typeofRequire: 'undefined',
+        typeofProcess: 'undefined',
+        typeofArrayPush: 'number',
+        typeofFunctionApply: 'boolean',
+        typeofPreloadExecuteJavaScriptProperty: 'number',
+        typeofOpenedWindow: 'object',
+        documentHidden: true,
+        documentVisibilityState: 'hidden'
+      }
+    }
+
+    beforeEach(() => {
+      if (w != null) w.destroy()
+      w = new BrowserWindow({
+        show: false,
+        webPreferences: {
+          contextIsolation: true,
+          preload: path.join(fixtures, 'api', 'isolated-preload.js')
+        }
+      })
+    })
+
+    it('separates the page context from the Electron/preload context', (done) => {
+      ipcMain.once('isolated-world', (event, data) => {
+        assert.deepEqual(data, expectedContextData)
+        done()
+      })
+      w.loadURL('file://' + fixtures + '/api/isolated.html')
+    })
+
+    it('recreates the contexts on reload', (done) => {
+      w.webContents.once('did-finish-load', () => {
+        ipcMain.once('isolated-world', (event, data) => {
+          assert.deepEqual(data, expectedContextData)
+          done()
+        })
+        w.webContents.reload()
+      })
+      w.loadURL('file://' + fixtures + '/api/isolated.html')
+    })
+
+    it('enables context isolation on child windows', function (done) {
+      app.once('browser-window-created', function (event, window) {
+        assert.equal(window.webContents.getWebPreferences().contextIsolation, true)
+        done()
+      })
+      w.loadURL('file://' + fixtures + '/pages/window-open.html')
+    })
+  })
+
   describe('offscreen rendering', function () {
     beforeEach(function () {
       if (w != null) w.destroy()

+ 43 - 38
spec/chromium-spec.js

@@ -6,7 +6,7 @@ const url = require('url')
 const {ipcRenderer, remote} = require('electron')
 const {closeWindow} = require('./window-helpers')
 
-const {BrowserWindow, ipcMain, protocol, session, webContents} = remote
+const {app, BrowserWindow, ipcMain, protocol, session, webContents} = remote
 
 const isCI = remote.getGlobal('isCi')
 
@@ -197,12 +197,6 @@ describe('chromium feature', function () {
       var b = window.open('about:blank', '', 'show=no')
       assert.equal(b.closed, false)
       assert.equal(b.constructor.name, 'BrowserWindowProxy')
-
-      // Check that guestId is not writeable
-      assert(b.guestId)
-      b.guestId = 'anotherValue'
-      assert.notEqual(b.guestId, 'anoterValue')
-
       b.close()
     })
 
@@ -295,43 +289,54 @@ describe('chromium feature', function () {
       } else {
         targetURL = 'file://' + fixtures + '/pages/base-page.html'
       }
-      b = window.open(targetURL)
-      webContents.fromId(b.guestId).once('did-finish-load', function () {
-        assert.equal(b.location, targetURL)
-        b.close()
-        done()
+      app.once('browser-window-created', (event, window) => {
+        window.webContents.once('did-finish-load', () => {
+          assert.equal(b.location, targetURL)
+          b.close()
+          done()
+        })
       })
+      b = window.open(targetURL)
     })
 
     it('defines a window.location setter', function (done) {
-      // Load a page that definitely won't redirect
-      var b = window.open('about:blank')
-      webContents.fromId(b.guestId).once('did-finish-load', function () {
-        // When it loads, redirect
-        b.location = 'file://' + fixtures + '/pages/base-page.html'
-        webContents.fromId(b.guestId).once('did-finish-load', function () {
-          // After our second redirect, cleanup and callback
-          b.close()
-          done()
+      let b
+      app.once('browser-window-created', (event, {webContents}) => {
+        webContents.once('did-finish-load', function () {
+          // When it loads, redirect
+          b.location = 'file://' + fixtures + '/pages/base-page.html'
+          webContents.once('did-finish-load', function () {
+            // After our second redirect, cleanup and callback
+            b.close()
+            done()
+          })
         })
       })
+      // Load a page that definitely won't redirect
+      b = window.open('about:blank')
     })
 
     it('open a blank page when no URL is specified', function (done) {
-      let b = window.open()
-      webContents.fromId(b.guestId).once('did-finish-load', function () {
-        const {location} = b
-        b.close()
-        assert.equal(location, 'about:blank')
-
-        let c = window.open('')
-        webContents.fromId(c.guestId).once('did-finish-load', function () {
-          const {location} = c
-          c.close()
+      let b
+      app.once('browser-window-created', (event, {webContents}) => {
+        webContents.once('did-finish-load', function () {
+          const {location} = b
+          b.close()
           assert.equal(location, 'about:blank')
-          done()
+
+          let c
+          app.once('browser-window-created', (event, {webContents}) => {
+            webContents.once('did-finish-load', function () {
+              const {location} = c
+              c.close()
+              assert.equal(location, 'about:blank')
+              done()
+            })
+          })
+          c = window.open('')
         })
       })
+      b = window.open()
     })
   })
 
@@ -496,8 +501,7 @@ describe('chromium feature', function () {
 
   describe('window.postMessage', function () {
     it('sets the source and origin correctly', function (done) {
-      var b, sourceId
-      sourceId = remote.getCurrentWindow().id
+      var b
       listener = function (event) {
         window.removeEventListener('message', listener)
         b.close()
@@ -505,15 +509,16 @@ describe('chromium feature', function () {
         assert.equal(message.data, 'testing')
         assert.equal(message.origin, 'file://')
         assert.equal(message.sourceEqualsOpener, true)
-        assert.equal(message.sourceId, sourceId)
         assert.equal(event.origin, 'file://')
         done()
       }
       window.addEventListener('message', listener)
-      b = window.open('file://' + fixtures + '/pages/window-open-postMessage.html', '', 'show=no')
-      webContents.fromId(b.guestId).once('did-finish-load', function () {
-        b.postMessage('testing', '*')
+      app.once('browser-window-created', (event, {webContents}) => {
+        webContents.once('did-finish-load', function () {
+          b.postMessage('testing', '*')
+        })
       })
+      b = window.open('file://' + fixtures + '/pages/window-open-postMessage.html', '', 'show=no')
     })
   })
 

+ 19 - 0
spec/fixtures/api/isolated-preload.js

@@ -0,0 +1,19 @@
+const {ipcRenderer, webFrame} = require('electron')
+
+window.foo = 3
+
+webFrame.executeJavaScript('window.preloadExecuteJavaScriptProperty = 1234;')
+
+window.addEventListener('message', (event) => {
+  ipcRenderer.send('isolated-world', {
+    preloadContext: {
+      preloadProperty: typeof window.foo,
+      pageProperty: typeof window.hello,
+      typeofRequire: typeof require,
+      typeofProcess: typeof process,
+      typeofArrayPush: typeof Array.prototype.push,
+      typeofFunctionApply: typeof Function.prototype.apply
+    },
+    pageContext: event.data
+  })
+})

+ 31 - 0
spec/fixtures/api/isolated.html

@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <title>Isolated World</title>
+    <script>
+      window.hello = 'world'
+      Array.prototype.push = 3
+      Function.prototype.apply = true
+
+      const opened = window.open()
+      opened.close()
+
+      window.postMessage({
+        preloadProperty: typeof window.foo,
+        pageProperty: typeof window.hello,
+        typeofRequire: typeof require,
+        typeofProcess: typeof process,
+        typeofArrayPush: typeof Array.prototype.push,
+        typeofFunctionApply: typeof Function.prototype.apply,
+        typeofPreloadExecuteJavaScriptProperty: typeof window.preloadExecuteJavaScriptProperty,
+        typeofOpenedWindow: typeof opened,
+        documentHidden: document.hidden,
+        documentVisibilityState: document.visibilityState
+      }, '*')
+    </script>
+  </head>
+  <body>
+
+  </body>
+</html>

+ 1 - 2
spec/fixtures/pages/window-open-postMessage.html

@@ -5,8 +5,7 @@
     window.opener.postMessage(JSON.stringify({
       origin: e.origin,
       data: e.data,
-      sourceEqualsOpener: e.source === window.opener,
-      sourceId: e.source.guestId
+      sourceEqualsOpener: e.source === window.opener
     }), '*');
   });
 </script>

+ 38 - 1
spec/webview-spec.js

@@ -2,9 +2,12 @@ const assert = require('assert')
 const path = require('path')
 const http = require('http')
 const url = require('url')
-const {app, session, getGuestWebContents, ipcMain, BrowserWindow, webContents} = require('electron').remote
+const {remote} = require('electron')
+const {app, session, getGuestWebContents, ipcMain, BrowserWindow, webContents} = remote
 const {closeWindow} = require('./window-helpers')
 
+const isCI = remote.getGlobal('isCi')
+
 describe('<webview> tag', function () {
   this.timeout(3 * 60 * 1000)
 
@@ -429,6 +432,40 @@ describe('<webview> tag', function () {
       webview.src = 'data:text/html;base64,' + encoded
       document.body.appendChild(webview)
     })
+
+    it('can enable context isolation', (done) => {
+      ipcMain.once('isolated-world', (event, data) => {
+        assert.deepEqual(data, {
+          preloadContext: {
+            preloadProperty: 'number',
+            pageProperty: 'undefined',
+            typeofRequire: 'function',
+            typeofProcess: 'object',
+            typeofArrayPush: 'function',
+            typeofFunctionApply: 'function'
+          },
+          pageContext: {
+            preloadProperty: 'undefined',
+            pageProperty: 'string',
+            typeofRequire: 'undefined',
+            typeofProcess: 'undefined',
+            typeofArrayPush: 'number',
+            typeofFunctionApply: 'boolean',
+            typeofPreloadExecuteJavaScriptProperty: 'number',
+            typeofOpenedWindow: 'object',
+            documentHidden: isCI,
+            documentVisibilityState: isCI ? 'hidden' : 'visible'
+          }
+        })
+        done()
+      })
+
+      webview.setAttribute('preload', path.join(fixtures, 'api', 'isolated-preload.js'))
+      webview.setAttribute('allowpopups', 'yes')
+      webview.setAttribute('webpreferences', 'contextIsolation=yes')
+      webview.src = 'file://' + fixtures + '/api/isolated.html'
+      document.body.appendChild(webview)
+    })
   })
 
   describe('new-window event', function () {