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

lib: be robust when process global is clobbered #10135

Closed

Conversation

bnoordhuis
Copy link
Member

@bnoordhuis bnoordhuis commented Dec 6, 2016

Program bugs sometimes happen to overwrite the process global object,
leading to unpredictable runtime errors. The bug is often hard to track
down because the crash usually looks unrelated and happens far away from
the bug point.

Make core mostly immune to such corruption by ensuring that all core
modules close over a direct reference to the process object instead
of a dynamic reference.

CI: https://ci.nodejs.org/job/node-test-pull-request/5234/

@nodejs-github-bot nodejs-github-bot added lib / src Issues and PRs related to general changes in the lib or src directory. module Issues and PRs related to the module subsystem. labels Dec 6, 2016
@mscdex mscdex removed the lib / src Issues and PRs related to general changes in the lib or src directory. label Dec 6, 2016
@jasnell
Copy link
Member

jasnell commented Dec 6, 2016

Nice. I like it.
I want to be careful on this tho... would this have to be a semver-major? (please say no)

/* eslint-disable no-global-assign */
/* eslint-disable required-modules */
'use strict';

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

require('../common');

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added the required-modules eslint directive so I didn't have to include test/common.js, because while core is now robust to tampering, the code in test/common.js need not be.

@Fishrock123
Copy link
Contributor

This probably won't work in ES Modules, also this changes this.length... idk if these is some strange wrapper that might rely on that or not.

@bnoordhuis
Copy link
Member Author

This probably won't work in ES Modules

Why would that be an issue for built-in modules?

also this changes this.length... idk if these is some strange wrapper that might rely on that or not

Seems unlikely. Is there a plausible scenario where it would make a difference? It can't be monkey-patching the module loader because core modules use (the inaccessible to the outside world)NativeModule.require, not regular require.

Copy link
Member

@addaleax addaleax left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@@ -504,7 +504,7 @@
};

NativeModule.wrapper = [
'(function (exports, require, module, __filename, __dirname) { ',
'(function (exports, require, module, __filename, __dirname, process) { ',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess you might as well lose the __dirname argument here?

@sam-github
Copy link
Contributor

I'm +1 on this.

Can process be made read-only, so it can't be assigned null? Are people deleting the process global on purpose, or accident?

Program bugs sometimes happen to overwrite the `process` global object,
leading to unpredictable runtime errors.  The bug is often hard to track
down because the crash usually looks unrelated and happens far away from
the bug point.

Make core mostly immune to such corruption by ensuring that all core
modules close over a direct reference to the process object instead
of a dynamic reference.
@bnoordhuis
Copy link
Member Author

bnoordhuis commented Dec 11, 2016

@addaleax Removed __dirname.

@sam-github I've added a commit that makes process non-writable (but still configurable), PTAL.

CI: https://ci.nodejs.org/job/node-test-pull-request/5355/

EDIT: To answer your question: accidental.

@addaleax
Copy link
Member

Still LGTM but tbh I’d feel a bit better considering the second commit to be semver-major?

@bnoordhuis
Copy link
Member Author

No strong feelings. semver-major is fine by me.

@@ -504,7 +513,7 @@
};

