Skip to content

Commit d8ccfe9

Browse files
committedAug 8, 2023
policy: handle Module.constructor and main.extensions bypass
Signed-off-by: RafaelGSS <rafael.nunu@hotmail.com> PR-URL: nodejs-private/node-private#445 Refs: https://hackerone.com/bugs?subject=nodejs&report_id=1960870 Refs: https://hackerone.com/bugs?subject=nodejs&report_id=2043807 CVE-ID: CVE-2023-32002,CVE-2023-32006
1 parent 242aaa0 commit d8ccfe9

9 files changed

+97
-2
lines changed
 

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

+12-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const {
1414
StringPrototypeStartsWith,
1515
} = primordials;
1616
const {
17+
ERR_INVALID_ARG_TYPE,
1718
ERR_MANIFEST_DEPENDENCY_MISSING,
1819
ERR_UNKNOWN_BUILTIN_MODULE
1920
} = require('internal/errors').codes;
@@ -56,12 +57,22 @@ function loadBuiltinModule(filename, request) {
5657
}
5758
}
5859

60+
let $Module = null;
61+
function lazyModule() {
62+
$Module = $Module || require('internal/modules/cjs/loader').Module;
63+
return $Module;
64+
}
65+
5966
// Invoke with makeRequireFunction(module) where |module| is the Module object
6067
// to use as the context for the require() function.
6168
// Use redirects to set up a mapping from a policy and restrict dependencies
6269
const urlToFileCache = new SafeMap();
6370
function makeRequireFunction(mod, redirects) {
64-
const Module = mod.constructor;
71+
// lazy due to cycle
72+
const Module = lazyModule();
73+
if (mod instanceof Module !== true) {
74+
throw new ERR_INVALID_ARG_TYPE('mod', 'Module', mod);
75+
}
6576

6677
let require;
6778
if (redirects) {

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

+10-1
Original file line numberDiff line numberDiff line change
@@ -155,8 +155,8 @@ const isWindows = process.platform === 'win32';
155155
const relativeResolveCache = ObjectCreate(null);
156156

157157
let requireDepth = 0;
158-
let statCache = null;
159158
let isPreloading = false;
159+
let statCache = null;
160160

161161
function internalRequire(module, id) {
162162
validateString(id, 'id');
@@ -1389,5 +1389,14 @@ Module.isBuiltin = function isBuiltin(moduleName) {
13891389
return allBuiltins.has(moduleName);
13901390
};
13911391

1392+
ObjectDefineProperty(Module.prototype, 'constructor', {
1393+
__proto__: null,
1394+
get: function() {
1395+
return policy ? undefined : Module;
1396+
},
1397+
configurable: false,
1398+
enumerable: false,
1399+
});
1400+
13921401
// Backwards compatibility
13931402
Module.Module = Module;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
const os = module.constructor.createRequire('file:///os-access-module.js')('os')
2+
os.cpus()

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

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
const m = new require.main.constructor();
2+
m.require('./invalid-module')
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
const m = new require.main.constructor();
2+
require.extensions['.js'](m, './invalid-module')
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"resources": {
3+
"./createRequire-bypass.js": {
4+
"integrity": true
5+
},
6+
"/os-access-module.js": {
7+
"integrity": true,
8+
"dependencies": {
9+
"os": true
10+
}
11+
}
12+
}
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.constructor._load('node:child_process');

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

+55
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,58 @@ const fixtures = require('../common/fixtures.js');
8181
assert.match(stderr, /ERR_MANIFEST_DEPENDENCY_MISSING/);
8282
assert.match(stderr, /does not list os as a dependency specifier for conditions: require, node, node-addons/);
8383
}
84+
85+
{
86+
const policyFilepath = fixtures.path('policy-manifest', 'onerror-exit.json');
87+
const mainModuleBypass = fixtures.path('policy-manifest', 'module-constructor-bypass.js');
88+
const result = spawnSync(process.execPath, [
89+
'--experimental-policy',
90+
policyFilepath,
91+
mainModuleBypass,
92+
]);
93+
assert.notStrictEqual(result.status, 0);
94+
const stderr = result.stderr.toString();
95+
assert.match(stderr, /TypeError/);
96+
}
97+
98+
{
99+
const policyFilepath = fixtures.path('policy-manifest', 'manifest-impersonate.json');
100+
const createRequireBypass = fixtures.path('policy-manifest', 'createRequire-bypass.js');
101+
const result = spawnSync(process.execPath, [
102+
'--experimental-policy',
103+
policyFilepath,
104+
createRequireBypass,
105+
]);
106+
107+
assert.notStrictEqual(result.status, 0);
108+
const stderr = result.stderr.toString();
109+
assert.match(stderr, /TypeError/);
110+
}
111+
112+
{
113+
const policyFilepath = fixtures.path('policy-manifest', 'onerror-exit.json');
114+
const mainModuleBypass = fixtures.path('policy-manifest', 'main-constructor-bypass.js');
115+
const result = spawnSync(process.execPath, [
116+
'--experimental-policy',
117+
policyFilepath,
118+
mainModuleBypass,
119+
]);
120+
121+
assert.notStrictEqual(result.status, 0);
122+
const stderr = result.stderr.toString();
123+
assert.match(stderr, /TypeError/);
124+
}
125+
126+
{
127+
const policyFilepath = fixtures.path('policy-manifest', 'onerror-exit.json');
128+
const mainModuleBypass = fixtures.path('policy-manifest', 'main-constructor-extensions-bypass.js');
129+
const result = spawnSync(process.execPath, [
130+
'--experimental-policy',
131+
policyFilepath,
132+
mainModuleBypass,
133+
]);
134+
135+
assert.notStrictEqual(result.status, 0);
136+
const stderr = result.stderr.toString();
137+
assert.match(stderr, /TypeError/);
138+
}

0 commit comments

Comments
 (0)
Please sign in to comment.