Skip to content

Commit 83975b7

Browse files
RafaelGSSbmeck
authored andcommittedFeb 15, 2023
policy: makeRequireFunction on mainModule.require
Signed-off-by: RafaelGSS <rafael.nunu@hotmail.com> Co-authored-by: Bradley Farias <bradley.meck@gmail.com> Backport-PR-URL: nodejs-private/node-private#373 Refs: https://hackerone.com/bugs?subject=nodejs&report_id=1747642 CVE-ID: CVE-2023-23918 PR-URL: nodejs-private/node-private#358 Reviewed-by: Bradley Farias <bradley.meck@gmail.com> Reviewed-by: Michael Dawson <midawson@redhat.com>
1 parent fa115ee commit 83975b7

10 files changed

+158
-30
lines changed
 

‎lib/internal/modules/cjs/helpers.js

+7-3
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ const { getOptionValue } = require('internal/options');
2626
const { setOwnProperty } = require('internal/util');
2727
const userConditions = getOptionValue('--conditions');
2828

29+
const {
30+
require_private_symbol,
31+
} = internalBinding('util');
32+
2933
let debug = require('internal/util/debuglog').debuglog('module', (fn) => {
3034
debug = fn;
3135
});
@@ -85,7 +89,7 @@ function makeRequireFunction(mod, redirects) {
8589
filepath = fileURLToPath(destination);
8690
urlToFileCache.set(href, filepath);
8791
}
88-
return mod.require(filepath);
92+
return mod[require_private_symbol](mod, filepath);
8993
}
9094
}
9195
if (missing) {
@@ -95,11 +99,11 @@ function makeRequireFunction(mod, redirects) {
9599
ArrayPrototypeJoin([...conditions], ', ')
96100
));
97101
}
98-
return mod.require(specifier);
102+
return mod[require_private_symbol](mod, specifier);
99103
};
100104
} else {
101105
require = function require(path) {
102-
return mod.require(path);
106+
return mod[require_private_symbol](mod, path);
103107
};
104108
}
105109

‎lib/internal/modules/cjs/loader.js