NativeModule.wrapper = [
'(function (exports, require, module, __filename, __dirname) { ',
'(function (exports, require, module, __filename, process) { ',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its confusing that there are two wrappers now, and they are different.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Am I reading this correctly, internal modules no longer have __dirname? I guess it would be too big a change to have process be injected as a module argument for all modules, internal and external, rather than be in globals? Ben, was there discussion about whether node's "globals" should be true-global or module locals? module/exports/__filename/__dirname have meaning only in local context, but why did require() become a local?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its confusing that there are two wrappers now, and they are different.

That's the price of progress.

Am I reading this correctly, internal modules no longer have __dirname?

They never really did, it was always undefined because built-in modules don't live on disk.

I guess it would be too big a change to have process be injected as a module argument for all modules, internal and external, rather than be in globals?

I'm not against that per se but that's a bigger change and probably needs more discussion.

why did require() become a local?

I believe it always was. It sort of has to be because it works relative to the calling script.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assumed it knew its current module using some other mechanism, but that makes sense. Also about __dirname.

@italoacasas italoacasas added the wip Issues and PRs that are still a work in progress. label Dec 12, 2016
@evanlucas
Copy link
Contributor

@sam-github every time I've seen it happen has been someone naming a function process

@jasnell jasnell added the semver-major PRs that contain breaking changes and should be released in the next major version. label Dec 27, 2016
@jasnell
Copy link
Member

jasnell commented Dec 27, 2016

@nodejs/ctc ... any thoughts on this?

Copy link
Member

@indutny indutny left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@rvagg
Copy link
Member

rvagg commented Dec 28, 2016

+0, I can see this causing some hurt but it's probably a good idea

@rvagg
Copy link
Member

rvagg commented Dec 28, 2016

Actually, a thought: could this mess up people who use spies or mocks in place of process in their tests?

@bmeck
Copy link
Member

bmeck commented Jan 12, 2017

I know at one point N|Solid's agent was heavily guarding against this style of attack by creating local const even to perform calls. Might take inspiration from that.

@@ -196,7 +200,12 @@
enumerable: false,
configurable: true
});
global.process = process;
Object.defineProperty(global, 'process', {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is needed / warranted. If someone wants to delete global.process it should still work. Natives having proper refs to process is already achieved via the wrapper hange.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was requested by @sam-github in #10135 (comment).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I as well as @ljharb disagree with this change.

Copy link
Member

@bmeck bmeck Jan 13, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

basically, if we set this precedent I would be much more comfortable locking down all things that core could depend on, either by adding them to a closure ala https://github.com/bmeck/node/tree/no-globals or by freezing them on the global. There are many of these globals that break npm modules in the wild if they are overwritten. I do not see process as exceptionally high use vs Error for example. I am against freezing them on the global for polyfill reasons.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there many? https://nodejs.org/api/globals.html documents only a bit more than a dozen "globals", and half of them aren't global, they are module-scoped variables. Perhaps all the "globals" should be module-scoped variables? So that its impossible to mess with them globally?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this does not include prototype hijacking protection, thats raw global refs.

require('../common');
const assert = require('assert');

assert.throws(() => process = null, /Cannot assign to read only property/);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this should throw. Natives should be robust w/o relying on globals once startup finishes

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The built-in libraries are, this checks that process is not assignable per the second commit.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that is appropriate to lock down.

@bmeck
Copy link
Member

bmeck commented Jan 12, 2017

make lint on https://github.com/bmeck/node/tree/no-globals [was] showing more than 8000 unchecked global accesses (ones that don't come from a require)

@addaleax
Copy link
Member

Btw, Electron ships with a different module wrapper, maybe it’s worth considering a change like the one proposed here?

@bnoordhuis
Copy link
Member Author

Btw, Electron ships with a different module wrapper, maybe it’s worth considering a change like the one proposed here?

That issue makes a good point about breaking const process = ... but the proposed solution introduces an extra function closure for every require() call.

@sam-github
Copy link
Contributor

While I am guilty of suggesting this, I'm also OK to give node users enough guns to shoot themselves in the foot. Its possible to creatively or accidently corrupt your runtime in many languages, node is no exception, so if we want to leave that as a possibility, I'm OK with it.

@ljharb
Copy link
Member

ljharb commented Jan 13, 2017

The language runtime, which includes process, should indeed behave that way. Core code, however, should not be so fragile as to be easily broken by users doing inadvisable things.

@bmeck
Copy link
Member

bmeck commented Jan 13, 2017

@ljharb unfortunately some popular libraries mutate internal prototypes so to some extent there is a need to preserve some inadvisable behavior.

@sam-github
Copy link
Contributor

Unless we freeze all the prototypes of built-in objects, breaking most node monitoring products as a pretty nasty side-effect, the runtime can be broken by users. Its just a question of how hard we want to make them work to do it.

@ljharb
Copy link
Member

ljharb commented Jan 13, 2017

Again, that's fine in the runtime, but core code should not be broken by those changes.

@sam-github
Copy link
Contributor

@ljharb I actually don't know what you mean by attempting to distinguish "runtime" and "core node". Those words mean the same thing to me, but apparently not to you, can you define them?

@bmeck
Copy link
Member

bmeck commented Jan 13, 2017

@ljharb some libraries like express change how internals work, in ways that could potentially break core assumptions. Node just was not designed to defend against monkey patching.

@ljharb
Copy link
Member

ljharb commented Jan 13, 2017

@sam-github I'm saying that the behavior of non-spec (ie, non-language) code that the user did not write or install should not be changeable by user code in unintended ways.

@bmeck and I think that "changing how internals work" is something that shouldn't be wantonly permitted (ie, outside of documented/established/endorsed mechanisms to do so), and that if that warrants breaking changes to make node core more robust against third-party code I run making my own code behave in ways I can't defend against (I can defend against language builtins being modified; I can't defend against core being modified), then that's worth it.

@bmeck
Copy link
Member

bmeck commented Jan 13, 2017

I suggest adding a new CTC discussion around the definition of robust as a goal here and if it is limited to some subset of globals used by Core.

@ChALkeR
Copy link
Member

ChALkeR commented Jan 18, 2017

Note that a significant amount of packages seem to override global.process under various circumstances.

Examples:

Some others do global.process = require('process'), probably for browserifying reasons.

@ChALkeR
Copy link
Member

ChALkeR commented Jan 25, 2017

@ChALkeR
Copy link
Member

ChALkeR commented Feb 2, 2017

I expect making global.process non-writable will break some things (most of which are testcases), including npm tests. It doesn't look like those are ran during a CI or CITGM, so they should be manually triggered, I presume. /cc @MylesBorins

@targos
Copy link
Member

targos commented Feb 2, 2017

why don't we just add const process = require('process') in all the lib files where it's needed, and make sure the process module is loaded before executing any user code?

@jasnell
Copy link
Member

jasnell commented Mar 22, 2017

@bnoordhuis ... how do you want to proceed with this one?

@jasnell jasnell added the stalled Issues and PRs that are stalled. label Mar 24, 2017
@bnoordhuis
Copy link
Member Author

I'm going to close and revisit when I have time.

@bnoordhuis bnoordhuis closed this Apr 19, 2017
@bmeck bmeck mentioned this pull request Jul 26, 2017
4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
module Issues and PRs related to the module subsystem. semver-major PRs that contain breaking changes and should be released in the next major version. stalled Issues and PRs that are stalled.
Projects
None yet
Development

Successfully merging this pull request may close these issues.