Skip to content

Commit 6d17b69

Browse files
RafaelGSSbmeck
andcommitted
policy: makeRequireFunction on mainModule.require
Signed-off-by: RafaelGSS <rafael.nunu@hotmail.com> Co-authored-by: Bradley Farias <bradley.meck@gmail.com> Refs: https://hackerone.com/bugs?subject=nodejs&report_id=1747642 CVE-ID: CVE-2023-23918 PR-URL: #358 Reviewed-by: Bradley Farias <bradley.meck@gmail.com> Reviewed-by: Michael Dawson <midawson@redhat.com>
1 parent 10a4c47 commit 6d17b69

9 files changed

+147
-40
lines changed

lib/internal/modules/cjs/loader.js

+35-24
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,12 @@ const path = require('path');
9696
const { sep } = path;
9797
const { internalModuleStat } = internalBinding('fs');
9898
const { safeGetenv } = internalBinding('credentials');
99+
const {
100+
privateSymbols: {
101+
require_private_symbol,
102+
},
103+
} = internalBinding('util');
104+
99105
const {
100106
getCjsConditions,
101107
initializeCjsConditions,
@@ -151,6 +157,20 @@ let requireDepth = 0;
151157
let statCache = null;
152158
let isPreloading = false;
153159

160+
function internalRequire(module, id) {
161+
validateString(id, 'id');
162+
if (id === '') {
163+
throw new ERR_INVALID_ARG_VALUE('id', id,
164+
'must be a non-empty string');
165+
}
166+
requireDepth++;
167+
try {
168+
return Module._load(id, module, /* isMain */ false);
169+
} finally {
170+
requireDepth--;
171+
}
172+
}
173+
154174
function stat(filename) {
155175
filename = path.toNamespacedPath(filename);
156176
if (statCache !== null) {
@@ -205,6 +225,16 @@ function Module(id = '', parent) {
205225
this.filename = null;
206226
this.loaded = false;
207227
this.children = [];
228+
let redirects;
229+
const manifest = policy()?.manifest;
230+
if (manifest) {
231+
const moduleURL = pathToFileURL(id);
232+
redirects = manifest.getDependencyMapper(moduleURL);
233+
}
234+
setOwnProperty(this, 'require', makeRequireFunction(this, redirects));
235+
// Loads a module at the given file path. Returns that module's
236+
// `exports` property.
237+
this[require_private_symbol] = internalRequire;
208238
}
209239

210240
Module._cache = { __proto__: null };
@@ -927,6 +957,7 @@ Module._load = function(request, parent, isMain) {
927957

928958
if (isMain) {
929959
process.mainModule = module;
960+
setOwnProperty(module.require, 'main', process.mainModule);
930961
module.id = '.';
931962
}
932963

@@ -1113,24 +1144,6 @@ Module.prototype.load = function(filename) {
11131144
cascadedLoader.cjsCache.set(this, exports);
11141145
};
11151146

1116-
1117-
// Loads a module at the given file path. Returns that module's
1118-
// `exports` property.
1119-
Module.prototype.require = function(id) {
1120-
validateString(id, 'id');
1121-
if (id === '') {
1122-
throw new ERR_INVALID_ARG_VALUE('id', id,
1123-
'must be a non-empty string');
1124-
}
1125-
requireDepth++;
1126-
try {
1127-
return Module._load(id, this, /* isMain */ false);
1128-
} finally {
1129-
requireDepth--;
1130-
}
1131-
};
1132-
1133-
11341147
// Resolved path to process.argv[1] will be lazily placed here
11351148
// (needed for setting breakpoint when called with --inspect-brk)
11361149
let resolvedArgv;
@@ -1199,11 +1212,10 @@ function wrapSafe(filename, content, cjsModuleInstance) {
11991212
// Returns exception, if any.
12001213
Module.prototype._compile = function(content, filename) {
12011214
let moduleURL;
1202-
let redirects;
12031215
const manifest = policy()?.manifest;
12041216
if (manifest) {
12051217
moduleURL = pathToFileURL(filename);
1206-
redirects = manifest.getDependencyMapper(moduleURL);
1218+
manifest.getDependencyMapper(moduleURL);
12071219
manifest.assertIntegrity(moduleURL, content);
12081220
}
12091221

@@ -1233,18 +1245,17 @@ Module.prototype._compile = function(content, filename) {
12331245
}
12341246
}
12351247
const dirname = path.dirname(filename);
1236-
const require = makeRequireFunction(this, redirects);
12371248
let result;
12381249
const exports = this.exports;
12391250
const thisValue = exports;
12401251
const module = this;
12411252
if (requireDepth === 0) statCache = new SafeMap();
12421253
if (inspectorWrapper) {
12431254
result = inspectorWrapper(compiledWrapper, thisValue, exports,
1244-
require, module, filename, dirname);
1255+
module.require, module, filename, dirname);
12451256
} else {
12461257
result = ReflectApply(compiledWrapper, thisValue,
1247-
[exports, require, module, filename, dirname]);
1258+
[exports, module.require, module, filename, dirname]);
12481259
}
12491260
hasLoadedAnyUserCJSModule = true;
12501261
if (requireDepth === 0) statCache = null;
@@ -1422,7 +1433,7 @@ Module._preloadModules = function(requests) {
14221433
}
14231434
}
14241435
for (let n = 0; n < requests.length; n++)
1425-
parent.require(requests[n]);
1436+
internalRequire(parent, requests[n]);
14261437
isPreloading = false;
14271438
};
14281439

lib/internal/modules/helpers.js

+9-3
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ const { pathToFileURL, fileURLToPath, URL } = require('internal/url');
2626
const { getOptionValue } = require('internal/options');
2727
const { setOwnProperty } = require('internal/util');
2828

29+
const {
30+
privateSymbols: {
31+
require_private_symbol,
32+
},
33+
} = internalBinding('util');
34+
2935
let debug = require('internal/util/debuglog').debuglog('module', (fn) => {
3036
debug = fn;
3137
});
@@ -95,7 +101,7 @@ function makeRequireFunction(mod, redirects) {
95101
filepath = fileURLToPath(destination);
96102
urlToFileCache.set(href, filepath);
97103
}
98-
return mod.require(filepath);
104+
return mod[require_private_symbol](mod, filepath);
99105
}
100106
}
101107
if (missing) {
@@ -105,11 +111,11 @@ function makeRequireFunction(mod, redirects) {
105111
ArrayPrototypeJoin([...conditions], ', ')
106112
));
107113
}
108-
return mod.require(specifier);
114+
return mod[require_private_symbol](mod, specifier);
109115
};
110116
} else {
111117
require = function require(path) {
112-
return mod.require(path);
118+
return mod[require_private_symbol](mod, path);
113119
};
114120
}
115121