+36-25
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,11 @@ const {
7676
maybeCacheSourceMap,
7777
} = require('internal/source_map/source_map_cache');
7878
const { pathToFileURL, fileURLToPath, isURLInstance } = require('internal/url');
79-
const { deprecate, filterOwnProperties, setOwnProperty } = require('internal/util');
79+
const {
80+
deprecate,
81+
filterOwnProperties,
82+
setOwnProperty,
83+
} = require('internal/util');
8084
const vm = require('vm');
8185
const assert = require('internal/assert');
8286
const fs = require('fs');
@@ -86,6 +90,9 @@ const { sep } = path;
8690
const { internalModuleStat } = internalBinding('fs');
8791
const packageJsonReader = require('internal/modules/package_json_reader');
8892
const { safeGetenv } = internalBinding('credentials');
93+
const {
94+
require_private_symbol,
95+
} = internalBinding('util');
8996
const {
9097
makeRequireFunction,
9198
normalizeReferrerURL,
@@ -143,6 +150,20 @@ let requireDepth = 0;
143150
let statCache = null;
144151
let isPreloading = false;
145152

153+
function internalRequire(module, id) {
154+
validateString(id, 'id');
155+
if (id === '') {
156+
throw new ERR_INVALID_ARG_VALUE('id', id,
157+
'must be a non-empty string');
158+
}
159+
requireDepth++;
160+
try {
161+
return Module._load(id, module, /* isMain */ false);
162+
} finally {
163+
requireDepth--;
164+
}
165+
}
166+
146167
function stat(filename) {
147168
filename = path.toNamespacedPath(filename);
148169
if (statCache !== null) {
@@ -169,6 +190,15 @@ function Module(id = '', parent) {
169190
this.filename = null;
170191
this.loaded = false;
171192
this.children = [];
193+
let redirects;
194+
if (policy?.manifest) {
195+
const moduleURL = pathToFileURL(id);
196+
redirects = policy.manifest.getDependencyMapper(moduleURL);
197+
}
198+
setOwnProperty(this, 'require', makeRequireFunction(this, redirects));
199+
// Loads a module at the given file path. Returns that module's
200+
// `exports` property.
201+
this[require_private_symbol] = internalRequire;
172202
}
173203

174204
const builtinModules = [];
@@ -776,6 +806,7 @@ Module._load = function(request, parent, isMain) {
776806

777807
if (isMain) {
778808
process.mainModule = module;
809+
setOwnProperty(module.require, 'main', process.mainModule);
779810
module.id = '.';
780811
}
781812

@@ -959,24 +990,6 @@ Module.prototype.load = function(filename) {
959990
ESMLoader.cjsCache.set(this, exports);
960991
};
961992

962-
963-
// Loads a module at the given file path. Returns that module's
964-
// `exports` property.
965-
Module.prototype.require = function(id) {
966-
validateString(id, 'id');
967-
if (id === '') {
968-
throw new ERR_INVALID_ARG_VALUE('id', id,
969-
'must be a non-empty string');
970-
}
971-
requireDepth++;
972-
try {
973-
return Module._load(id, this, /* isMain */ false);
974-
} finally {
975-
requireDepth--;
976-
}
977-
};
978-
979-
980993
// Resolved path to process.argv[1] will be lazily placed here
981994
// (needed for setting breakpoint when called with --inspect-brk)
982995
let resolvedArgv;
@@ -1037,10 +1050,9 @@ function wrapSafe(filename, content, cjsModuleInstance) {
10371050
// Returns exception, if any.
10381051
Module.prototype._compile = function(content, filename) {
10391052
let moduleURL;
1040-
let redirects;
10411053
if (policy?.manifest) {
10421054
moduleURL = pathToFileURL(filename);
1043-
redirects = policy.manifest.getDependencyMapper(moduleURL);
1055+
policy.manifest.getDependencyMapper(moduleURL);
10441056
policy.manifest.assertIntegrity(moduleURL, content);
10451057
}
10461058

@@ -1071,18 +1083,17 @@ Module.prototype._compile = function(content, filename) {
10711083
}
10721084
}
10731085
const dirname = path.dirname(filename);
1074-
const require = makeRequireFunction(this, redirects);
10751086
let result;
10761087
const exports = this.exports;
10771088
const thisValue = exports;
10781089
const module = this;
10791090
if (requireDepth === 0) statCache = new SafeMap();
10801091
if (inspectorWrapper) {
10811092
result = inspectorWrapper(compiledWrapper, thisValue, exports,
1082-
require, module, filename, dirname);
1093+
module.require, module, filename, dirname);
10831094
} else {
10841095
result = ReflectApply(compiledWrapper, thisValue,
1085-
[exports, require, module, filename, dirname]);
1096+
[exports, module.require, module, filename, dirname]);
10861097
}
10871098
hasLoadedAnyUserCJSModule = true;
10881099
if (requireDepth === 0) statCache = null;
@@ -1240,7 +1251,7 @@ Module._preloadModules = function(requests) {
12401251
}
12411252
}
12421253
for (let n = 0; n < requests.length; n++)
1243-
parent.require(requests[n]);
1254+
internalRequire(parent, requests[n]);
12441255
isPreloading = false;
12451256
};
12461257

‎src/env.h

+1
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ constexpr size_t kFsStatsBufferLength =
153153
V(napi_type_tag, "node:napi:type_tag") \
154154
V(napi_wrapper, "node:napi:wrapper") \
155155
V(untransferable_object_private_symbol, "node:untransferableObject") \
156+
V(require_private_symbol, "node:require_private_symbol")
156157

157158
// Symbols are per-isolate primitives but Environment proxies them
158159
// for the sake of convenience.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
process.mainModule.require('os').cpus();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
let requires = new WeakMap()
2+
Object.defineProperty(Object.getPrototypeOf(module), 'require', {
3+
get() {
4+
return requires.get(this);
5+
},
6+
set(v) {
7+
requires.set(this, v);
8+
process.nextTick(() => {
9+
let fs = Reflect.apply(v, this, ['fs'])
10+
if (typeof fs.readFileSync === 'function') {
11+
process.exit(1);
12+
}
13+
})
14+
return requires.get(this);
15+
},
16+
configurable: true
17+
})
18+
19+
require('./valid-module')
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"onerror": "exit",
3+
"scopes": {
4+
"file:": {
5+
"integrity": true,
6+
"dependencies": {}
7+
}
8+
}
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"onerror": "exit",
3+
"resources": {
4+
"./object-define-property-bypass.js": {
5+
"integrity": true,
6+
"dependencies": {
7+
"./valid-module": true
8+
}
9+
},
10+
"./valid-module.js": {
11+
"integrity": true,
12+
"dependencies": {
13+
"fs": true
14+
}
15+
}
16+
}
17+
}

