Skip to content

Commit 58bae4d

Browse files
committed
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 4faec56 commit 58bae4d

File tree

3 files changed

+124
-3
lines changed

3 files changed

+124
-3
lines changed

doc/api/util.md

+5
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,11 @@ stream.write('With ES6');
397397
<!-- YAML
398398
added: v0.3.0
399399
changes:
400+
- version: REPLACEME
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.
400405
- version:
401406
- v13.13.0
402407
- v12.17.0

lib/internal/util/inspect.js

+36-3
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
/**
@@ -727,7 +757,10 @@ function formatValue(ctx, value, recurseTimes, typedArray) {
727757
// This makes sure the recurseTimes are reported as before while using
728758
// a counter internally.
729759
const depth = ctx.depth === null ? null : ctx.depth - recurseTimes;
730-
const ret = maybeCustom.call(context, depth, getUserOptions(ctx));
760+
const isCrossContext =
761+
proxy !== undefined || !(context instanceof Object);
762+
const ret = FunctionPrototypeCall(
763+
maybeCustom, context, depth, getUserOptions(ctx, isCrossContext));
731764
// If the custom inspection method returned `this`, don't go into
732765
// infinite recursion.
733766
if (ret !== context) {

test/parallel/test-util-inspect.js

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

0 commit comments

Comments
 (0)