Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: security: add an option to disable the remote module (backport: 4-0-x) #15222

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 @@ -453,12 +459,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 @@ -475,6 +497,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 @@ -484,11 +507,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
}