fix: window.open not overriding parent's webPreferences (#32057)

* fix: window.open not overriding parent's webPreferences

* test: remove "nativeWindowOpen: false" from renderer tests
This commit is contained in:
Cheng Zhao 2021-12-06 12:54:14 +09:00 committed by GitHub
parent 77287febf4
commit 35ac7fb8e6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 98 additions and 33 deletions

View file

@ -64,6 +64,9 @@ window.open('https://github.com', '_blank', 'top=500,left=200,frame=false,nodeIn
`features` will be passed to any registered `webContents`'s
`did-create-window` event handler in the `options` argument.
* `frameName` follows the specification of `windowName` located in the [native documentation](https://developer.mozilla.org/en-US/docs/Web/API/Window/open#parameters).
* When opening `about:blank`, the child window's `WebPreferences` will be copied
from the parent window, and there is no way to override it because Chromium
skips browser side navigation in this case.
To customize or cancel the creation of the window, you can optionally set an
override handler with `webContents.setWindowOpenHandler()` from the main

View file

@ -680,16 +680,6 @@ WebContents.prototype._init = function () {
postBody
};
windowOpenOverriddenOptions = this._callWindowOpenHandler(event, details);
// if attempting to use this API with the deprecated new-window event,
// windowOpenOverriddenOptions will always return null. This ensures
// short-term backwards compatibility until new-window is removed.
const parsedFeatures = parseFeatures(rawFeatures);
const overriddenFeatures: BrowserWindowConstructorOptions = {
...parsedFeatures.options,
webPreferences: parsedFeatures.webPreferences
};
windowOpenOverriddenOptions = windowOpenOverriddenOptions || overriddenFeatures;
if (!event.defaultPrevented) {
const secureOverrideWebPreferences = windowOpenOverriddenOptions ? {
// Allow setting of backgroundColor as a webPreference even though
@ -699,9 +689,19 @@ WebContents.prototype._init = function () {
transparent: windowOpenOverriddenOptions.transparent,
...windowOpenOverriddenOptions.webPreferences
} : undefined;
this._setNextChildWebPreferences(
makeWebPreferences({ embedder: event.sender, secureOverrideWebPreferences })
);
// TODO(zcbenz): The features string is parsed twice: here where it is
// passed to C++, and in |makeBrowserWindowOptions| later where it is
// not actually used since the WebContents is created here.
// We should be able to remove the latter once the |nativeWindowOpen|
// option is removed.
const { webPreferences: parsedWebPreferences } = parseFeatures(rawFeatures);
// Parameters should keep same with |makeBrowserWindowOptions|.
const webPreferences = makeWebPreferences({
embedder: event.sender,
insecureParsedWebPreferences: parsedWebPreferences,
secureOverrideWebPreferences
});
this._setNextChildWebPreferences(webPreferences);
}
});

View file

@ -217,6 +217,10 @@ function makeBrowserWindowOptions ({ embedder, features, overrideOptions }: {
height: 600,
...parsedOptions,
...overrideOptions,
// Note that for |nativeWindowOpen: true| the WebContents is created in
// |api::WebContents::WebContentsCreatedWithFullParams|, with prefs
// parsed in the |-will-add-new-contents| event.
// The |webPreferences| here is only used by |nativeWindowOpen: false|.
webPreferences: makeWebPreferences({
embedder,
insecureParsedWebPreferences: parsedWebPreferences,

View file

@ -500,6 +500,14 @@ class WebFrameRenderer : public gin::Wrappable<WebFrameRenderer>,
// NOTE: openerId is internal-only.
return gin::ConvertToV8(isolate, prefs.opener_id);
} else if (pref_name == "isWebView") {
// FIXME(zcbenz): For child windows opened with window.open('') from
// webview, the WebPreferences is inherited from webview and the value
// of |is_webview| is wrong.
// Please check ElectronRenderFrameObserver::DidInstallConditionalFeatures
// for the background.
auto* web_frame = render_frame->GetWebFrame();
if (web_frame->Opener())
return gin::ConvertToV8(isolate, false);
return gin::ConvertToV8(isolate, prefs.is_webview);
} else if (pref_name == options::kHiddenPage) {
// NOTE: hiddenPage is internal-only.

View file

@ -31,6 +31,7 @@
#include "third_party/blink/public/web/web_element.h"
#include "third_party/blink/public/web/web_local_frame.h"
#include "third_party/blink/public/web/web_script_source.h"
#include "third_party/blink/renderer/core/frame/web_local_frame_impl.h" // nogncheck
#include "ui/base/resource/resource_bundle.h"
namespace electron {
@ -58,12 +59,57 @@ ElectronRenderFrameObserver::ElectronRenderFrameObserver(
}
void ElectronRenderFrameObserver::DidClearWindowObject() {
// Do a delayed Node.js initialization for child window.
// Check DidInstallConditionalFeatures below for the background.
auto* web_frame =
static_cast<blink::WebLocalFrameImpl*>(render_frame_->GetWebFrame());
if (has_delayed_node_initialization_ && web_frame->Opener() &&
!web_frame->IsOnInitialEmptyDocument()) {
v8::Isolate* isolate = blink::MainThreadIsolate();
v8::HandleScope handle_scope(isolate);
v8::MicrotasksScope microtasks_scope(
isolate, v8::MicrotasksScope::kDoNotRunMicrotasks);
v8::Handle<v8::Context> context = web_frame->MainWorldScriptContext();
v8::Context::Scope context_scope(context);
// DidClearWindowObject only emits for the main world.
DidInstallConditionalFeatures(context, MAIN_WORLD_ID);
}
renderer_client_->DidClearWindowObject(render_frame_);
}
void ElectronRenderFrameObserver::DidInstallConditionalFeatures(
v8::Handle<v8::Context> context,
int world_id) {
// When a child window is created with window.open, its WebPreferences will
// be copied from its parent, and Chromium will intialize JS context in it
// immediately.
// Normally the WebPreferences is overriden in browser before navigation,
// but this behavior bypasses the browser side navigation and the child
// window will get wrong WebPreferences in the initialization.
// This will end up initializing Node.js in the child window with wrong
// WebPreferences, leads to problem that child window having node integration
// while "nodeIntegration=no" is passed.
// We work around this issue by delaying the child window's initialization of
// Node.js if this is the initial empty document, and only do it when the
// acutal page has started to load.
auto* web_frame =
static_cast<blink::WebLocalFrameImpl*>(render_frame_->GetWebFrame());
if (web_frame->Opener() && web_frame->IsOnInitialEmptyDocument()) {
// FIXME(zcbenz): Chromium does not do any browser side navigation for
// window.open('about:blank'), so there is no way to override WebPreferences
// of it. We should not delay Node.js initialization as there will be no
// further loadings.
// Please check http://crbug.com/1215096 for updates which may help remove
// this hack.
GURL url = web_frame->GetDocument().Url();
if (!url.IsAboutBlank()) {
has_delayed_node_initialization_ = true;
return;
}
}
has_delayed_node_initialization_ = false;
auto* isolate = context->GetIsolate();
v8::MicrotasksScope microtasks_scope(
isolate, v8::MicrotasksScope::kDoNotRunMicrotasks);

View file

@ -44,6 +44,7 @@ class ElectronRenderFrameObserver : public content::RenderFrameObserver {
void OnTakeHeapSnapshot(IPC::PlatformFileForTransit file_handle,
const std::string& channel);
bool has_delayed_node_initialization_ = false;
content::RenderFrame* render_frame_;
RendererClientBase* renderer_client_;
};

View file

@ -157,6 +157,7 @@ void ElectronRendererClient::WillReleaseScriptContext(
// We also do this if we have disable electron site instance overrides to
// avoid memory leaks
auto prefs = render_frame->GetBlinkPreferences();
gin_helper::MicrotasksScope microtasks_scope(env->isolate());
node::FreeEnvironment(env);
if (env == node_bindings_->uv_env())
node::FreeIsolateData(node_bindings_->isolate_data());

View file

@ -37,6 +37,8 @@ WebWorkerObserver::WebWorkerObserver()
WebWorkerObserver::~WebWorkerObserver() {
lazy_tls.Pointer()->Set(nullptr);
gin_helper::MicrotasksScope microtasks_scope(
node_bindings_->uv_env()->isolate());
node::FreeEnvironment(node_bindings_->uv_env());
node::FreeIsolateData(node_bindings_->isolate_data());
}

View file

@ -814,6 +814,17 @@ describe('chromium features', () => {
expect(typeofProcessGlobal).to.equal('undefined');
});
it('can disable node integration when it is enabled on the parent window with nativeWindowOpen: true', async () => {
const w = new BrowserWindow({ show: false, webPreferences: { nodeIntegration: true, nativeWindowOpen: true } });
w.loadURL('about:blank');
w.webContents.executeJavaScript(`
{ b = window.open('about:blank', '', 'nodeIntegration=no,show=no'); null }
`);
const [, contents] = await emittedOnce(app, 'web-contents-created');
const typeofProcessGlobal = await contents.executeJavaScript('typeof process');
expect(typeofProcessGlobal).to.equal('undefined');
});
it('disables JavaScript when it is disabled on the parent window', async () => {
const w = new BrowserWindow({ show: true, webPreferences: { nodeIntegration: true } });
w.webContents.loadFile(path.resolve(__dirname, 'fixtures', 'blank.html'));

View file

@ -1,10 +1,12 @@
<html>
<body>
<script>
// `setImmediate` schedules a function to be run on the next UV tick, which
// ensures that `window.open` is run from `UvRunOnce()`.
setImmediate(async () => {
if (!location.hash) {
if (location.hash) {
window.opener.postMessage('foo', '*');
} else {
// `setImmediate` schedules a function to be run on the next UV tick, which
// ensures that `window.open` is run from `UvRunOnce()`.
setImmediate(async () => {
const p = new Promise(resolve => {
window.addEventListener('message', resolve, { once: true });
});
@ -12,10 +14,8 @@ setImmediate(async () => {
const e = await p;
window.close();
} else {
window.opener.postMessage('foo', '*');
}
});
});
}
</script>
</body>
</html>

View file

@ -154,16 +154,6 @@ describe('chromium feature', () => {
});
});
describe('window.postMessage', () => {
it('throws an exception when the targetOrigin cannot be converted to a string', () => {
const b = window.open('');
expect(() => {
b.postMessage('test', { toString: null });
}).to.throw('Cannot convert object to primitive value');
b.close();
});
});
describe('window.opener.postMessage', () => {
it('sets source and origin correctly', async () => {
const message = waitForEvent(window, 'message');

View file

@ -109,8 +109,7 @@ app.whenReady().then(async function () {
backgroundThrottling: false,
nodeIntegration: true,
webviewTag: true,
contextIsolation: false,
nativeWindowOpen: false
contextIsolation: false
}
});
window.loadFile('static/index.html', {