Skip to content

Commit 8d0c21f

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 4a2accb commit 8d0c21f

File tree

3 files changed

+124
-3
lines changed

3 files changed

+124
-3
lines changed

doc/api/util.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,11 @@ stream.write('With ES6');
414414
<!-- YAML
415415
added: v0.3.0
416416
changes:
417+
- version: REPLACEME
418+
pr-url: https://github.com/nodejs/node/pull/33690
419+
description: If `object` is from a different `vm.Context` now, a custom
420+
inspection function on it will not receive context-specific
421+
arguments anymore.
417422
- version: v12.17.0
418423
pr-url: https://github.com/nodejs/node/pull/32392
419424
description: The `maxStringLength` option is supported now.

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,
@@ -212,8 +215,8 @@ const ansi = new RegExp(ansiPattern, 'g');
212215

213216
let getStringWidth;
214217

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

233263
/**
@@ -723,7 +753,10 @@ function formatValue(ctx, value, recurseTimes, typedArray) {
723753
// This makes sure the recurseTimes are reported as before while using
724754
// a counter internally.
725755
const depth = ctx.depth === null ? null : ctx.depth - recurseTimes;
726-
const ret = maybeCustom.call(context, depth, getUserOptions(ctx));
756+
const isCrossContext =
757+
proxy !== undefined || !(context instanceof Object);
758+
const ret = FunctionPrototypeCall(
759+
maybeCustom, context, depth, getUserOptions(ctx, isCrossContext));
727760
// If the custom inspection method returned `this`, don't go into
728761
// infinite recursion.
729762
if (ret !== context) {

test/parallel/test-util-inspect.js

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2874,3 +2874,86 @@ assert.strictEqual(
28742874
"'aaaa'... 999996 more characters"
28752875
);
28762876
}
2877+
2878+
{
2879+
// Verify that util.inspect() invokes custom inspect functions on objects
2880+
// from other vm.Contexts but does not pass data from its own Context to that
2881+
// function.
2882+
const target = vm.runInNewContext(`
2883+
({
2884+
[Symbol.for('nodejs.util.inspect.custom')](depth, ctx) {
2885+
this.depth = depth;
2886+
this.ctx = ctx;
2887+
try {
2888+
this.stylized = ctx.stylize('🐈');
2889+
} catch (e) {
2890+
this.stylizeException = e;
2891+
}
2892+
return this.stylized;
2893+
}
2894+
})
2895+
`, Object.create(null));
2896+
assert.strictEqual(target.ctx, undefined);
2897+
2898+
{
2899+
// Subtest 1: Just try to inspect the object with default options.
2900+
assert.strictEqual(util.inspect(target), '🐈');
2901+
assert.strictEqual(typeof target.ctx, 'object');
2902+
const objectGraph = fullObjectGraph(target);
2903+
assert(!objectGraph.has(Object));
2904+
assert(!objectGraph.has(Function));
2905+
}
2906+
2907+
{
2908+
// Subtest 2: Use a stylize function that returns a non-primitive.
2909+
const output = util.inspect(target, {
2910+
stylize: common.mustCall((str) => {
2911+
return {};
2912+
})
2913+
});
2914+
assert.strictEqual(output, '[object Object]');
2915+
assert.strictEqual(typeof target.ctx, 'object');
2916+
const objectGraph = fullObjectGraph(target);
2917+
assert(!objectGraph.has(Object));
2918+
assert(!objectGraph.has(Function));
2919+
}
2920+
2921+
{
2922+
// Subtest 3: Use a stylize function that throws an exception.
2923+
const output = util.inspect(target, {
2924+
stylize: common.mustCall((str) => {
2925+
throw new Error('oops');
2926+
})
2927+
});
2928+
assert.strictEqual(output, '🐈');
2929+
assert.strictEqual(typeof target.ctx, 'object');
2930+
const objectGraph = fullObjectGraph(target);
2931+
assert(!objectGraph.has(Object));
2932+
assert(!objectGraph.has(Function));
2933+
}
2934+
2935+
function fullObjectGraph(value) {
2936+
const graph = new Set([value]);
2937+
2938+
for (const entry of graph) {
2939+
if ((typeof entry !== 'object' && typeof entry !== 'function') ||
2940+
entry === null) {
2941+
continue;
2942+
}
2943+
2944+
graph.add(Object.getPrototypeOf(entry));
2945+
const descriptors = Object.values(
2946+
Object.getOwnPropertyDescriptors(entry));
2947+
for (const descriptor of descriptors) {
2948+
graph.add(descriptor.value);
2949+
graph.add(descriptor.set);
2950+
graph.add(descriptor.get);
2951+
}
2952+
}
2953+
2954+
return graph;
2955+
}
2956+
2957+
// Consistency check.
2958+
assert(fullObjectGraph(global).has(Function.prototype));
2959+
}

0 commit comments

Comments
 (0)