‎test/fixtures/policy-manifest/valid-module.js

Whitespace-only changes.

‎test/parallel/test-module-prototype-mutation.js

+5-2
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@ const fixtures = require('../common/fixtures');
44
const assert = require('assert');
55

66
assert.strictEqual(
7-
require(fixtures.path('es-module-specifiers', 'node_modules', 'no-main-field')),
7+
require(
8+
fixtures.path('es-module-specifiers', 'node_modules', 'no-main-field')
9+
),
810
'no main field'
911
);
1012

1113
import(fixtures.fileURL('es-module-specifiers', 'index.mjs'))
12-
.then(common.mustCall((module) => assert.strictEqual(module.noMain, 'no main field')));
14+
.then(common.mustCall((module) =>
15+
assert.strictEqual(module.noMain, 'no main field')));

‎test/parallel/test-policy-manifest.js

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
5+
if (!common.hasCrypto)
6+
common.skip('missing crypto');
7+
8+
common.requireNoPackageJSONAbove();
9+
10+
const assert = require('assert');
11+
const { spawnSync } = require('child_process');
12+
const fixtures = require('../common/fixtures.js');
13+
14+
{
15+
const policyFilepath = fixtures.path('policy-manifest', 'onerror-exit.json');
16+
const result = spawnSync(process.execPath, [
17+
'--experimental-policy',
18+
policyFilepath,
19+
'-e',
20+
'require("os").cpus()',
21+
]);
22+
23+
assert.notStrictEqual(result.status, 0);
24+
const stderr = result.stderr.toString();
25+
assert.match(stderr, /ERR_MANIFEST_DEPENDENCY_MISSING/);
26+
assert.match(stderr, /does not list os as a dependency specifier for conditions: require, node, node-addons/);
27+
}
28+
29+
{
30+
const policyFilepath = fixtures.path('policy-manifest', 'onerror-exit.json');
31+
const mainModuleBypass = fixtures.path(
32+
'policy-manifest',
33+
'main-module-bypass.js',
34+
);
35+
const result = spawnSync(process.execPath, [
36+
'--experimental-policy',
37+
policyFilepath,
38+
mainModuleBypass,
39+
]);
40+
41+
assert.notStrictEqual(result.status, 0);
42+
const stderr = result.stderr.toString();
43+
assert.match(stderr, /ERR_MANIFEST_DEPENDENCY_MISSING/);
44+
assert.match(stderr, /does not list os as a dependency specifier for conditions: require, node, node-addons/);
45+
}
46+
47+
{
48+
const policyFilepath = fixtures.path(
49+
'policy-manifest',
50+
'onerror-resource-exit.json',
51+
);
52+
const objectDefinePropertyBypass = fixtures.path(
53+
'policy-manifest',
54+
'object-define-property-bypass.js',
55+
);
56+
const result = spawnSync(process.execPath, [
57+
'--experimental-policy',
58+
policyFilepath,
59+
objectDefinePropertyBypass,
60+
]);
61+
62+
assert.strictEqual(result.status, 0);
63+
}

0 commit comments

Comments
 (0)
Please sign in to comment.