Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

util: restrict custom inspect function + vm.Context interaction #33690

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions doc/api/util.md
Expand Up @@ -398,6 +398,11 @@ stream.write('With ES6');
<!-- YAML
added: v0.3.0
changes:
- version: REPLACEME
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
Expand Down
39 changes: 36 additions & 3 deletions lib/internal/util/inspect.js
Expand Up @@ -12,6 +12,7 @@ const {
DatePrototypeToString,
ErrorPrototypeToString,
Float32Array,
FunctionPrototypeCall,
FunctionPrototypeToString,
JSONStringify,
Map,
Expand All @@ -25,6 +26,7 @@ const {
Number,
NumberIsNaN,
NumberPrototypeValueOf,
Object,
ObjectAssign,
ObjectCreate,
ObjectDefineProperty,
Expand All @@ -37,6 +39,7 @@ const {
ObjectPrototypeHasOwnProperty,
ObjectPrototypePropertyIsEnumerable,
ObjectSeal,
ObjectSetPrototypeOf,
RegExp,
RegExpPrototypeToString,
Set,
Expand Down Expand Up @@ -212,8 +215,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 @@ -228,6 +231,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];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about cloning objects and to set the prototype to the destination's Object prototype? The returned object and stylize function could also be set to the destination's Object and Function prototype.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’ve thought about that, but it’s not trivial to determine what the Object prototype is in that case.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something like that should work well enough:

let proto = maybeCustom
while(true) {
  const next = Object.getPrototypeOf(proto)
  if (next === null) {
    if (proto === maybeCustom)
      proto = null
    break
  }
  proto = next
}

There is no guarantee that it's actually the Object prototype but it should be. It would also be possible to check for the constructor name (which is also not "reliable") but I would just use this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to have consistent behavior here rather than to take a guess at the correct prototype.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The behavior might actually cause issues inside of the vm context: we return the object prototype in the "regular" context and return a null prototype otherwise. The code inside of the VM might not be aware of that and expect an object as prototype and use e.g., hasOwnProperty. Using this pattern should work in all cases where the user did not actively manipulate the passed through functions prototype and that is very unlikely.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I understand that problem. I still think consistency is more important than the problems that could occur from mis-guessing the prototype.

}
}
ret.stylize = ObjectSetPrototypeOf((value, flavour) => {
let stylized;
try {
stylized = `${ctx.stylize(value, flavour)}`;
} catch {}

if (typeof stylized !== 'string') return value;
Copy link
Member

@BridgeAR BridgeAR Jun 4, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this required? The current stylize function does not check for strings and I would keep those aligned (especially, since it's not something supported to do).

We should probably only filter objects and functions.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn’t the purpose of .stylize() to transform strings into other strings?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes but this function is not filtering anything in the "regular context". I would try to keep the behavior aligned (and it's not a supported function anyway).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes but this function is not filtering anything in the "regular context". I would try to keep the behavior aligned

I can do this if you feel strongly about it, but imo the function is broken anyway if it does not return a string.

and it's not a supported function anyway.

It’s used in examples in the documentation, so I think it qualifies as officially supported.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Also, practically speaking, I don’t think anybody is going to override this function with a custom one.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It’s used in examples in the documentation, so I think it qualifies as officially supported.

Thanks for pointing that out (even though there's no description of what it would do). I was not aware of that.

practically speaking, I don’t think anybody is going to override this function with a custom one.

The guard here would never be triggered with the internal stylize function.

the function is broken anyway if it does not return a string.

Absolutely. It would either trigger an error in such cases or it changes the printed output to the stringified value. It would therefore align the behavior more closely by using my suggestion.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

practically speaking, I don’t think anybody is going to override this function with a custom one.

The guard here would never be triggered with the internal stylize function.

See the example above.

the function is broken anyway if it does not return a string.

Absolutely. It would either trigger an error in such cases or it changes the printed output to the stringified value. It would therefore align the behavior more closely by using my suggestion.

I can add stringification here as well, inside the try/catch. If I understand correctly, that is your main concern? I don’t see any issue with that.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I’ve implemented that.

// `stylized` is a string as it should be, which is safe to pass along.
return stylized;
}, null);
}

return ret;
}

/**
Expand Down Expand Up @@ -726,7 +756,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
Expand Up @@ -2901,3 +2901,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));
}