зеркало из https://github.com/electron/electron.git
feat: Add content script world isolation (#17032)
* Execute content script in isolated world * Inject script into newly created extension worlds * Create new content_script_bundle for extension scripts * Initialize chrome API in content script bundle * Define Chrome extension isolated world ID range 1 << 20 was chosen as it provides a sufficiently large range of IDs for extensions, but also provides a large enough buffer for any user worlds in [1000, 1 << 20). Ultimately this range can be changed if any user application raises it as an issue. * Insert content script CSS into document This now avoids a script wrapper to inject the style sheet. This closely matches the code used by chromium in `ScriptInjection::InjectCss`. * Pass extension ID to isolated world via v8 private
This commit is contained in:
Родитель
6072da239d
Коммит
f943db7ad5
33
BUILD.gn
33
BUILD.gn
|
@ -137,6 +137,37 @@ npm_action("atom_browserify_isolated") {
|
|||
]
|
||||
}
|
||||
|
||||
npm_action("atom_browserify_content_script") {
|
||||
script = "browserify"
|
||||
deps = [
|
||||
":build_electron_definitions",
|
||||
]
|
||||
|
||||
inputs = [
|
||||
"lib/content_script/init.js",
|
||||
"tsconfig.electron.json",
|
||||
"tsconfig.json",
|
||||
]
|
||||
|
||||
outputs = [
|
||||
"$target_gen_dir/js2c/content_script_bundle.js",
|
||||
]
|
||||
|
||||
args = [
|
||||
"lib/content_script/init.js",
|
||||
"-t",
|
||||
"aliasify",
|
||||
"-p",
|
||||
"[",
|
||||
"tsify",
|
||||
"-p",
|
||||
"tsconfig.electron.json",
|
||||
"]",
|
||||
"-o",
|
||||
rebase_path(outputs[0]),
|
||||
]
|
||||
}
|
||||
|
||||
copy("atom_js2c_copy") {
|
||||
sources = [
|
||||
"lib/common/asar.js",
|
||||
|
@ -149,12 +180,14 @@ copy("atom_js2c_copy") {
|
|||
|
||||
action("atom_js2c") {
|
||||
deps = [
|
||||
":atom_browserify_content_script",
|
||||
":atom_browserify_isolated",
|
||||
":atom_browserify_sandbox",
|
||||
":atom_js2c_copy",
|
||||
]
|
||||
|
||||
browserify_sources = [
|
||||
"$target_gen_dir/js2c/content_script_bundle.js",
|
||||
"$target_gen_dir/js2c/isolated_bundle.js",
|
||||
"$target_gen_dir/js2c/preload_bundle.js",
|
||||
]
|
||||
|
|
|
@ -113,6 +113,12 @@ void AtomRenderFrameObserver::DidCreateScriptContext(
|
|||
CreateIsolatedWorldContext();
|
||||
renderer_client_->SetupMainWorldOverrides(context, render_frame_);
|
||||
}
|
||||
|
||||
if (world_id >= World::ISOLATED_WORLD_EXTENSIONS &&
|
||||
world_id <= World::ISOLATED_WORLD_EXTENSIONS_END) {
|
||||
renderer_client_->SetupExtensionWorldOverrides(context, render_frame_,
|
||||
world_id);
|
||||
}
|
||||
}
|
||||
|
||||
void AtomRenderFrameObserver::DraggableRegionsChanged() {
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
#include "base/strings/string16.h"
|
||||
#include "content/public/renderer/render_frame_observer.h"
|
||||
#include "ipc/ipc_platform_file.h"
|
||||
#include "third_party/blink/public/platform/web_isolated_world_ids.h"
|
||||
#include "third_party/blink/public/web/web_local_frame.h"
|
||||
|
||||
namespace base {
|
||||
|
@ -21,9 +22,19 @@ namespace atom {
|
|||
|
||||
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
|
||||
ISOLATED_WORLD = 999,
|
||||
|
||||
// Numbers for isolated worlds for extensions are set in
|
||||
// lib/renderer/content-script-injector.ts, and are greater than or equal to
|
||||
// this number, up to ISOLATED_WORLD_EXTENSIONS_END.
|
||||
ISOLATED_WORLD_EXTENSIONS = 1 << 20,
|
||||
|
||||
// Last valid isolated world ID.
|
||||
ISOLATED_WORLD_EXTENSIONS_END =
|
||||
blink::IsolatedWorldId::kEmbedderWorldIdLimit - 1
|
||||
};
|
||||
|
||||
// Helper class to forward the messages to the client.
|
||||
|
|
|
@ -210,6 +210,27 @@ void AtomRendererClient::SetupMainWorldOverrides(
|
|||
&isolated_bundle_args, nullptr);
|
||||
}
|
||||
|
||||
void AtomRendererClient::SetupExtensionWorldOverrides(
|
||||
v8::Handle<v8::Context> context,
|
||||
content::RenderFrame* render_frame,
|
||||
int world_id) {
|
||||
auto* isolate = context->GetIsolate();
|
||||
|
||||
std::vector<v8::Local<v8::String>> isolated_bundle_params = {
|
||||
node::FIXED_ONE_BYTE_STRING(isolate, "nodeProcess"),
|
||||
node::FIXED_ONE_BYTE_STRING(isolate, "isolatedWorld"),
|
||||
node::FIXED_ONE_BYTE_STRING(isolate, "worldId")};
|
||||
|
||||
std::vector<v8::Local<v8::Value>> isolated_bundle_args = {
|
||||
GetEnvironment(render_frame)->process_object(),
|
||||
GetContext(render_frame->GetWebFrame(), isolate)->Global(),
|
||||
v8::Integer::New(isolate, world_id)};
|
||||
|
||||
node::per_process::native_module_loader.CompileAndCall(
|
||||
context, "electron/js2c/content_script_bundle", &isolated_bundle_params,
|
||||
&isolated_bundle_args, nullptr);
|
||||
}
|
||||
|
||||
node::Environment* AtomRendererClient::GetEnvironment(
|
||||
content::RenderFrame* render_frame) const {
|
||||
if (injected_frames_.find(render_frame) == injected_frames_.end())
|
||||
|
|
|
@ -33,6 +33,9 @@ class AtomRendererClient : public RendererClientBase {
|
|||
content::RenderFrame* render_frame) override;
|
||||
void SetupMainWorldOverrides(v8::Handle<v8::Context> context,
|
||||
content::RenderFrame* render_frame) override;
|
||||
void SetupExtensionWorldOverrides(v8::Handle<v8::Context> context,
|
||||
content::RenderFrame* render_frame,
|
||||
int world_id) override;
|
||||
|
||||
private:
|
||||
// content::ContentRendererClient:
|
||||
|
|
|
@ -270,6 +270,30 @@ void AtomSandboxedRendererClient::SetupMainWorldOverrides(
|
|||
&isolated_bundle_args, nullptr);
|
||||
}
|
||||
|
||||
void AtomSandboxedRendererClient::SetupExtensionWorldOverrides(
|
||||
v8::Handle<v8::Context> context,
|
||||
content::RenderFrame* render_frame,
|
||||
int world_id) {
|
||||
auto* isolate = context->GetIsolate();
|
||||
|
||||
mate::Dictionary process = mate::Dictionary::CreateEmpty(isolate);
|
||||
process.SetMethod("binding", GetBinding);
|
||||
|
||||
std::vector<v8::Local<v8::String>> isolated_bundle_params = {
|
||||
node::FIXED_ONE_BYTE_STRING(isolate, "nodeProcess"),
|
||||
node::FIXED_ONE_BYTE_STRING(isolate, "isolatedWorld"),
|
||||
node::FIXED_ONE_BYTE_STRING(isolate, "worldId")};
|
||||
|
||||
std::vector<v8::Local<v8::Value>> isolated_bundle_args = {
|
||||
process.GetHandle(),
|
||||
GetContext(render_frame->GetWebFrame(), isolate)->Global(),
|
||||
v8::Integer::New(isolate, world_id)};
|
||||
|
||||
node::per_process::native_module_loader.CompileAndCall(
|
||||
context, "electron/js2c/content_script_bundle", &isolated_bundle_params,
|
||||
&isolated_bundle_args, nullptr);
|
||||
}
|
||||
|
||||
void AtomSandboxedRendererClient::WillReleaseScriptContext(
|
||||
v8::Handle<v8::Context> context,
|
||||
content::RenderFrame* render_frame) {
|
||||
|
|
|
@ -32,6 +32,9 @@ class AtomSandboxedRendererClient : public RendererClientBase {
|
|||
content::RenderFrame* render_frame) override;
|
||||
void SetupMainWorldOverrides(v8::Handle<v8::Context> context,
|
||||
content::RenderFrame* render_frame) override;
|
||||
void SetupExtensionWorldOverrides(v8::Handle<v8::Context> context,
|
||||
content::RenderFrame* render_frame,
|
||||
int world_id) override;
|
||||
// content::ContentRendererClient:
|
||||
void RenderFrameCreated(content::RenderFrame*) override;
|
||||
void RenderViewCreated(content::RenderView*) override;
|
||||
|
|
|
@ -34,6 +34,9 @@ class RendererClientBase : public content::ContentRendererClient {
|
|||
virtual void DidClearWindowObject(content::RenderFrame* render_frame);
|
||||
virtual void SetupMainWorldOverrides(v8::Handle<v8::Context> context,
|
||||
content::RenderFrame* render_frame) = 0;
|
||||
virtual void SetupExtensionWorldOverrides(v8::Handle<v8::Context> context,
|
||||
content::RenderFrame* render_frame,
|
||||
int world_id) = 0;
|
||||
|
||||
bool isolated_world() const { return isolated_world_; }
|
||||
|
||||
|
|
|
@ -95,6 +95,12 @@ webFrame.setSpellCheckProvider('en-US', {
|
|||
})
|
||||
```
|
||||
|
||||
### `webFrame.insertCSS(css)`
|
||||
|
||||
* `css` String - CSS source code.
|
||||
|
||||
Inserts `css` as a style sheet in the document.
|
||||
|
||||
### `webFrame.insertText(text)`
|
||||
|
||||
* `text` String
|
||||
|
@ -119,7 +125,7 @@ this limitation.
|
|||
|
||||
### `webFrame.executeJavaScriptInIsolatedWorld(worldId, scripts[, userGesture, callback])`
|
||||
|
||||
* `worldId` Integer - The ID of the world to run the javascript in, `0` is the default world, `999` is the world used by Electrons `contextIsolation` feature. You can provide any integer here.
|
||||
* `worldId` Integer - The ID of the world to run the javascript in, `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.
|
||||
* `scripts` [WebSource[]](structures/web-source.md)
|
||||
* `userGesture` Boolean (optional) - Default is `false`.
|
||||
* `callback` Function (optional) - Called after script has been executed.
|
||||
|
@ -129,27 +135,27 @@ Work like `executeJavaScript` but evaluates `scripts` in an isolated context.
|
|||
|
||||
### `webFrame.setIsolatedWorldContentSecurityPolicy(worldId, csp)` _(Deprecated)_
|
||||
|
||||
* `worldId` Integer - The ID of the world to run the javascript in, `0` is the default world, `999` is the world used by Electrons `contextIsolation` feature. You can provide any integer here.
|
||||
* `worldId` Integer - The ID of the world to run the javascript in, `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.
|
||||
* `csp` String
|
||||
|
||||
Set the content security policy of the isolated world.
|
||||
|
||||
### `webFrame.setIsolatedWorldHumanReadableName(worldId, name)` _(Deprecated)_
|
||||
|
||||
* `worldId` Integer - The ID of the world to run the javascript in, `0` is the default world, `999` is the world used by Electrons `contextIsolation` feature. You can provide any integer here.
|
||||
* `worldId` Integer - The ID of the world to run the javascript in, `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.
|
||||
* `name` String
|
||||
|
||||
Set the name of the isolated world. Useful in devtools.
|
||||
|
||||
### `webFrame.setIsolatedWorldSecurityOrigin(worldId, securityOrigin)` _(Deprecated)_
|
||||
|
||||
* `worldId` Integer - The ID of the world to run the javascript in, `0` is the default world, `999` is the world used by Electrons `contextIsolation` feature. You can provide any integer here.
|
||||
* `worldId` Integer - The ID of the world to run the javascript in, `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.
|
||||
* `securityOrigin` String
|
||||
|
||||
Set the security origin of the isolated world.
|
||||
|
||||
### `webFrame.setIsolatedWorldInfo(worldId, info)`
|
||||
* `worldId` Integer - The ID of the world to run the javascript in, `0` is the default world, `999` is the world used by Electrons `contextIsolation` feature. You can provide any integer here.
|
||||
* `worldId` Integer - The ID of the world to run the javascript in, `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.
|
||||
* `info` Object
|
||||
* `securityOrigin` String (optional) - Security origin for the isolated world.
|
||||
* `csp` String (optional) - Content Security Policy for the isolated world.
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
'use strict'
|
||||
|
||||
/* global nodeProcess, isolatedWorld, worldId */
|
||||
|
||||
const { EventEmitter } = require('events')
|
||||
|
||||
process.atomBinding = require('@electron/internal/common/atom-binding-setup').atomBindingSetup(nodeProcess.binding, 'renderer')
|
||||
|
||||
const v8Util = process.atomBinding('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', new EventEmitter())
|
||||
// The process object created by browserify is not an event emitter, fix it so
|
||||
// the API is more compatible with non-sandboxed renderers.
|
||||
for (const prop of Object.keys(EventEmitter.prototype)) {
|
||||
if (process.hasOwnProperty(prop)) {
|
||||
delete process[prop]
|
||||
}
|
||||
}
|
||||
Object.setPrototypeOf(process, EventEmitter.prototype)
|
||||
|
||||
const isolatedWorldArgs = v8Util.getHiddenValue(isolatedWorld, 'isolated-world-args')
|
||||
|
||||
if (isolatedWorldArgs) {
|
||||
const { ipcRendererInternal, guestInstanceId, isHiddenPage, openerId, usesNativeWindowOpen } = isolatedWorldArgs
|
||||
const { windowSetup } = require('@electron/internal/renderer/window-setup')
|
||||
windowSetup(ipcRendererInternal, guestInstanceId, openerId, isHiddenPage, usesNativeWindowOpen)
|
||||
}
|
||||
|
||||
const extensionId = v8Util.getHiddenValue(isolatedWorld, `extension-${worldId}`)
|
||||
|
||||
if (extensionId) {
|
||||
const chromeAPI = require('@electron/internal/renderer/chrome-api')
|
||||
chromeAPI.injectTo(extensionId, false, window)
|
||||
}
|
|
@ -1,5 +1,24 @@
|
|||
import { ipcRendererInternal } from '@electron/internal/renderer/ipc-renderer-internal'
|
||||
import { runInThisContext } from 'vm'
|
||||
import { webFrame } from 'electron'
|
||||
|
||||
const v8Util = process.atomBinding('v8_util')
|
||||
|
||||
const IsolatedWorldIDs = {
|
||||
/**
|
||||
* Start of extension isolated world IDs, as defined in
|
||||
* atom_render_frame_observer.h
|
||||
*/
|
||||
ISOLATED_WORLD_EXTENSIONS: 1 << 20
|
||||
}
|
||||
|
||||
let isolatedWorldIds = IsolatedWorldIDs.ISOLATED_WORLD_EXTENSIONS
|
||||
const extensionWorldId: {[key: string]: number | undefined} = {}
|
||||
|
||||
// https://cs.chromium.org/chromium/src/extensions/renderer/script_injection.cc?type=cs&sq=package:chromium&g=0&l=52
|
||||
const getIsolatedWorldIdForInstance = () => {
|
||||
// TODO(samuelmaddock): allocate and cleanup IDs
|
||||
return isolatedWorldIds++
|
||||
}
|
||||
|
||||
// Check whether pattern matches.
|
||||
// https://developer.chrome.com/extensions/match_patterns
|
||||
|
@ -12,21 +31,21 @@ const matchesPattern = function (pattern: string) {
|
|||
|
||||
// Run the code with chrome API integrated.
|
||||
const runContentScript = function (this: any, extensionId: string, url: string, code: string) {
|
||||
const context: { chrome?: any } = {}
|
||||
require('@electron/internal/renderer/chrome-api').injectTo(extensionId, false, context)
|
||||
const wrapper = `((chrome) => {\n ${code}\n })`
|
||||
try {
|
||||
const compiledWrapper = runInThisContext(wrapper, {
|
||||
filename: url,
|
||||
lineOffset: 1,
|
||||
displayErrors: true
|
||||
})
|
||||
return compiledWrapper.call(this, context.chrome)
|
||||
} catch (error) {
|
||||
// TODO(samuelmaddock): Run scripts in isolated world, see chromium script_injection.cc
|
||||
console.error(`Error running content script JavaScript for '${extensionId}'`)
|
||||
console.error(error)
|
||||
}
|
||||
// Assign unique world ID to each extension
|
||||
const worldId = extensionWorldId[extensionId] ||
|
||||
(extensionWorldId[extensionId] = getIsolatedWorldIdForInstance())
|
||||
|
||||
// store extension ID for content script to read in isolated world
|
||||
v8Util.setHiddenValue(global, `extension-${worldId}`, extensionId)
|
||||
|
||||
webFrame.setIsolatedWorldInfo(worldId, {
|
||||
name: `${extensionId} [${worldId}]`
|
||||
// TODO(samuelmaddock): read `content_security_policy` from extension manifest
|
||||
// csp: manifest.content_security_policy,
|
||||
})
|
||||
|
||||
const sources = [{ code, url }]
|
||||
webFrame.executeJavaScriptInIsolatedWorld(worldId, sources)
|
||||
}
|
||||
|
||||
const runAllContentScript = function (scripts: Array<Electron.InjectionBase>, extensionId: string) {
|
||||
|
@ -36,28 +55,7 @@ const runAllContentScript = function (scripts: Array<Electron.InjectionBase>, ex
|
|||
}
|
||||
|
||||
const runStylesheet = function (this: any, url: string, code: string) {
|
||||
const wrapper = `((code) => {
|
||||
function init() {
|
||||
const styleElement = document.createElement('style');
|
||||
styleElement.textContent = code;
|
||||
document.head.append(styleElement);
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
})`
|
||||
|
||||
try {
|
||||
const compiledWrapper = runInThisContext(wrapper, {
|
||||
filename: url,
|
||||
lineOffset: 1,
|
||||
displayErrors: true
|
||||
})
|
||||
|
||||
return compiledWrapper.call(this, code)
|
||||
} catch (error) {
|
||||
// TODO(samuelmaddock): Insert stylesheet directly into document, see chromium script_injection.cc
|
||||
console.error(`Error inserting content script stylesheet ${url}`)
|
||||
console.error(error)
|
||||
}
|
||||
webFrame.insertCSS(code)
|
||||
}
|
||||
|
||||
const runAllStylesheet = function (css: Array<Electron.InjectionBase>) {
|
||||
|
|
Загрузка…
Ссылка в новой задаче