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:
Samuel Maddock 2019-03-11 19:27:57 -04:00 committed by Samuel Attard
parent 6072da239d
commit f943db7ad5
11 changed files with 187 additions and 44 deletions

View file

@ -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",
]

View file

@ -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() {

View file

@ -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.

View file

@ -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())

View file

@ -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:

View file

@ -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) {

View file

@ -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;

View file

@ -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_; }

View file

@ -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.

View file

@ -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)
}

View file

@ -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>) {