Skip to content

Commit

Permalink
util: restrict custom inspect function + vm.Context interaction
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
addaleax authored and MylesBorins committed Jul 16, 2020
1 parent 3b26809 commit e0206ba
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 3 deletions.
7 changes: 7 additions & 0 deletions doc/api/util.md
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,13 @@ stream.write('With ES6');
added: v0.3.0
changes:
- version: v14.0.0
pr-url: https://github.com/nodejs/node/pull/33690
description: If `object` is from a different `vm.Context` now, a custom
inspection function on it will not receive context-specific
arguments anymore.
- version:
- v13.13.0
- v12.17.0
pr-url: https://github.com/nodejs/node/pull/32392
description: The `maxStringLength` option is supported now.
- version:
Expand Down
39 changes: 36 additions & 3 deletions lib/internal/util/inspect.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const {
DatePrototypeToString,
ErrorPrototypeToString,
Float32Array,
FunctionPrototypeCall,
FunctionPrototypeToString,
Int16Array,
JSONStringify,
Expand All @@ -26,6 +27,7 @@ const {
Number,
NumberIsNaN,
NumberPrototypeValueOf,
Object,
ObjectAssign,
ObjectCreate,
ObjectDefineProperty,
Expand All @@ -38,6 +40,7 @@ const {
ObjectPrototypeHasOwnProperty,
ObjectPrototypePropertyIsEnumerable,
ObjectSeal,
ObjectSetPrototypeOf,
RegExp,
RegExpPrototypeToString,
Set,
Expand Down Expand Up @@ -213,8 +216,8 @@ const ansi = new RegExp(ansiPattern, 'g');

let getStringWidth;

function getUserOptions(ctx) {
return {
function getUserOptions(ctx, isCrossContext) {
const ret = {
stylize: ctx.stylize,
showHidden: ctx.showHidden,
depth: ctx.depth,
Expand All @@ -229,6 +232,33 @@ function getUserOptions(ctx) {
getters: ctx.getters,
...ctx.userOptions
};

// Typically, the target value will be an instance of `Object`. If that is
// *not* the case, the object may come from another vm.Context, and we want
// to avoid passing it objects from this Context in that case, so we remove
// the prototype from the returned object itself + the `stylize()` function,
// and remove all other non-primitives, including non-primitive user options.
if (isCrossContext) {
ObjectSetPrototypeOf(ret, null);
for (const key of ObjectKeys(ret)) {
if ((typeof ret[key] === 'object' || typeof ret[key] === 'function') &&
ret[key] !== null) {
delete ret[key];
}
}
ret.stylize = ObjectSetPrototypeOf((value, flavour) => {
let stylized;
try {
stylized = `${ctx.stylize(value, flavour)}`;
} catch {}

if (typeof stylized !== 'string') return value;
// `stylized` is a string as it should be, which is safe to pass along.
return stylized;
}, null);
}

return ret;
}

/**
Expand Down Expand Up @@ -728,7 +758,10 @@ function formatValue(ctx, value, recurseTimes, typedArray) {
// This makes sure the recurseTimes are reported as before while using
// a counter internally.
const depth = ctx.depth === null ? null : ctx.depth - recurseTimes;
const ret = maybeCustom.call(context, depth, getUserOptions(ctx));
const isCrossContext =
proxy !== undefined || !(context instanceof Object);
const ret = FunctionPrototypeCall(
maybeCustom, context, depth, getUserOptions(ctx, isCrossContext));
// If the custom inspection method returned `this`, don't go into
// infinite recursion.
if (ret !== context) {
Expand Down
83 changes: 83 additions & 0 deletions test/parallel/test-util-inspect.js
Original file line number Diff line number Diff line change
Expand Up @@ -2906,3 +2906,86 @@ assert.strictEqual(
"'aaaa'... 999996 more characters"
);
}

{
// Verify that util.inspect() invokes custom inspect functions on objects
// from other vm.Contexts but does not pass data from its own Context to that
// function.
const target = vm.runInNewContext(`
({
[Symbol.for('nodejs.util.inspect.custom')](depth, ctx) {
this.depth = depth;
this.ctx = ctx;
try {
this.stylized = ctx.stylize('🐈');
} catch (e) {
this.stylizeException = e;
}
return this.stylized;
}
})
`, Object.create(null));
assert.strictEqual(target.ctx, undefined);

{
// Subtest 1: Just try to inspect the object with default options.
assert.strictEqual(util.inspect(target), '🐈');
assert.strictEqual(typeof target.ctx, 'object');
const objectGraph = fullObjectGraph(target);
assert(!objectGraph.has(Object));
assert(!objectGraph.has(Function));
}

{
// Subtest 2: Use a stylize function that returns a non-primitive.
const output = util.inspect(target, {
stylize: common.mustCall((str) => {
return {};
})
});
assert.strictEqual(output, '[object Object]');
assert.strictEqual(typeof target.ctx, 'object');
const objectGraph = fullObjectGraph(target);
assert(!objectGraph.has(Object));
assert(!objectGraph.has(Function));
}

{
// Subtest 3: Use a stylize function that throws an exception.
const output = util.inspect(target, {
stylize: common.mustCall((str) => {
throw new Error('oops');
})
});
assert.strictEqual(output, '🐈');
assert.strictEqual(typeof target.ctx, 'object');
const objectGraph = fullObjectGraph(target);
assert(!objectGraph.has(Object));
assert(!objectGraph.has(Function));
}

function fullObjectGraph(value) {
const graph = new Set([value]);

for (const entry of graph) {
if ((typeof entry !== 'object' && typeof entry !== 'function') ||
entry === null) {
continue;
}

graph.add(Object.getPrototypeOf(entry));
const descriptors = Object.values(
Object.getOwnPropertyDescriptors(entry));
for (const descriptor of descriptors) {
graph.add(descriptor.value);
graph.add(descriptor.set);
graph.add(descriptor.get);
}
}

return graph;
}

// Consistency check.
assert(fullObjectGraph(global).has(Function.prototype));
}

0 comments on commit e0206ba

Please sign in to comment.