Skip to content

Commit

Permalink
feat: security: add an option to disable the remote module (#15222)
Browse files Browse the repository at this point in the history
Add `webPreferences.enableRemoteModule` option allowing to disable the remote module to increase sandbox security.
  • Loading branch information
trop[bot] authored and alexeykuzmin committed Oct 23, 2018
1 parent 11ebf5c commit a313aae
Show file tree
Hide file tree
Showing 36 changed files with 303 additions and 45 deletions.
8 changes: 8 additions & 0 deletions atom/browser/api/atom_api_web_contents.cc
Expand Up @@ -1896,6 +1896,13 @@ v8::Local<v8::Value> WebContents::GetLastWebPreferences(
return mate::ConvertToV8(isolate, *web_preferences->last_preference());
}

bool WebContents::IsRemoteModuleEnabled() const {
if (auto* web_preferences = WebContentsPreferences::From(web_contents())) {
return web_preferences->IsRemoteModuleEnabled();
}
return true;
}

v8::Local<v8::Value> WebContents::GetOwnerBrowserWindow() const {
if (owner_window())
return BrowserWindow::From(isolate(), owner_window());
Expand Down Expand Up @@ -2067,6 +2074,7 @@ void WebContents::BuildPrototype(v8::Isolate* isolate,
.SetMethod("_getPreloadPath", &WebContents::GetPreloadPath)
.SetMethod("getWebPreferences", &WebContents::GetWebPreferences)
.SetMethod("getLastWebPreferences", &WebContents::GetLastWebPreferences)
.SetMethod("_isRemoteModuleEnabled", &WebContents::IsRemoteModuleEnabled)
.SetMethod("getOwnerBrowserWindow", &WebContents::GetOwnerBrowserWindow)
.SetMethod("hasServiceWorker", &WebContents::HasServiceWorker)
.SetMethod("unregisterServiceWorker",
Expand Down
2 changes: 2 additions & 0 deletions atom/browser/api/atom_api_web_contents.h
Expand Up @@ -249,6 +249,8 @@ class WebContents : public mate::TrackableObject<WebContents>,
v8::Local<v8::Value> GetWebPreferences(v8::Isolate* isolate) const;
v8::Local<v8::Value> GetLastWebPreferences(v8::Isolate* isolate) const;

bool IsRemoteModuleEnabled() const;

// Returns the owner window.
v8::Local<v8::Value> GetOwnerBrowserWindow() const;

Expand Down
8 changes: 8 additions & 0 deletions atom/browser/web_contents_preferences.cc
Expand Up @@ -171,6 +171,10 @@ bool WebContentsPreferences::GetPreference(const base::StringPiece& name,
return GetAsString(&preference_, name, value);
}

bool WebContentsPreferences::IsRemoteModuleEnabled() const {
return IsEnabled(options::kEnableRemoteModule, true);
}

bool WebContentsPreferences::GetPreloadPath(
base::FilePath::StringType* path) const {
DCHECK(path);
Expand Down Expand Up @@ -267,6 +271,10 @@ void WebContentsPreferences::AppendCommandLineSwitches(
}
}

// Whether to enable the remote module
if (!IsRemoteModuleEnabled())
command_line->AppendSwitch(switches::kDisableRemoteModule);

// Run Electron APIs and preload script in isolated world
if (IsEnabled(options::kContextIsolation))
command_line->AppendSwitch(switches::kContextIsolation);
Expand Down
3 changes: 3 additions & 0 deletions atom/browser/web_contents_preferences.h
Expand Up @@ -55,6 +55,9 @@ class WebContentsPreferences
// Return true if the particular preference value exists.
bool GetPreference(const base::StringPiece& name, std::string* value) const;

// Whether to enable the remote module
bool IsRemoteModuleEnabled() const;

// Returns the preload script path.
bool GetPreloadPath(base::FilePath::StringType* path) const;

Expand Down
4 changes: 4 additions & 0 deletions atom/common/options_switches.cc
Expand Up @@ -110,6 +110,9 @@ const char kPreloadURL[] = "preloadURL";
// Enable the node integration.
const char kNodeIntegration[] = "nodeIntegration";

// Enable the remote module
const char kEnableRemoteModule[] = "enableRemoteModule";

// Enable context isolation of Electron APIs and preload script
const char kContextIsolation[] = "contextIsolation";

Expand Down Expand Up @@ -193,6 +196,7 @@ const char kBackgroundColor[] = "background-color";
const char kPreloadScript[] = "preload";
const char kPreloadScripts[] = "preload-scripts";
const char kNodeIntegration[] = "node-integration";
const char kDisableRemoteModule[] = "disable-remote-module";
const char kContextIsolation[] = "context-isolation";
const char kGuestInstanceID[] = "guest-instance-id";
const char kOpenerID[] = "opener-id";
Expand Down
2 changes: 2 additions & 0 deletions atom/common/options_switches.h
Expand Up @@ -58,6 +58,7 @@ extern const char kZoomFactor[];
extern const char kPreloadScript[];
extern const char kPreloadURL[];
extern const char kNodeIntegration[];
extern const char kEnableRemoteModule[];
extern const char kContextIsolation[];
extern const char kGuestInstanceID[];
extern const char kExperimentalFeatures[];
Expand Down Expand Up @@ -97,6 +98,7 @@ extern const char kBackgroundColor[];
extern const char kPreloadScript[];
extern const char kPreloadScripts[];
extern const char kNodeIntegration[];
extern const char kDisableRemoteModule[];
extern const char kContextIsolation[];
extern const char kGuestInstanceID[];
extern const char kOpenerID[];
Expand Down
20 changes: 16 additions & 4 deletions atom/renderer/renderer_client_base.cc
Expand Up @@ -71,6 +71,15 @@ std::vector<std::string> ParseSchemesCLISwitch(base::CommandLine* command_line,
base::SPLIT_WANT_NONEMPTY);
}

void SetHiddenValue(v8::Handle<v8::Context> context,
const base::StringPiece& key,
v8::Local<v8::Value> value) {
v8::Isolate* isolate = context->GetIsolate();
v8::Local<v8::Private> privateKey =
v8::Private::ForApi(isolate, mate::StringToV8(isolate, key));
context->Global()->SetPrivate(context, privateKey, value);
}

} // namespace

RendererClientBase::RendererClientBase() {
Expand Down Expand Up @@ -100,10 +109,13 @@ void RendererClientBase::DidCreateScriptContext(
auto context_id = base::StringPrintf(
"%s-%" PRId64, renderer_client_id_.c_str(), ++next_context_id_);
v8::Isolate* isolate = context->GetIsolate();
v8::Local<v8::String> key = mate::StringToSymbol(isolate, "contextId");
v8::Local<v8::Private> private_key = v8::Private::ForApi(isolate, key);
v8::Local<v8::Value> value = mate::ConvertToV8(isolate, context_id);
context->Global()->SetPrivate(context, private_key, value);
SetHiddenValue(context, "contextId", mate::ConvertToV8(isolate, context_id));

auto* command_line = base::CommandLine::ForCurrentProcess();
bool enableRemoteModule =
!command_line->HasSwitch(switches::kDisableRemoteModule);
SetHiddenValue(context, "enableRemoteModule",
mate::ConvertToV8(isolate, enableRemoteModule));
}

void RendererClientBase::AddRenderBindings(
Expand Down
2 changes: 2 additions & 0 deletions docs/api/browser-window.md
Expand Up @@ -270,6 +270,8 @@ It creates a new `BrowserWindow` with native properties as set by the `options`.
are more limited. Read more about the option [here](sandbox-option.md).
**Note:** This option is currently experimental and may change or be
removed in future Electron releases.
* `enableRemoteModule` Boolean (optional) - Whether to enable the [`remote`](remote.md) module.
Default is `true`.
* `session` [Session](session.md#class-session) (optional) - Sets the session used by the
page. Instead of passing the Session object directly, you can also choose to
use the `partition` option instead, which accepts a partition string. When
Expand Down
3 changes: 3 additions & 0 deletions docs/api/clipboard.md
Expand Up @@ -4,6 +4,9 @@
Process: [Main](../glossary.md#main-process), [Renderer](../glossary.md#renderer-process)

In the renderer process context it depends on the [`remote`](remote.md) module on Linux,
it is therefore not available when this module is disabled.

The following example shows how to write a string to the clipboard:

```javascript
Expand Down
6 changes: 5 additions & 1 deletion docs/api/remote.md
Expand Up @@ -24,6 +24,10 @@ win.loadURL('https://github.com')
**Note:** For the reverse (access the renderer process from the main process),
you can use [webContents.executeJavaScript](web-contents.md#contentsexecutejavascriptcode-usergesture-callback).

**Note:** The remote module can be disabled for security reasons in the following contexts:
- [`BrowserWindow`](browser-window.md) - by setting the `enableRemoteModule` option to `false`.
- [`<webview>`](webview-tag.md) - by setting the `enableremotemodule` attribute to `false`.

## Remote Objects

Each object (including functions) returned by the `remote` module represents an
Expand Down Expand Up @@ -180,7 +184,7 @@ belongs.
**Note:** Do not use `removeAllListeners` on [`BrowserWindow`](browser-window.md).
Use of this can remove all [`blur`](https://developer.mozilla.org/en-US/docs/Web/Events/blur)
listeners, disable click events on touch bar buttons, and other unintended
consequences.
consequences.

### `remote.getCurrentWebContents()`

Expand Down
3 changes: 3 additions & 0 deletions docs/api/screen.md
Expand Up @@ -7,6 +7,9 @@ Process: [Main](../glossary.md#main-process), [Renderer](../glossary.md#renderer
You cannot require or use this module until the `ready` event of the `app`
module is emitted.

In the renderer process context it depends on the [`remote`](remote.md) module,
it is therefore not available when this module is disabled.

`screen` is an [EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter).

**Note:** In the renderer / DevTools, `window.screen` is a reserved DOM
Expand Down
12 changes: 12 additions & 0 deletions docs/api/webview-tag.md
Expand Up @@ -126,6 +126,15 @@ integration and can use node APIs like `require` and `process` to access low
level system resources. Node integration is disabled by default in the guest
page.

### `enableremotemodule`

```html
<webview src="http://www.google.com/" enableremotemodule="false"></webview>
```

When this attribute is `false` the guest page in `webview` will not have access
to the [`remote`](remote.md) module. The remote module is avaiable by default.

### `plugins`

```html
Expand Down Expand Up @@ -613,6 +622,9 @@ Shows pop-up dictionary that searches the selected word on the page.
Returns [`WebContents`](web-contents.md) - The web contents associated with
this `webview`.

It depends on the [`remote`](remote.md) module,
it is therefore not available when this module is disabled.

## DOM events

The following DOM events are available to the `webview` tag:
Expand Down
1 change: 1 addition & 0 deletions filenames.gni
Expand Up @@ -68,6 +68,7 @@ filenames = {
"lib/renderer/init.js",
"lib/renderer/inspector.js",
"lib/renderer/ipc-renderer-internal.js",
"lib/renderer/remote.js",
"lib/renderer/override.js",
"lib/renderer/security-warnings.js",
"lib/renderer/web-frame-init.js",
Expand Down
2 changes: 2 additions & 0 deletions lib/browser/guest-view-manager.js
Expand Up @@ -209,6 +209,7 @@ const attachGuest = function (event, embedderFrameId, elementInstanceId, guestIn
const webPreferences = {
guestInstanceId: guestInstanceId,
nodeIntegration: params.nodeintegration != null ? params.nodeintegration : false,
enableRemoteModule: params.enableremotemodule,
plugins: params.plugins,
zoomFactor: embedder._getZoomFactor(),
webSecurity: !params.disablewebsecurity,
Expand Down Expand Up @@ -243,6 +244,7 @@ const attachGuest = function (event, embedderFrameId, elementInstanceId, guestIn
['javascript', false],
['nativeWindowOpen', true],
['nodeIntegration', false],
['enableRemoteModule', false],
['sandbox', true]
])

Expand Down
3 changes: 2 additions & 1 deletion lib/browser/guest-window-manager.js
Expand Up @@ -14,6 +14,7 @@ const inheritedWebPreferences = new Map([
['javascript', false],
['nativeWindowOpen', true],
['nodeIntegration', false],
['enableRemoteModule', false],
['sandbox', true],
['webviewTag', false]
])
Expand Down Expand Up @@ -195,7 +196,7 @@ ipcMain.on('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_OPEN', (event, url, frameName,
const options = {}

const ints = ['x', 'y', 'width', 'height', 'minWidth', 'maxWidth', 'minHeight', 'maxHeight', 'zoomFactor']
const webPreferences = ['zoomFactor', 'nodeIntegration', 'preload', 'javascript', 'contextIsolation', 'webviewTag']
const webPreferences = ['zoomFactor', 'nodeIntegration', 'enableRemoteModule', 'preload', 'javascript', 'contextIsolation', 'webviewTag']
const disposition = 'new-window'

// Used to store additional features
Expand Down
35 changes: 25 additions & 10 deletions lib/browser/rpc-server.js
Expand Up @@ -263,11 +263,17 @@ const callFunction = function (event, contextId, func, caller, args) {
const handleRemoteCommand = function (channel, handler) {
ipcMain.on(channel, (event, contextId, ...args) => {
let returnValue
if (!event.sender._isRemoteModuleEnabled()) {
event.returnValue = null
return
}

try {
returnValue = handler(event, contextId, ...args)
} catch (error) {
returnValue = exceptionToMeta(event.sender, contextId, error)
}

if (returnValue !== undefined) {
event.returnValue = returnValue
}
Expand Down Expand Up @@ -457,12 +463,28 @@ const crashReporterInit = function (options) {
}
}

ipcMain.on('ELECTRON_CRASH_REPORTER_INIT', function (event, options) {
const setReturnValue = function (event, getValue) {
try {
event.returnValue = [null, crashReporterInit(options)]
event.returnValue = [null, getValue()]
} catch (error) {
event.returnValue = [errorUtils.serialize(error)]
}
}

ipcMain.on('ELECTRON_CRASH_REPORTER_INIT', function (event, options) {
setReturnValue(event, () => crashReporterInit(options))
})

ipcMain.on('ELECTRON_BROWSER_GET_LAST_WEB_PREFERENCES', function (event) {
setReturnValue(event, () => event.sender.getLastWebPreferences())
})

ipcMain.on('ELECTRON_BROWSER_CLIPBOARD_READ_FIND_TEXT', function (event) {
setReturnValue(event, () => electron.clipboard.readFindText())
})

ipcMain.on('ELECTRON_BROWSER_CLIPBOARD_WRITE_FIND_TEXT', function (event, text) {
setReturnValue(event, () => electron.clipboard.writeFindText(text))
})

ipcMain.on('ELECTRON_BROWSER_SANDBOX_LOAD', function (event) {
Expand All @@ -479,6 +501,7 @@ ipcMain.on('ELECTRON_BROWSER_SANDBOX_LOAD', function (event) {
event.returnValue = {
preloadSrc,
preloadError,
isRemoteModuleEnabled: event.sender._isRemoteModuleEnabled(),
process: {
arch: process.arch,
platform: process.platform,
Expand All @@ -488,11 +511,3 @@ ipcMain.on('ELECTRON_BROWSER_SANDBOX_LOAD', function (event) {
}
}
})

ipcMain.on('ELECTRON_BROWSER_GET_LAST_WEB_PREFERENCES', function (event) {
try {
event.returnValue = [null, event.sender.getLastWebPreferences()]
} catch (error) {
event.returnValue = [errorUtils.serialize(error)]
}
})
20 changes: 17 additions & 3 deletions lib/common/api/clipboard.js
Expand Up @@ -2,15 +2,29 @@

if (process.platform === 'linux' && process.type === 'renderer') {
// On Linux we could not access clipboard in renderer process.
module.exports = require('electron').remote.clipboard
const { getRemoteForUsage } = require('@electron/internal/renderer/remote')
module.exports = getRemoteForUsage('clipboard').clipboard
} else {
const clipboard = process.atomBinding('clipboard')

// Read/write to find pasteboard over IPC since only main process is notified
// of changes
if (process.platform === 'darwin' && process.type === 'renderer') {
clipboard.readFindText = require('electron').remote.clipboard.readFindText
clipboard.writeFindText = require('electron').remote.clipboard.writeFindText
const ipcRenderer = require('@electron/internal/renderer/ipc-renderer-internal')
const errorUtils = require('@electron/internal/common/error-utils')

const invoke = function (command, ...args) {
const [ error, result ] = ipcRenderer.sendSync(command, ...args)

if (error) {
throw errorUtils.deserialize(error)
} else {
return result
}
}

clipboard.readFindText = (...args) => invoke('ELECTRON_BROWSER_CLIPBOARD_READ_FIND_TEXT', ...args)
clipboard.writeFindText = (...args) => invoke('ELECTRON_BROWSER_CLIPBOARD_WRITE_FIND_TEXT', ...args)
}

module.exports = clipboard
Expand Down
11 changes: 7 additions & 4 deletions lib/renderer/api/module-list.js
@@ -1,6 +1,9 @@
'use strict'

const features = process.atomBinding('features')
const v8Util = process.atomBinding('v8_util')

const enableRemoteModule = v8Util.getHiddenValue(global, 'enableRemoteModule')

// Renderer side modules, please sort alphabetically.
// A module is `enabled` if there is no explicit condition defined.
Expand All @@ -11,8 +14,8 @@ module.exports = [
file: 'desktop-capturer',
enabled: features.isDesktopCapturerEnabled()
},
{ name: 'ipcRenderer', file: 'ipc-renderer', enabled: true },
{ name: 'remote', file: 'remote', enabled: true },
{ name: 'screen', file: 'screen', enabled: true },
{ name: 'webFrame', file: 'web-frame', enabled: true }
{ name: 'ipcRenderer', file: 'ipc-renderer' },
{ name: 'remote', file: 'remote', enabled: enableRemoteModule },
{ name: 'screen', file: 'screen' },
{ name: 'webFrame', file: 'web-frame' }
]
3 changes: 2 additions & 1 deletion lib/renderer/api/screen.js
@@ -1,3 +1,4 @@
'use strict'

module.exports = require('electron').remote.screen
const { getRemoteForUsage } = require('@electron/internal/renderer/remote')
module.exports = getRemoteForUsage('screen').screen
10 changes: 10 additions & 0 deletions lib/renderer/remote.js
@@ -0,0 +1,10 @@
'use strict'

const { remote } = require('electron')

exports.getRemoteForUsage = function (usage) {
if (!remote) {
throw new Error(`${usage} requires remote, which is not enabled`)
}
return remote
}

0 comments on commit a313aae

Please sign in to comment.