vm: harden module type checks

Check if the value returned from user linker function is a null-ish
value.

`validateInternalField` should be preferred when checking `this`
argument to guard against null-ish `this`.

Co-authored-by: Mike Ralphson <mike.ralphson@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/52162
Reviewed-By: Vinícius Lourenço Claro Cardoso <contact@viniciusl.com.br>
Reviewed-By: Yagiz Nizipli <yagiz.nizipli@sentry.io>
This commit is contained in:
Chengzhong Wu 2024-03-22 17:41:02 +08:00 committed by GitHub
parent 0b676736a0
commit d1d5da22e4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 57 additions and 54 deletions

View File

@ -9,6 +9,7 @@ const {
ObjectDefineProperty, ObjectDefineProperty,
ObjectFreeze, ObjectFreeze,
ObjectGetPrototypeOf, ObjectGetPrototypeOf,
ObjectPrototypeHasOwnProperty,
ObjectSetPrototypeOf, ObjectSetPrototypeOf,
ReflectApply, ReflectApply,
SafePromiseAllReturnVoid, SafePromiseAllReturnVoid,
@ -44,6 +45,7 @@ const {
validateObject, validateObject,
validateUint32, validateUint32,
validateString, validateString,
validateInternalField,
} = require('internal/validators'); } = require('internal/validators');
const binding = internalBinding('module_wrap'); const binding = internalBinding('module_wrap');
@ -76,6 +78,13 @@ const kLink = Symbol('kLink');
const { isContext } = require('internal/vm'); const { isContext } = require('internal/vm');
function isModule(object) {
if (typeof object !== 'object' || object === null || !ObjectPrototypeHasOwnProperty(object, kWrap)) {
return false;
}
return true;
}
class Module { class Module {
constructor(options) { constructor(options) {
emitExperimentalWarning('VM Modules'); emitExperimentalWarning('VM Modules');
@ -149,23 +158,17 @@ class Module {
} }
get identifier() { get identifier() {
if (this[kWrap] === undefined) { validateInternalField(this, kWrap, 'Module');
throw new ERR_VM_MODULE_NOT_MODULE();
}
return this[kWrap].url; return this[kWrap].url;
} }
get context() { get context() {
if (this[kWrap] === undefined) { validateInternalField(this, kWrap, 'Module');
throw new ERR_VM_MODULE_NOT_MODULE();
}
return this[kContext]; return this[kContext];
} }
get namespace() { get namespace() {
if (this[kWrap] === undefined) { validateInternalField(this, kWrap, 'Module');
throw new ERR_VM_MODULE_NOT_MODULE();
}
if (this[kWrap].getStatus() < kInstantiated) { if (this[kWrap].getStatus() < kInstantiated) {
throw new ERR_VM_MODULE_STATUS('must not be unlinked or linking'); throw new ERR_VM_MODULE_STATUS('must not be unlinked or linking');
} }
@ -173,16 +176,12 @@ class Module {
} }
get status() { get status() {
if (this[kWrap] === undefined) { validateInternalField(this, kWrap, 'Module');
throw new ERR_VM_MODULE_NOT_MODULE();
}
return STATUS_MAP[this[kWrap].getStatus()]; return STATUS_MAP[this[kWrap].getStatus()];
} }
get error() { get error() {
if (this[kWrap] === undefined) { validateInternalField(this, kWrap, 'Module');
throw new ERR_VM_MODULE_NOT_MODULE();
}
if (this[kWrap].getStatus() !== kErrored) { if (this[kWrap].getStatus() !== kErrored) {
throw new ERR_VM_MODULE_STATUS('must be errored'); throw new ERR_VM_MODULE_STATUS('must be errored');
} }
@ -190,9 +189,7 @@ class Module {
} }
async link(linker) { async link(linker) {
if (this[kWrap] === undefined) { validateInternalField(this, kWrap, 'Module');
throw new ERR_VM_MODULE_NOT_MODULE();
}
validateFunction(linker, 'linker'); validateFunction(linker, 'linker');
if (this.status === 'linked') { if (this.status === 'linked') {
throw new ERR_VM_MODULE_ALREADY_LINKED(); throw new ERR_VM_MODULE_ALREADY_LINKED();
@ -205,10 +202,7 @@ class Module {
} }
async evaluate(options = kEmptyObject) { async evaluate(options = kEmptyObject) {
if (this[kWrap] === undefined) { validateInternalField(this, kWrap, 'Module');
throw new ERR_VM_MODULE_NOT_MODULE();
}
validateObject(options, 'options'); validateObject(options, 'options');
let timeout = options.timeout; let timeout = options.timeout;
@ -231,9 +225,7 @@ class Module {
} }
[customInspectSymbol](depth, options) { [customInspectSymbol](depth, options) {
if (this[kWrap] === undefined) { validateInternalField(this, kWrap, 'Module');
throw new ERR_VM_MODULE_NOT_MODULE();
}
if (typeof depth === 'number' && depth < 0) if (typeof depth === 'number' && depth < 0)
return this; return this;
@ -308,7 +300,7 @@ class SourceTextModule extends Module {
const promises = this[kWrap].link(async (identifier, attributes) => { const promises = this[kWrap].link(async (identifier, attributes) => {
const module = await linker(identifier, this, { attributes, assert: attributes }); const module = await linker(identifier, this, { attributes, assert: attributes });
if (module[kWrap] === undefined) { if (!isModule(module)) {
throw new ERR_VM_MODULE_NOT_MODULE(); throw new ERR_VM_MODULE_NOT_MODULE();
} }
if (module.context !== this.context) { if (module.context !== this.context) {
@ -339,17 +331,13 @@ class SourceTextModule extends Module {
} }
get dependencySpecifiers() { get dependencySpecifiers() {
if (this[kWrap] === undefined) { validateInternalField(this, kDependencySpecifiers, 'SourceTextModule');
throw new ERR_VM_MODULE_NOT_MODULE();
}
this[kDependencySpecifiers] ??= ObjectFreeze(this[kWrap].getStaticDependencySpecifiers()); this[kDependencySpecifiers] ??= ObjectFreeze(this[kWrap].getStaticDependencySpecifiers());
return this[kDependencySpecifiers]; return this[kDependencySpecifiers];
} }
get status() { get status() {
if (this[kWrap] === undefined) { validateInternalField(this, kDependencySpecifiers, 'SourceTextModule');
throw new ERR_VM_MODULE_NOT_MODULE();
}
if (this.#error !== kNoError) { if (this.#error !== kNoError) {
return 'errored'; return 'errored';
} }
@ -360,9 +348,7 @@ class SourceTextModule extends Module {
} }
get error() { get error() {
if (this[kWrap] === undefined) { validateInternalField(this, kDependencySpecifiers, 'SourceTextModule');
throw new ERR_VM_MODULE_NOT_MODULE();
}
if (this.#error !== kNoError) { if (this.#error !== kNoError) {
return this.#error; return this.#error;
} }
@ -415,9 +401,7 @@ class SyntheticModule extends Module {
} }
setExport(name, value) { setExport(name, value) {
if (this[kWrap] === undefined) { validateInternalField(this, kWrap, 'SyntheticModule');
throw new ERR_VM_MODULE_NOT_MODULE();
}
validateString(name, 'name'); validateString(name, 'name');
if (this[kWrap].getStatus() < kInstantiated) { if (this[kWrap].getStatus() < kInstantiated) {
throw new ERR_VM_MODULE_STATUS('must be linked'); throw new ERR_VM_MODULE_STATUS('must be linked');
@ -432,7 +416,7 @@ function importModuleDynamicallyWrap(importModuleDynamically) {
if (isModuleNamespaceObject(m)) { if (isModuleNamespaceObject(m)) {
return m; return m;
} }
if (!m || m[kWrap] === undefined) { if (!isModule(m)) {
throw new ERR_VM_MODULE_NOT_MODULE(); throw new ERR_VM_MODULE_NOT_MODULE();
} }
if (m.status === 'errored') { if (m.status === 'errored') {

View File

@ -84,13 +84,15 @@ const util = require('util');
assert.strictEqual(util.inspect(m, { depth: -1 }), '[SourceTextModule]'); assert.strictEqual(util.inspect(m, { depth: -1 }), '[SourceTextModule]');
assert.throws( for (const value of [null, { __proto__: null }, SourceTextModule.prototype]) {
() => m[util.inspect.custom].call({ __proto__: null }), assert.throws(
{ () => m[util.inspect.custom].call(value),
code: 'ERR_VM_MODULE_NOT_MODULE', {
message: 'Provided module is not an instance of Module' code: 'ERR_INVALID_ARG_TYPE',
}, message: /The "this" argument must be an instance of Module/,
); },
);
}
} }
{ {

View File

@ -216,8 +216,8 @@ async function checkInvalidOptionForEvaluate() {
await assert.rejects(async () => { await assert.rejects(async () => {
await Module.prototype[method](); await Module.prototype[method]();
}, { }, {
code: 'ERR_VM_MODULE_NOT_MODULE', code: 'ERR_INVALID_ARG_TYPE',
message: /Provided module is not an instance of Module/ message: /The "this" argument must be an instance of Module/
}); });
}); });
} }
@ -241,8 +241,8 @@ function checkInvalidCachedData() {
function checkGettersErrors() { function checkGettersErrors() {
const expectedError = { const expectedError = {
code: 'ERR_VM_MODULE_NOT_MODULE', code: 'ERR_INVALID_ARG_TYPE',
message: /Provided module is not an instance of Module/ message: /The "this" argument must be an instance of (?:Module|SourceTextModule)/,
}; };
const getters = ['identifier', 'context', 'namespace', 'status', 'error']; const getters = ['identifier', 'context', 'namespace', 'status', 'error'];
getters.forEach((getter) => { getters.forEach((getter) => {

View File

@ -28,6 +28,22 @@ async function simple() {
delete globalThis.fiveResult; delete globalThis.fiveResult;
} }
async function invalidLinkValue() {
const invalidValues = [
undefined,
null,
{},
SourceTextModule.prototype,
];
for (const value of invalidValues) {
const module = new SourceTextModule('import "foo"');
await assert.rejects(module.link(() => value), {
code: 'ERR_VM_MODULE_NOT_MODULE',
});
}
}
async function depth() { async function depth() {
const foo = new SourceTextModule('export default 5'); const foo = new SourceTextModule('export default 5');
await foo.link(common.mustNotCall()); await foo.link(common.mustNotCall());
@ -143,6 +159,7 @@ const finished = common.mustCall();
(async function main() { (async function main() {
await simple(); await simple();
await invalidLinkValue();
await depth(); await depth();
await circular(); await circular();
await circular2(); await circular2();

View File

@ -66,12 +66,12 @@ const assert = require('assert');
}); });
} }
{ for (const value of [null, {}, SyntheticModule.prototype]) {
assert.throws(() => { assert.throws(() => {
SyntheticModule.prototype.setExport.call({}, 'foo'); SyntheticModule.prototype.setExport.call(value, 'foo');
}, { }, {
code: 'ERR_VM_MODULE_NOT_MODULE', code: 'ERR_INVALID_ARG_TYPE',
message: /Provided module is not an instance of Module/ message: /The "this" argument must be an instance of SyntheticModule/
}); });
} }