src/env_properties.h

+2-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
V(napi_type_tag, "node:napi:type_tag") \
2525
V(napi_wrapper, "node:napi:wrapper") \
2626
V(untransferable_object_private_symbol, "node:untransferableObject") \
27-
V(exiting_aliased_Uint32Array, "node:exiting_aliased_Uint32Array")
27+
V(exiting_aliased_Uint32Array, "node:exiting_aliased_Uint32Array") \
28+
V(require_private_symbol, "node:require_private_symbol")
2829

2930
// Symbols are per-isolate primitives but Environment proxies them
3031
// 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-policy-manifest.js

+55-12
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,58 @@ const assert = require('assert');
1111
const { spawnSync } = require('child_process');
1212
const fixtures = require('../common/fixtures.js');
1313

14-
const policyFilepath = fixtures.path('policy-manifest', 'invalid.json');
15-
16-
const result = spawnSync(process.execPath, [
17-
'--experimental-policy',
18-
policyFilepath,
19-
'./fhqwhgads.js',
20-
]);
21-
22-
assert.notStrictEqual(result.status, 0);
23-
const stderr = result.stderr.toString();
24-
assert.match(stderr, /ERR_MANIFEST_INVALID_SPECIFIER/);
25-
assert.match(stderr, /pattern needs to have a single trailing "\*"/);
14+
{
15+
const policyFilepath = fixtures.path('policy-manifest', 'invalid.json');
16+
const result = spawnSync(process.execPath, [
17+
'--experimental-policy',
18+
policyFilepath,
19+
'./fhqwhgads.js',
20+
]);
21+
22+
assert.notStrictEqual(result.status, 0);
23+
const stderr = result.stderr.toString();
24+
assert.match(stderr, /ERR_MANIFEST_INVALID_SPECIFIER/);
25+
assert.match(stderr, /pattern needs to have a single trailing "\*"/);
26+
}
27+
28+
{
29+
const policyFilepath = fixtures.path('policy-manifest', 'onerror-exit.json');
30+
const result = spawnSync(process.execPath, [
31+
'--experimental-policy',
32+
policyFilepath,
33+
'-e',
34+
'require("os").cpus()',
35+
]);
36+
37+
assert.notStrictEqual(result.status, 0);
38+
const stderr = result.stderr.toString();
39+
assert.match(stderr, /ERR_MANIFEST_DEPENDENCY_MISSING/);
40+
assert.match(stderr, /does not list module as a dependency specifier for conditions: require, node, node-addons/);
41+
}
42+
43+
{
44+
const policyFilepath = fixtures.path('policy-manifest', 'onerror-exit.json');
45+
const mainModuleBypass = fixtures.path('policy-manifest', 'main-module-bypass.js');
46+
const result = spawnSync(process.execPath, [
47+
'--experimental-policy',
48+
policyFilepath,
49+
mainModuleBypass,
50+
]);
51+
52+
assert.notStrictEqual(result.status, 0);
53+
const stderr = result.stderr.toString();
54+
assert.match(stderr, /ERR_MANIFEST_DEPENDENCY_MISSING/);
55+
assert.match(stderr, /does not list os as a dependency specifier for conditions: require, node, node-addons/);
56+
}
57+
58+
{
59+
const policyFilepath = fixtures.path('policy-manifest', 'onerror-resource-exit.json');
60+
const objectDefinePropertyBypass = fixtures.path('policy-manifest', 'object-define-property-bypass.js');
61+
const result = spawnSync(process.execPath, [
62+
'--experimental-policy',
63+
policyFilepath,
64+
objectDefinePropertyBypass,
65+
]);
66+
67+
assert.strictEqual(result.status, 0);
68+
}

0 commit comments

Comments
 (0)