-
Notifications
You must be signed in to change notification settings - Fork 30.3k
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
prototypes and libraries are unsafe inside node core libs #17434
Comments
as an alternative to creating a direct reference to every single prototype method or library method we may want to call, perhaps we can shadow the globals in nativemodule wrapper so that they stay scoped to the context that the vm runs them in? (function (exports, require, module, internalBinding, process, { String, Array, Object, JSON, etc }) { or if the |
@devsnek that would ensure you retain a reference to |
Oh, maybe I'm misunderstanding. I thought node core code doesn't tend to run in a vm? If it's running in a vm, then there'd be different problems, unless that vm's globals were all deep-frozen. |
I would tread very carefully as you never know who relies on the ability to monkey patch such things... |
Certainly changing old code in this way might be breaking; but ensuring that new code is robust should be safe. |
You could implement your stuff in a new vm context and use vanilla objects from there, but that doesn't seem practical. |
@hashseed that opens new problems, because it might become possible for users to traverse from the objects created, to those vm primordials, exposing them to the same hazard. |
Right. You could have to hold onto the actual functions and perform Function.prototype.call on them. Same goes for Function.prototype.call itself, obviously. |
I might be missing something here, but why would we try to "protect" node.js core from such modifications? If people monkey-patch existing prototypes etc., then any breakage will be their fault, and the |
@tniessen If node breaks as a result of JS code running, it's node's fault. Since a node app runs with JS (and C) code written by many authors, it's simply not true that the user experiencing the breakage will necessarily be that user's fault. |
(a good example right now is doing |
I think we can probably be safer around this than we are currently — a notable example being that we often call On the opposite end, IMHO, users modifying (But that then opens up a whole new can of worms because we know that our module wrappers are actually escapable.) Just my 2 cents and it's more than likely I'm wrong on this. |
@apapirovski the user is almost certainly doing that intentionally; but that should not change how core behaves. Basically, if I can observe that any part of node core is written in JS, then there's been an encapsulation failure, full stop. |
I highly doubt making assumptions about the used programming language indicates such a failure, or if it does, has any relevance. > ''.replace.toString()
'function replace() { [native code] }'
> fs.readFile.toString()
'function (path, options, callback) {\n callback = maybeCallback(callback || options);\n options = getOptions(options, { flag: \'r\' });\n\n if (handleError((path = getPathFromURL(path)), callback))\n return;\n if (!nullCheck(path, callback))\n return;\n\n var context = new ReadFileContext(callback, options.encoding);\n context.isUserFd = isFd(path); // file descriptor ownership\n var req = new FSReqWrap();\n req.context = context;\n req.oncomplete = readFileAfterOpen;\n\n if (context.isUserFd) {\n process.nextTick(function() {\n req.oncomplete(null, path);\n });\n return;\n }\n\n binding.open(pathModule._makeLong(path),\n stringToFlags(options.flag || \'r\'),\n 0o666,\n req);\n}'
// Encapsulation failure, this function was written in JS!!! If people monkey-patch things in a way that breaks existing code, they should not expect everything to keep working. If you ever used Detours on Windows (or literally any other API hooking library on any platform), you will notice that you can easily break standard APIs (which are not system calls) by messing with the function lookup tables. There are dozens of ways to interfere with others' code, and JavaScript makes it particularly easy. // Another example of "encapsulation failure"
const fs = require('fs');
fs.readFile = null;
// In another module (okay unless this happens in core)
const fs = require('fs');
fs.readFile('foo', (err) => { console.log(err); });
// Oops, crashed I don't have a strong opinion on this, but I think what you might call "protection" against this should not affect the maintainability, readability or performance of node.js. As long as it doesn't, I am okay with any changes leading to "protection" of the core, such that people can only break their code and other people's code, including required modules, but luckily, the core APIs will still work. |
does anyone have any objections to adding |
I'd prefer to have it in an internal module. It could be a module where we export all safe builtins that we need. They could even be pre-uncurried. Then we could do anywhere: const {
uncurried: { Object_proto_hasOwnProperty },
Object_keys
} = require('internal/builtins');
Object_proto_hasOwnProperty({x: 1}, 'x') // true
Object_keys({x: 1}) // ['x'] PS: I haven't really thought about the names |
Still not sure how I feel about this outside of the modules implementation. If we were going in this direction, I honestly wish we could find a way to have safe object prototypes within our wrappers. I don't like the idea of adjusting existing code with this half-solution which also makes code more difficult to understand and harder to maintain. Allowing contributors to write JS the way that they're used to makes for a much friendlier initial experience and makes for one less obstacle in the way of making one's first PR. The learning curve in contributing to |
Core could use a babel transform to transform statics into these alternate safe versions. |
i feel nervous about that just because it would turn error messages from core into spaghetti, unless we also packaged and used source maps for errors, which just sounds bloated |
We could execute code in a frozen realm for core, but that too has problems with marshaling data between realms. I think exposing a large list of primordials is easier overall than a transform. |
We had the same issue in V8's internal JS code. What we ended up doing is storing functions required by internal code in the function context of the function that sets up internal JS during bootstrap. That incurred no additional cost other than context slots because bootstrapping is performed at build time and baked into the startup snapshot. Node sure how well that maps to Node.js. It doesn't work for lazily loaded modules. |
@hashseed is that something we can easily do/any docs or examples on it? (i'd be happy to implement it if possible) |
It's basically adding |
so basically @targos's suggestion? |
Yes. |
One way you could police that is to overwrite all builtin prototypes with proxies that assert that the last stack frame is not from internal lib. Not for production of course. |
closing in favor of #18795 |
In a PR i'm working on @ljharb pointed out several cases where using things like
String.prototype.replace
orfs.readFileSync
are unsafe because user code could override them, forcing me to use things likeconst StringReplace = Function.call.bind(String.prototype.replace)
and use that instead. A fair amount of node code uses this pattern, and a fair amount doesn't guard against this at all. I opened this issue to create a discussion about what the pattern should be moving forward, if there are things we can do to prevent this behavior from affection core libs, etc.The text was updated successfully, but these errors were encountered: