Skip to content

Commit 992c5f6

Browse files
christianaurichzmaduh95
authored andcommitted
errors: validate constructor name
determineSpecificType() checks for a usable constructor name with `'name' in value.constructor`. That accepts an empty name, and the `in` operator requires its right-hand side to be an object. Anonymous classes own a `name` that is the empty string, so they produce messages ending in a dangling "Received an instance of ". A truthy primitive `constructor` reaches the `in` operator and throws while the message is being built, so ERR_INVALID_ARG_TYPE is replaced by a TypeError carrying no `code`. The latter is reachable from untrusted input, since `constructor` is an ordinary JSON key. The check changed in #49696, while this function was rewritten as a switch. That pull request updated test/common's invalidArgTypeHelper to match its deliberate change to the `function` branch, but left the helper's `object` branch on the original truthy check, so the two have disagreed since. Read `constructor` and its `name` once and use the name only when it is a non-empty string. Requiring a string also stops meaningless names from being interpolated: 42 currently yields "an instance of 42", and a symbol name throws outright. The current check reads `constructor` three times, which an accessor can observe. Move the helper to the same check so the two cannot drift apart again. Signed-off-by: Christian Aurich <christian.aurichzm@gmail.com> PR-URL: #65607 Refs: #49696 Reviewed-By: Filip Skokan <panva.ip@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent 1c36206 commit 992c5f6

3 files changed

Lines changed: 66 additions & 5 deletions

File tree

lib/internal/errors.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1035,11 +1035,16 @@ function determineSpecificType(value) {
10351035
return `type symbol (${String(value)})`;
10361036
case 'function':
10371037
return `function ${value.name}`;
1038-
case 'object':
1039-
if (value.constructor && 'name' in value.constructor) {
1040-
return `an instance of ${value.constructor.name}`;
1038+
case 'object': {
1039+
// `constructor` may be user-controlled: it need not be an object, and
1040+
// its `name` need not be a non-empty string. Reading either can invoke
1041+
// an accessor, so read each one once.
1042+
const name = value.constructor?.name;
1043+
if (typeof name === 'string' && name !== '') {
1044+
return `an instance of ${name}`;
10411045
}
10421046
return `${lazyInternalUtilInspect().inspect(value, { depth: -1 })}`;
1047+
}
10431048
case 'string':
10441049
value.length > 28 && (value = `${StringPrototypeSlice(value, 0, 25)}...`);
10451050
if (StringPrototypeIndexOf(value, "'") === -1) {

test/common/index.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -855,8 +855,9 @@ function invalidArgTypeHelper(input) {
855855
return ` Received function ${input.name}`;
856856
}
857857
if (typeof input === 'object') {
858-
if (input.constructor?.name) {
859-
return ` Received an instance of ${input.constructor.name}`;
858+
const name = input.constructor?.name;
859+
if (typeof name === 'string' && name !== '') {
860+
return ` Received an instance of ${name}`;
860861
}
861862
return ` Received ${inspect(input, { depth: -1 })}`;
862863
}

test/parallel/test-error-value-type-detection.mjs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,3 +209,58 @@ assert.strictEqual(
209209
determineSpecificType(new WeakSet()),
210210
'an instance of WeakSet',
211211
);
212+
213+
// Anonymous classes have an empty `name`, so the value is inspected instead.
214+
// `inspect(..., { depth: -1 })` prints this one as `{}` because it has no own
215+
// properties; the values below carry a `constructor` property of their own and
216+
// print as `[Object]`.
217+
assert.strictEqual(
218+
determineSpecificType(new (class {})()),
219+
'{}',
220+
);
221+
222+
// `constructor` is an ordinary, user-controlled property that need not be a
223+
// function. Describing such a value must not throw.
224+
assert.strictEqual(
225+
determineSpecificType(JSON.parse('{"constructor": 5}')),
226+
'[Object]',
227+
);
228+
229+
assert.strictEqual(
230+
determineSpecificType({ constructor: { name: '' } }),
231+
'[Object]',
232+
);
233+
234+
// A `constructor.name` that is not a usable string must not be interpolated
235+
// into the message.
236+
assert.strictEqual(
237+
determineSpecificType({ constructor: { name: Symbol('x') } }),
238+
'[Object]',
239+
);
240+
241+
assert.strictEqual(
242+
determineSpecificType({ constructor: { name: 42 } }),
243+
'[Object]',
244+
);
245+
246+
// `constructor` and its `name` are each read once. Both may be accessors, so
247+
// repeated reads are observable, and a `name` validated by one read and
248+
// interpolated from another need not be the same value twice.
249+
let constructorReads = 0;
250+
let nameReads = 0;
251+
const named = {
252+
get constructor() {
253+
constructorReads++;
254+
return { get name() { nameReads++; return 'Foo'; } };
255+
},
256+
};
257+
assert.strictEqual(determineSpecificType(named), 'an instance of Foo');
258+
assert.strictEqual(constructorReads, 1);
259+
assert.strictEqual(nameReads, 1);
260+
261+
// Building the error must not fail when `constructor` is a truthy primitive.
262+
assert.strictEqual(
263+
new errorsModule.codes.ERR_INVALID_ARG_TYPE(
264+
'arg', 'string', JSON.parse('{"constructor": 5}')).code,
265+
'ERR_INVALID_ARG_TYPE',
266+
);

0 commit comments

Comments
 (0)