Skip to content

Commit

Permalink
feat: Add content script world isolation (#17032)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
samuelmaddock authored and MarshallOfSound committed Mar 11, 2019
1 parent 6072da2 commit f943db7
Show file tree
Hide file tree
Showing 11 changed files with 187 additions and 44 deletions.
33 changes: 33 additions & 0 deletions BUILD.gn
Expand Up @@ -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") { copy("atom_js2c_copy") {
sources = [ sources = [
"lib/common/asar.js", "lib/common/asar.js",
Expand All @@ -149,12 +180,14 @@ copy("atom_js2c_copy") {


action("atom_js2c") { action("atom_js2c") {
deps = [ deps = [
":atom_browserify_content_script",
":atom_browserify_isolated", ":atom_browserify_isolated",
":atom_browserify_sandbox", ":atom_browserify_sandbox",
":atom_js2c_copy", ":atom_js2c_copy",
] ]


browserify_sources = [ browserify_sources = [
"$target_gen_dir/js2c/content_script_bundle.js",
"$target_gen_dir/js2c/isolated_bundle.js", "$target_gen_dir/js2c/isolated_bundle.js",
"$target_gen_dir/js2c/preload_bundle.js", "$target_gen_dir/js2c/preload_bundle.js",
] ]
Expand Down
6 changes: 6 additions & 0 deletions atom/renderer/atom_render_frame_observer.cc
Expand Up @@ -113,6 +113,12 @@ void AtomRenderFrameObserver::DidCreateScriptContext(
CreateIsolatedWorldContext(); CreateIsolatedWorldContext();
renderer_client_->SetupMainWorldOverrides(context, render_frame_); 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() { void AtomRenderFrameObserver::DraggableRegionsChanged() {
Expand Down
13 changes: 12 additions & 1 deletion atom/renderer/atom_render_frame_observer.h
Expand Up @@ -11,6 +11,7 @@
#include "base/strings/string16.h" #include "base/strings/string16.h"
#include "content/public/renderer/render_frame_observer.h" #include "content/public/renderer/render_frame_observer.h"
#include "ipc/ipc_platform_file.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" #include "third_party/blink/public/web/web_local_frame.h"


namespace base { namespace base {
Expand All @@ -21,9 +22,19 @@ namespace atom {


enum World { enum World {
MAIN_WORLD = 0, MAIN_WORLD = 0,

// Use a high number far away from 0 to not collide with any other world // Use a high number far away from 0 to not collide with any other world
// IDs created internally by Chrome. // 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. // Helper class to forward the messages to the client.
Expand Down
21 changes: 21 additions & 0 deletions atom/renderer/atom_renderer_client.cc
Expand Up @@ -210,6 +210,27 @@ void AtomRendererClient::SetupMainWorldOverrides(
&isolated_bundle_args, nullptr); &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( node::Environment* AtomRendererClient::GetEnvironment(
content::RenderFrame* render_frame) const { content::RenderFrame* render_frame) const {
if (injected_frames_.find(render_frame) == injected_frames_.end()) if (injected_frames_.find(render_frame) == injected_frames_.end())
Expand Down
3 changes: 3 additions & 0 deletions atom/renderer/atom_renderer_client.h
Expand Up @@ -33,6 +33,9 @@ class AtomRendererClient : public RendererClientBase {
content::RenderFrame* render_frame) override; content::RenderFrame* render_frame) override;
void SetupMainWorldOverrides(v8::Handle<v8::Context> context, void SetupMainWorldOverrides(v8::Handle<v8::Context> context,
content::RenderFrame* render_frame) override; content::RenderFrame* render_frame) override;
void SetupExtensionWorldOverrides(v8::Handle<v8::Context> context,
content::RenderFrame* render_frame,
int world_id) override;


private: private:
// content::ContentRendererClient: // content::ContentRendererClient:
Expand Down
24 changes: 24 additions & 0 deletions atom/renderer/atom_sandboxed_renderer_client.cc
Expand Up @@ -270,6 +270,30 @@ void AtomSandboxedRendererClient::SetupMainWorldOverrides(
&isolated_bundle_args, nullptr); &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( void AtomSandboxedRendererClient::WillReleaseScriptContext(
v8::Handle<v8::Context> context, v8::Handle<v8::Context> context,
content::RenderFrame* render_frame) { content::RenderFrame* render_frame) {
Expand Down
3 changes: 3 additions & 0 deletions atom/renderer/atom_sandboxed_renderer_client.h
Expand Up @@ -32,6 +32,9 @@ class AtomSandboxedRendererClient : public RendererClientBase {
content::RenderFrame* render_frame) override; content::RenderFrame* render_frame) override;
void SetupMainWorldOverrides(v8::Handle<v8::Context> context, void SetupMainWorldOverrides(v8::Handle<v8::Context> context,
content::RenderFrame* render_frame) override; content::RenderFrame* render_frame) override;
void SetupExtensionWorldOverrides(v8::Handle<v8::Context> context,
content::RenderFrame* render_frame,
int world_id) override;
// content::ContentRendererClient: // content::ContentRendererClient:
void RenderFrameCreated(content::RenderFrame*) override; void RenderFrameCreated(content::RenderFrame*) override;
void RenderViewCreated(content::RenderView*) override; void RenderViewCreated(content::RenderView*) override;
Expand Down
3 changes: 3 additions & 0 deletions atom/renderer/renderer_client_base.h
Expand Up @@ -34,6 +34,9 @@ class RendererClientBase : public content::ContentRendererClient {
virtual void DidClearWindowObject(content::RenderFrame* render_frame); virtual void DidClearWindowObject(content::RenderFrame* render_frame);
virtual void SetupMainWorldOverrides(v8::Handle<v8::Context> context, virtual void SetupMainWorldOverrides(v8::Handle<v8::Context> context,
content::RenderFrame* render_frame) = 0; 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_; } bool isolated_world() const { return isolated_world_; }


Expand Down
16 changes: 11 additions & 5 deletions docs/api/web-frame.md
Expand Up @@ -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)` ### `webFrame.insertText(text)`


* `text` String * `text` String
Expand All @@ -119,7 +125,7 @@ this limitation.


### `webFrame.executeJavaScriptInIsolatedWorld(worldId, scripts[, userGesture, callback])` ### `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) * `scripts` [WebSource[]](structures/web-source.md)
* `userGesture` Boolean (optional) - Default is `false`. * `userGesture` Boolean (optional) - Default is `false`.
* `callback` Function (optional) - Called after script has been executed. * `callback` Function (optional) - Called after script has been executed.
Expand All @@ -129,27 +135,27 @@ Work like `executeJavaScript` but evaluates `scripts` in an isolated context.


### `webFrame.setIsolatedWorldContentSecurityPolicy(worldId, csp)` _(Deprecated)_ ### `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 * `csp` String


Set the content security policy of the isolated world. Set the content security policy of the isolated world.


### `webFrame.setIsolatedWorldHumanReadableName(worldId, name)` _(Deprecated)_ ### `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 * `name` String


Set the name of the isolated world. Useful in devtools. Set the name of the isolated world. Useful in devtools.


### `webFrame.setIsolatedWorldSecurityOrigin(worldId, securityOrigin)` _(Deprecated)_ ### `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 * `securityOrigin` String


Set the security origin of the isolated world. Set the security origin of the isolated world.


### `webFrame.setIsolatedWorldInfo(worldId, info)` ### `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 * `info` Object
* `securityOrigin` String (optional) - Security origin for the isolated world. * `securityOrigin` String (optional) - Security origin for the isolated world.
* `csp` String (optional) - Content Security Policy for the isolated world. * `csp` String (optional) - Content Security Policy for the isolated world.
Expand Down
35 changes: 35 additions & 0 deletions lib/content_script/init.js
@@ -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)
}
74 changes: 36 additions & 38 deletions lib/renderer/content-scripts-injector.ts
@@ -1,5 +1,24 @@
import { ipcRendererInternal } from '@electron/internal/renderer/ipc-renderer-internal' 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. // Check whether pattern matches.
// https://developer.chrome.com/extensions/match_patterns // https://developer.chrome.com/extensions/match_patterns
Expand All @@ -12,21 +31,21 @@ const matchesPattern = function (pattern: string) {


// Run the code with chrome API integrated. // Run the code with chrome API integrated.
const runContentScript = function (this: any, extensionId: string, url: string, code: string) { const runContentScript = function (this: any, extensionId: string, url: string, code: string) {
const context: { chrome?: any } = {} // Assign unique world ID to each extension
require('@electron/internal/renderer/chrome-api').injectTo(extensionId, false, context) const worldId = extensionWorldId[extensionId] ||
const wrapper = `((chrome) => {\n ${code}\n })` (extensionWorldId[extensionId] = getIsolatedWorldIdForInstance())
try {
const compiledWrapper = runInThisContext(wrapper, { // store extension ID for content script to read in isolated world
filename: url, v8Util.setHiddenValue(global, `extension-${worldId}`, extensionId)
lineOffset: 1,
displayErrors: true webFrame.setIsolatedWorldInfo(worldId, {
}) name: `${extensionId} [${worldId}]`
return compiledWrapper.call(this, context.chrome) // TODO(samuelmaddock): read `content_security_policy` from extension manifest
} catch (error) { // csp: manifest.content_security_policy,
// TODO(samuelmaddock): Run scripts in isolated world, see chromium script_injection.cc })
console.error(`Error running content script JavaScript for '${extensionId}'`)
console.error(error) const sources = [{ code, url }]
} webFrame.executeJavaScriptInIsolatedWorld(worldId, sources)
} }


const runAllContentScript = function (scripts: Array<Electron.InjectionBase>, extensionId: string) { const runAllContentScript = function (scripts: Array<Electron.InjectionBase>, extensionId: string) {
Expand All @@ -36,28 +55,7 @@ const runAllContentScript = function (scripts: Array<Electron.InjectionBase>, ex
} }


const runStylesheet = function (this: any, url: string, code: string) { const runStylesheet = function (this: any, url: string, code: string) {
const wrapper = `((code) => { webFrame.insertCSS(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)
}
} }


const runAllStylesheet = function (css: Array<Electron.InjectionBase>) { const runAllStylesheet = function (css: Array<Electron.InjectionBase>) {
Expand Down

0 comments on commit f943db7

Please sign in to comment.