-
Notifications
You must be signed in to change notification settings - Fork 29.8k
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
Changes from all commits
b5bd87c
583c1bd
8544be7
7b0b27b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,6 +12,7 @@ const { | |
DatePrototypeToString, | ||
ErrorPrototypeToString, | ||
Float32Array, | ||
FunctionPrototypeCall, | ||
FunctionPrototypeToString, | ||
JSONStringify, | ||
Map, | ||
|
@@ -25,6 +26,7 @@ const { | |
Number, | ||
NumberIsNaN, | ||
NumberPrototypeValueOf, | ||
Object, | ||
ObjectAssign, | ||
ObjectCreate, | ||
ObjectDefineProperty, | ||
|
@@ -37,6 +39,7 @@ const { | |
ObjectPrototypeHasOwnProperty, | ||
ObjectPrototypePropertyIsEnumerable, | ||
ObjectSeal, | ||
ObjectSetPrototypeOf, | ||
RegExp, | ||
RegExpPrototypeToString, | ||
Set, | ||
|
@@ -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, | ||
|
@@ -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]; | ||
} | ||
} | ||
ret.stylize = ObjectSetPrototypeOf((value, flavour) => { | ||
let stylized; | ||
try { | ||
stylized = `${ctx.stylize(value, flavour)}`; | ||
} catch {} | ||
|
||
if (typeof stylized !== 'string') return value; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
We should probably only filter objects and functions. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isn’t the purpose of There was a problem hiding this comment. Choose a reason for hiding this commentThe 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). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I can do this if you feel strongly about it, but imo the function is broken anyway if it does not return a string.
It’s used in examples in the documentation, so I think it qualifies as officially supported. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Thanks for pointing that out (even though there's no description of what it would do). I was not aware of that.
The guard here would never be triggered with the internal stylize function.
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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
See the example above.
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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
} | ||
|
||
/** | ||
|
@@ -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) { | ||
|
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.There was a problem hiding this comment.
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:
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.There was a problem hiding this comment.
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.