Skip to content

Commit b665171

Browse files
aduh95juanarbol
authored andcommitted
module: protect against prototype mutation
Ensures that mutating the `Object` prototype does not influence the parsing of `package.json` files. PR-URL: #44007 Reviewed-By: Geoffrey Booth <[email protected]>
1 parent 1022ece commit b665171

File tree

8 files changed

+96
-15
lines changed

8 files changed

+96
-15
lines changed

lib/internal/modules/cjs/helpers.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const path = require('path');
2424
const { pathToFileURL, fileURLToPath, URL } = require('internal/url');
2525

2626
const { getOptionValue } = require('internal/options');
27+
const { setOwnProperty } = require('internal/util');
2728
const userConditions = getOptionValue('--conditions');
2829

2930
let debug = require('internal/util/debuglog').debuglog('module', (fn) => {
@@ -117,7 +118,7 @@ function makeRequireFunction(mod, redirects) {
117118

118119
resolve.paths = paths;
119120

120-
require.main = process.mainModule;
121+
setOwnProperty(require, 'main', process.mainModule);
121122

122123
// Enable support to add extra extension types.
123124
require.extensions = Module._extensions;

lib/internal/modules/cjs/loader.js

+10-11
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ const {
7979
maybeCacheSourceMap,
8080
} = require('internal/source_map/source_map_cache');
8181
const { pathToFileURL, fileURLToPath, isURLInstance } = require('internal/url');
82-
const { deprecate, kEmptyObject } = require('internal/util');
82+
const { deprecate, kEmptyObject, filterOwnProperties, setOwnProperty } = require('internal/util');
8383
const vm = require('vm');
8484
const assert = require('internal/assert');
8585
const fs = require('fs');
@@ -172,7 +172,7 @@ const moduleParentCache = new SafeWeakMap();
172172
function Module(id = '', parent) {
173173
this.id = id;
174174
this.path = path.dirname(id);
175-
this.exports = {};
175+
setOwnProperty(this, 'exports', {});
176176
moduleParentCache.set(this, parent);
177177
updateChildren(parent, this, false);
178178
this.filename = null;
@@ -312,14 +312,13 @@ function readPackage(requestPath) {
312312
}
313313

314314
try {
315-
const parsed = JSONParse(json);
316-
const filtered = {
317-
name: parsed.name,
318-
main: parsed.main,
319-
exports: parsed.exports,
320-
imports: parsed.imports,
321-
type: parsed.type
322-
};
315+
const filtered = filterOwnProperties(JSONParse(json), [
316+
'name',
317+
'main',
318+
'exports',
319+
'imports',
320+
'type',
321+
]);
323322
packageJsonCache.set(jsonPath, filtered);
324323
return filtered;
325324
} catch (e) {
@@ -1191,7 +1190,7 @@ Module._extensions['.json'] = function(module, filename) {
11911190
}
11921191

11931192
try {
1194-
module.exports = JSONParse(stripBOM(content));
1193+
setOwnProperty(module, 'exports', JSONParse(stripBOM(content)));
11951194
} catch (err) {
11961195
err.message = filename + ': ' + err.message;
11971196
throw err;

lib/internal/modules/esm/package_config.js

+4-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
const {
44
JSONParse,
5+
ObjectPrototypeHasOwnProperty,
56
SafeMap,
67
StringPrototypeEndsWith,
78
} = primordials;
@@ -11,6 +12,7 @@ const {
1112
} = require('internal/errors').codes;
1213

1314
const packageJsonReader = require('internal/modules/package_json_reader');
15+
const { filterOwnProperties } = require('internal/util');
1416

1517

1618
/**
@@ -66,8 +68,8 @@ function getPackageConfig(path, specifier, base) {
6668
);
6769
}
6870

69-
let { imports, main, name, type } = packageJSON;
70-
const { exports } = packageJSON;
71+
let { imports, main, name, type } = filterOwnProperties(packageJSON, ['imports', 'main', 'name', 'type']);
72+
const exports = ObjectPrototypeHasOwnProperty(packageJSON, 'exports') ? packageJSON.exports : undefined;
7173
if (typeof imports !== 'object' || imports === null) {
7274
imports = undefined;
7375
}

lib/internal/util.js

+32
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const {
1414
ObjectGetOwnPropertyDescriptors,
1515
ObjectGetPrototypeOf,
1616
ObjectFreeze,
17+
ObjectPrototypeHasOwnProperty,
1718
ObjectSetPrototypeOf,
1819
Promise,
1920
ReflectApply,
@@ -518,6 +519,35 @@ ObjectFreeze(kEnumerableProperty);
518519

519520
const kEmptyObject = ObjectFreeze(ObjectCreate(null));
520521

522+
function filterOwnProperties(source, keys) {
523+
const filtered = ObjectCreate(null);
524+
for (let i = 0; i < keys.length; i++) {
525+
const key = keys[i];
526+
if (ObjectPrototypeHasOwnProperty(source, key)) {
527+
filtered[key] = source[key];
528+
}
529+
}
530+
531+
return filtered;
532+
}
533+
534+
/**
535+
* Mimics `obj[key] = value` but ignoring potential prototype inheritance.
536+
* @param {any} obj
537+
* @param {string} key
538+
* @param {any} value
539+
* @returns {any}
540+
*/
541+
function setOwnProperty(obj, key, value) {
542+
return ObjectDefineProperty(obj, key, {
543+
__proto__: null,
544+
configurable: true,
545+
enumerable: true,
546+
value,
547+
writable: true,
548+
});
549+
}
550+
521551
module.exports = {
522552
assertCrypto,
523553
cachedResult,
@@ -530,6 +560,7 @@ module.exports = {
530560
emitExperimentalWarning,
531561
exposeInterface,
532562
filterDuplicateStrings,
563+
filterOwnProperties,
533564
getConstructorOf,
534565
getSystemErrorMap,
535566
getSystemErrorName,
@@ -560,4 +591,5 @@ module.exports = {
560591

561592
kEmptyObject,
562593
kEnumerableProperty,
594+
setOwnProperty,
563595
};
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import explicit from 'explicit-main';
22
import implicit from 'implicit-main';
33
import implicitModule from 'implicit-main-type-module';
4+
import noMain from 'no-main-field';
45

56
function getImplicitCommonjs () {
67
return import('implicit-main-type-commonjs');
78
}
89

9-
export {explicit, implicit, implicitModule, getImplicitCommonjs};
10+
export {explicit, implicit, implicitModule, getImplicitCommonjs, noMain};
1011
export default 'success';

test/fixtures/es-module-specifiers/node_modules/no-main-field/index.js

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/fixtures/es-module-specifiers/node_modules/no-main-field/package.json

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
'use strict';
2+
const common = require('../common');
3+
const fixtures = require('../common/fixtures');
4+
const assert = require('assert');
5+
6+
Object.defineProperty(Object.prototype, 'name', {
7+
__proto__: null,
8+
get: common.mustNotCall('get %Object.prototype%.name'),
9+
set: common.mustNotCall('set %Object.prototype%.name'),
10+
enumerable: false,
11+
});
12+
Object.defineProperty(Object.prototype, 'main', {
13+
__proto__: null,
14+
get: common.mustNotCall('get %Object.prototype%.main'),
15+
set: common.mustNotCall('set %Object.prototype%.main'),
16+
enumerable: false,
17+
});
18+
Object.defineProperty(Object.prototype, 'type', {
19+
__proto__: null,
20+
get: common.mustNotCall('get %Object.prototype%.type'),
21+
set: common.mustNotCall('set %Object.prototype%.type'),
22+
enumerable: false,
23+
});
24+
Object.defineProperty(Object.prototype, 'exports', {
25+
__proto__: null,
26+
get: common.mustNotCall('get %Object.prototype%.exports'),
27+
set: common.mustNotCall('set %Object.prototype%.exports'),
28+
enumerable: false,
29+
});
30+
Object.defineProperty(Object.prototype, 'imports', {
31+
__proto__: null,
32+
get: common.mustNotCall('get %Object.prototype%.imports'),
33+
set: common.mustNotCall('set %Object.prototype%.imports'),
34+
enumerable: false,
35+
});
36+
37+
assert.strictEqual(
38+
require(fixtures.path('es-module-specifiers', 'node_modules', 'no-main-field')),
39+
'no main field'
40+
);
41+
42+
import(fixtures.fileURL('es-module-specifiers', 'index.mjs'))
43+
.then(common.mustCall((module) => assert.strictEqual(module.noMain, 'no main field')));

0 commit comments

Comments
 (0)