-
Notifications
You must be signed in to change notification settings - Fork 16.2k
Refactor sandbox preload initialization. #12877
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
Refactor sandbox preload initialization. #12877
Conversation
85ff0a9
to
92adee0
Compare
@@ -449,3 +450,13 @@ ipcMain.on('ELECTRON_BROWSER_WINDOW_CLOSE', function (event) { | |||
} | |||
event.returnValue = null | |||
}) | |||
|
|||
ipcMain.on('ELECTRON_BROWSER_SANDBOX_LOAD', function (event, preloadPath) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would not pass the preloadPath
as an argument. We know which webContents sent the IPC so it should be possible to get the preload script path from the webPreferences.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to assume that the renderer process is compromised and can send arbitrary IPC messages. We cannot allow it to load random files from disk pretending it is a preload script.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Apart from this issue I like this change, batching multiple synchronous IPC calls into one to make loading faster is definitely welcomed 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to assume that the renderer process is compromised
Electron is not designed to run untrusted code, so we can never assume that. Untrusted code with access to ipcRenderer
can easily control the browser process since it is possible to access any node.js module remotely and AFAIK there's no IPC filtering yet.
In any case, I agree that it would be cleaner to take preloadPath from webPreferences directly as it would also remove the need to pass the path as a renderer argument.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are actually planning to add some filtering for remote.require
, remote.getGlobal
, etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also if you just let fs.readFileSync throw and not assign the returnValue, sendSync in the renderer process will get stuck waiting.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with @miniak this is basically a one-liner "read any file plz" API, I'd rather we (in this PR) used event.sender.getLastWebPreferences()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@tarruda this is the last remaining issue
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with @miniak this is basically a one-liner "read any file plz" API, I'd rather we (in this PR) used event.sender.getLastWebPreferences()
We currently give access to electron.remote.fs
which can not only read any file, but also invoke any FS function. There's no regression in terms of security here by allowing the renderer to specify the preloadPath.
These are my arguments for keeping the current behavior and addressing this issue in a separate PR:
- If we start using
event.sender.getLastWebPreferences()
, there's no reason to pass the preload path as argument to the renderer command line, so it would become necessary to refactor more code in C++ both in browser/renderer. (Unless we are OK with leaving dead code to be handled in a separate PR) - There's no guarantee that this will work as expected without introducing new regressions. Maybe it will, but I will have to investigate if
preload
will be present in everyevent.sender.webPreferences
(in windows created by window.open, for example, which follow a different webContents creation path). - This is a completely separate change, not related to the goal of this PR which is to fix Error: Cannot get property 'id' on missing remote object #12316. I'd rather push refactoring changes in different PRs.
If you guys still insist that this should be done in the current PR, I will do it, but not right now since it will require more testing/verification which I can't do this week.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@MarshallOfSound? I guess I am ok if this gets addressed in a separate PR.
lib/sandboxed_renderer/init.js
Outdated
process.on('exit', () => preloadProcess.emit('exit')) | ||
|
||
// This is the `require` function that will be visible to the preload script | ||
function preloadRequire (module) { | ||
if (preloadModules.has(module)) { | ||
return preloadModules.get(module) | ||
return require(module) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this work with browserify?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the preload script is a browserify bundle, then it will have access to the preloadRequire
function. This require
is private to the sandbox initialization script (which is also a browserify bundle, but independent from what the user creates).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
According to the docs
Browserify parses the AST for require() calls to traverse the entire dependency graph of your project.
doesn't that mean that all the require calls have to have the module name as literal string?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see what you mean. I will split the remotely-loaded modules which should be lazy loaded (and have -r flags) from the browserify modules which must have static requires. Thanks for the catch
@tarruda off-topic question, why is |
lib/sandboxed_renderer/init.js
Outdated
const preloadModules = new Set([ | ||
'child_process', | ||
'electron', | ||
'fs', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why did you remove events?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was added recently in #12828
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was a merge error, I took this patch from an older electron version we use in our app. Will add it back 👍
I forgot to add it when this file was created. Will add it now 👍 |
@miniak about this change:
Since it involves some C++ changes I will do it in a separate PR ok? |
d5f6630
to
27bed69
Compare
return loadedModules.get(module) | ||
} | ||
if (remoteModules.has(module)) { | ||
return require(module) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more. < 9E81 /p>
shouldn't this be electron.remote.require instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
in which case do we still need to keep these files?
lib/sandboxed_renderer/api/exports/child_process.js
lib/sandboxed_renderer/api/exports/fs.js
lib/sandboxed_renderer/api/exports/os.js
lib/sandboxed_renderer/api/exports/path.js
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
and remove these from electron.gyp
'./lib/sandboxed_renderer/api/exports/fs.js:fs',
'-r',
'./lib/sandboxed_renderer/api/exports/os.js:os',
'-r',
'./lib/sandboxed_renderer/api/exports/path.js:path',
'-r',
'./lib/sandboxed_renderer/api/exports/child_process.js:child_process'
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, we still require these files to emulate a real node.js require for electron renderer modules that are reused in sandbox. Those are bundled in the executable and don't know they are running in a browserify environment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What I mean is, electron modules such as crashReporter won't use this require
function, so we need to tell browserify that when crashReporter requires fs
, it is requiring /lib/sandboxed_renderer/api/exports/fs.js
and not browserify shim.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok
'path' | ||
]) | ||
|
||
const loadedModules = new Map([ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe bundledModules could be a better name?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Technically, all modules are bundled. The ones that delegate to electron.remote are loaded lazily, but are still bundled.
@miniak With exception of taking preload path from webPreferences (which I want to do in a separate PR), I believe I've addressed all your comments. Let me know when you've finished your review so I can squash the fixup commits. |
${preloadSrc} | ||
})` | ||
|
||
// eval in window scope: | ||
// http://www.ecma-international.org/ecma-262/5.1/#sec-10.4.2 | ||
const geval = eval | ||
const preloadFn = geval(preloadWrapperSrc) | ||
const {setImmediate} = require('timers') | ||
preloadFn(preloadRequire, preloadProcess, Buffer, global, setImmediate) | ||
const {setImmediate, clearImmediate} = require('timers') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
if (preloadPath) { | ||
const preloadSrc = fs.readFileSync(preloadPath).toString() | ||
const preloadWrapperSrc = `(function(require, process, Buffer, global, setImmediate) { | ||
if (preloadSrc) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you need to check here if preloadSrc is an Error
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you are not logging the error here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've used preloadError
for the error
lib/browser/rpc-server.js
Outdated
try { | ||
preloadSrc = fs.readFileSync(preloadPath).toString() | ||
} catch (err) { | ||
preloadSrc = err |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe a separate variable? preloadError?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also we need to be aware the Error
objects to not JSON stringify very well by default, you'll need to manually pull out the stack
and message
attributes into a new object
lib/sandboxed_renderer/init.js
Outdated
preloadSrc, webContentsId, platform, execPath, env | ||
} = electron.ipcRenderer.sendSync('ELECTRON_BROWSER_SANDBOX_LOAD', preloadPath) | ||
|
||
process.webContentsId = webContentsId |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For safety
Object.defineProperty(process, 'webContentsId', {
configurable: false,
value: webContentsId
})
This way nothing can ever override that value
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
left some comments
lib/sandboxed_renderer/init.js
Outdated
@@ -103,4 +106,6 @@ if (preloadSrc) { | |||
const preloadFn = geval(preloadWrapperSrc) | |||
const {setImmediate, clearImmediate} = require('timers') | |||
preloadFn(preloadRequire, preloadProcess, Buffer, global, setImmediate, clearImmediate) | |||
} else if (preloadError) { | |||
console.error(preloadError.stack) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@miniak is this how you wanted to handle preload error? I can't think of a better way right now, logging via console.error
seems like a sane way to do it, since it would be available in the terminal if electron is started with --enable-logging
lib/browser/rpc-server.js
Outdated
try { | ||
preloadSrc = fs.readFileSync(preloadPath).toString() | ||
} catch (err) { | ||
preloadError = {stack: err.stack} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why don't you include name and message as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The stack
property already contains the message and Error class name. Example from the repl:
> console.log(new Error('msg').stack)
Error: msg
at repl:1:13
at ContextifyScript.Script.runInThisContext (vm.js:50:33)
at REPLServer.defaultEval (repl.js:240:29)
at bound (domain.js:301:14)
at REPLServer.runBound [as eval] (domain.js:314:12)
at REPLServer.onLine (repl.js:468:10)
at emitOne (events.js:121:20)
at REPLServer.emit (events.js:211:7)
at REPLServer.Interface._onLine (readline.js:282:10)
at REPLServer.Interface._line (readline.js:631:8)
lib/browser/rpc-server.js
Outdated
try { | ||
preloadSrc = fs.readFileSync(preloadPath).toString() | ||
} catch (err) { | ||
preloadError = {stack: err.stack} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I hate to be that person 😆 But welcome to javascript where you can throw null
This should check if we can get to .stack
before actually trying or our error handling will error 😆
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
be27e54
to
ebf6bf4
Compare
Rebased/squashed commits |
9f0e12a
to
c8640ef
Compare
} = electron.ipcRenderer.sendSync('ELECTRON_BROWSER_SANDBOX_LOAD', preloadPath) | ||
|
||
Object.defineProperty(process, 'webContentsId', { | ||
configurable: false, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
false
is a default value of the configurable
attribute, no reasons to explicitly set it.
If you want to be explicit then you should also set writable: false
(which is also a default value).
@tarruda the PR got conflicts, can you please rebase it on the latest master? Thanks! |
Use a single synchronous IPC call to retrieve data required by early sandbox scripts. This has two purposes: - Optimize preload script initialization by: - Using one synchronous IPC call to retrieve preload script, webContentsId (more on that later), process.{platform,execPath,env} - Lazy loading as many modules as possible. - Fix #12316 for sandbox. @MarshallOfSound addressed the issue in #12342, but it was still present in sandbox mode. By loading webContentsId very early and skipping remote module at early startup, we fix it for sandbox.
c8640ef
to
37f42e5
Compare
@alexeykuzmin Rebased and added |
An error occurred while attempting to backport this PR to "2-0-x", you will need to perform this backport manually |
@tarruda Can you please backport the changes to |
I swear I commented first 😆 |
👍 |
Use a single synchronous IPC call to retrieve data required by early
sandbox scripts. This has two purposes:
webContentsId (more on that later), process.{platform,execPath,env}
Remove the race condition between new process creation and context release #12342, but it was still present in sandbox mode. By loading
webContentsId very early and skipping remote module at early
startup, we fix it for sandbox.