Skip to content

Commit e0206ba

Browse files
addaleaxMylesBorins
authored andcommitted
util: restrict custom inspect function + vm.Context interaction
When `util.inspect()` is called on an object with a custom inspect function, and that object is from a different `vm.Context`, that function will not receive any arguments that access context-specific data anymore. PR-URL: #33690 Reviewed-By: James M Snell <[email protected]> Reviewed-By: Colin Ihrig <[email protected]>
1 parent 3b26809 commit e0206ba

File tree

3 files changed

+126
-3
lines changed

3 files changed

+126
-3
lines changed

doc/api/util.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,13 @@ stream.write('With ES6');
398398
added: v0.3.0
399399
changes:
400400
- version: v14.0.0
401+
pr-url: https://github.com/nodejs/node/pull/33690
402+
description: If `object` is from a different `vm.Context` now, a custom
403+
inspection function on it will not receive context-specific
404+
arguments anymore.
405+
- version:
406+
- v13.13.0
407+
- v12.17.0
401408
pr-url: https://github.com/nodejs/node/pull/32392
402409
description: The `maxStringLength` option is supported now.
403410
- version:

lib/internal/util/inspect.js

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const {
1212
DatePrototypeToString,
1313
ErrorPrototypeToString,
1414
Float32Array,
15+
FunctionPrototypeCall,
1516
FunctionPrototypeToString,
1617
Int16Array,
1718
JSONStringify,
@@ -26,6 +27,7 @@ const {
2627
Number,
2728
NumberIsNaN,
2829
NumberPrototypeValueOf,
30+
Object,
2931
ObjectAssign,
3032
ObjectCreate,
3133
ObjectDefineProperty,
@@ -38,6 +40,7 @@ const {
3840
ObjectPrototypeHasOwnProperty,
3941
ObjectPrototypePropertyIsEnumerable,
4042
ObjectSeal,
43+
ObjectSetPrototypeOf,
4144
RegExp,
4245
RegExpPrototypeToString,
4346
Set,
@@ -213,8 +216,8 @@ const ansi = new RegExp(ansiPattern, 'g');
213216

214217
let getStringWidth;
215218

216-
function getUserOptions(ctx) {
217-
return {
219+
function getUserOptions(ctx, isCrossContext) {
220+
const ret = {
218221
stylize: ctx.stylize,
219222
showHidden: ctx.showHidden,
220223
depth: ctx.depth,
@@ -229,6 +232,33 @@ function getUserOptions(ctx) {
229232
getters: ctx.getters,
230233
...ctx.userOptions
231234
};
235+
236+
// Typically, the target value will be an instance of `Object`. If that is
237+
// *not* the case, the object may come from another vm.Context, and we want
238+
// to avoid passing it objects from this Context in that case, so we remove
239+
// the prototype from the returned object itself + the `stylize()` function,
240+
// and remove all other non-primitives, including non-primitive user options.
241+
if (isCrossContext) {
242+
ObjectSetPrototypeOf(ret, null);
243+
for (const key of ObjectKeys(ret)) {
244+
if ((typeof ret[key] === 'object' || typeof ret[key] === 'function') &&
245+
ret[key] !== null) {
246+
delete ret[key];
247+
}
248+
}
249+
ret.stylize = ObjectSetPrototypeOf((value, flavour) => {
250+
let stylized;
251+
try {
252+
stylized = `${ctx.stylize(value, flavour)}`;
253+
} catch {}
254+
255+
if (typeof stylized !== 'string') return value;
256+
// `stylized` is a string as it should be, which is safe to pass along.
257+
return stylized;
258+
}, null);
259+
}
260+
261+
return ret;
232262
}
233263

234264
/**
@@ -728,7 +758,10 @@ function formatValue(ctx, value, recurseTimes, typedArray) {
728758
// This makes sure the recurseTimes are reported as before while using
729759
// a counter internally.
730760
const depth = ctx.depth === null ? null : ctx.depth - recurseTimes;
731-
const ret = maybeCustom.call(context, depth, getUserOptions(ctx));
761+
const isCrossContext =
762+
proxy !== undefined || !(context instanceof Object);
763+
const ret = FunctionPrototypeCall(
764+
maybeCustom, context, depth, getUserOptions(ctx, isCrossContext));
732765
// If the custom inspection method returned `this`, don't go into
733766
// infinite recursion.
734767
if (ret !== context) {

test/parallel/test-util-inspect.js

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2906,3 +2906,86 @@ assert.strictEqual(
29062906
"'aaaa'... 999996 more characters"
29072907
);
29082908
}
2909+
2910+
{
2911+
// Verify that util.inspect() invokes custom inspect functions on objects
2912+
// from other vm.Contexts but does not pass data from its own Context to that
2913+
// function.
2914+
const target = vm.runInNewContext(`
2915+
({
2916+
[Symbol.for('nodejs.util.inspect.custom')](depth, ctx) {
2917+
this.depth = depth;
2918+
this.ctx = ctx;
2919+
try {
2920+
this.stylized = ctx.stylize('🐈');
2921+
} catch (e) {
2922+
this.stylizeException = e;
2923+
}
2924+
return this.stylized;
2925+
}
2926+
})
2927+
`, Object.create(null));
2928+
assert.strictEqual(target.ctx, undefined);
2929+
2930+
{
2931+
// Subtest 1: Just try to inspect the object with default options.
2932+
assert.strictEqual(util.inspect(target), '🐈');
2933+
assert.strictEqual(typeof target.ctx, 'object');
2934+
const objectGraph = fullObjectGraph(target);
2935+
assert(!objectGraph.has(Object));
2936+
assert(!objectGraph.has(Function));
2937+
}
2938+
2939+
{
2940+
// Subtest 2: Use a stylize function that returns a non-primitive.
2941+
const output = util.inspect(target, {
2942+
stylize: common.mustCall((str) => {
2943+
return {};
2944+
})
2945+
});
2946+
assert.strictEqual(output, '[object Object]');
2947+
assert.strictEqual(typeof target.ctx, 'object');
2948+
const objectGraph = fullObjectGraph(target);
2949+
assert(!objectGraph.has(Object));
2950+
assert(!objectGraph.has(Function));
2951+
}
2952+
2953+
{
2954+
// Subtest 3: Use a stylize function that throws an exception.
2955+
const output = util.inspect(target, {
2956+
stylize: common.mustCall((str) => {
2957+
throw new Error('oops');
2958+
})
2959+
});
2960+
assert.strictEqual(output, '🐈');
2961+
assert.strictEqual(typeof target.ctx, 'object');
2962+
const objectGraph = fullObjectGraph(target);
2963+
assert(!objectGraph.has(Object));
2964+
assert(!objectGraph.has(Function));
2965+
}
2966+
2967+
function fullObjectGraph(value) {
2968+
const graph = new Set([value]);
2969+
2970+
for (const entry of graph) {
2971+
if ((typeof entry !== 'object' && typeof entry !== 'function') ||
2972+
entry === null) {
2973+
continue;
2974+
}
2975+
2976+
graph.add(Object.getPrototypeOf(entry));
2977+
const descriptors = Object.values(
2978+
Object.getOwnPropertyDescriptors(entry));
2979+
for (const descriptor of descriptors) {
2980+
graph.add(descriptor.value);
2981+
graph.add(descriptor.set);
2982+
graph.add(descriptor.get);
2983+
}
2984+
}
2985+
2986+
return graph;
2987+
}
2988+
2989+
// Consistency check.
2990+
assert(fullObjectGraph(global).has(Function.prototype));
2991+
}

0 commit comments

Comments
 (0)