|
| 1 | +/* eslint-disable @typescript-eslint/ban-types */ |
| 2 | +// Object, Function, Array, Promise, RegExp, Error, EvalError, RangeError, |
| 3 | +// ReferenceError, SyntaxError, TypeError and URIError are intentionally |
| 4 | +// not passed into the sandbox. There's a trade-off here, ideally we'd like all |
| 5 | +// these checks to pass: |
| 6 | +// |
| 7 | +// ```js |
| 8 | +// import vm from "vm"; |
| 9 | +// |
| 10 | +// const ctx1 = vm.createContext({ objectFunction: () => ({}) }); |
| 11 | +// |
| 12 | +// vm.runInContext("({}) instanceof Object", ctx1); // true |
| 13 | +// vm.runInContext("({}).constructor === Object", ctx1); // true |
| 14 | +// |
| 15 | +// vm.runInContext("objectFunction() instanceof Object", ctx1); // false |
| 16 | +// vm.runInContext("objectFunction().constructor === Object", ctx1); // false |
| 17 | +// |
| 18 | +// const ctx2 = vm.createContext({ Object: Object, objectFunction: () => ({}) }); |
| 19 | +// |
| 20 | +// vm.runInContext("({}) instanceof Object", ctx2); // false |
| 21 | +// vm.runInContext("({}).constructor === Object", ctx2); // false |
| 22 | +// |
| 23 | +// vm.runInContext("objectFunction() instanceof Object", ctx2); // true |
| 24 | +// vm.runInContext("objectFunction().constructor === Object", ctx2); // true |
| 25 | +// ``` |
| 26 | +// |
| 27 | +// wasm-bindgen (a tool used to make compiling Rust to WebAssembly easier), |
| 28 | +// often generates code that looks like `value instanceof Object`. |
| 29 | +// We'd like this check to succeed for objects generated outside the worker |
| 30 | +// (e.g. Workers runtime APIs), and inside user code (instances of classes we |
| 31 | +// pass in e.g. `new Uint8Array()`, and literals e.g. `{}`). To do this, we |
| 32 | +// override the `[Symbol.hasInstance]` property of primitive classes like |
| 33 | +// `Object` so `instanceof` performs a cross-realm check. |
| 34 | +// See `defineHasInstancesScript` later in this file. |
| 35 | +// |
| 36 | +// Historically, we used to do this by proxying the primitive classes instead: |
| 37 | +// |
| 38 | +// ```js |
| 39 | +// function isObject(value) { |
| 40 | +// return value !== null && typeof value === "object"; |
| 41 | +// } |
| 42 | +// |
| 43 | +// const ObjectProxy = new Proxy(Object, { |
| 44 | +// get(target, property, receiver) { |
| 45 | +// if (property === Symbol.hasInstance) return isObject; |
| 46 | +// return Reflect.get(target, property, receiver); |
| 47 | +// }, |
| 48 | +// }); |
| 49 | +// |
| 50 | +// const ctx3 = vm.createContext({ |
| 51 | +// Object: ObjectProxy, |
| 52 | +// objectFunction: () => ({}), |
| 53 | +// }); |
| 54 | +// |
| 55 | +// vm.runInContext("({}) instanceof Object", ctx3); // true |
| 56 | +// vm.runInContext("({}).constructor === Object", ctx3); // false |
| 57 | +// |
| 58 | +// vm.runInContext("objectFunction() instanceof Object", ctx3); // true |
| 59 | +// vm.runInContext("objectFunction().constructor === Object", ctx3); // false |
| 60 | +// ``` |
| 61 | +// |
| 62 | +// The problem with this option is that the `constructor`/`prototype` checks |
| 63 | +// fail, because we're passing in the `Object` from the outer realm. |
| 64 | +// These are used quite a lot in JS, and this was the cause of several issues: |
| 65 | +// - https://github.com/cloudflare/miniflare/issues/109 |
| 66 | +// - https://github.com/cloudflare/miniflare/issues/137 |
| 67 | +// - https://github.com/cloudflare/miniflare/issues/141 |
| 68 | +// - https://github.com/cloudflare/wrangler2/issues/91 |
| 69 | +// |
| 70 | +// The new behaviour still has the issue `constructor`/`prototype`/`instanceof` |
| 71 | +// checks for `Object`s created outside the sandbox would fail, but I think |
| 72 | +// that's less likely to be a problem. |
| 73 | + |
| 74 | +import util from "util"; |
| 75 | +import vm from "vm"; |
| 76 | + |
| 77 | +// https://nodejs.org/api/util.html#util_util_isobject_object |
| 78 | +function isObject(value: any): value is Object { |
| 79 | + return value !== null && typeof value === "object"; |
| 80 | +} |
| 81 | + |
| 82 | +// https://nodejs.org/api/util.html#util_util_isfunction_object |
| 83 | +function isFunction(value: any): value is Function { |
| 84 | + return typeof value === "function"; |
| 85 | +} |
| 86 | + |
| 87 | +function isError<Ctor extends ErrorConstructor>(errorCtor: Ctor) { |
| 88 | + const name = errorCtor.prototype.name; |
| 89 | + return function (value: any): value is InstanceType<Ctor> { |
| 90 | + if (!util.types.isNativeError(value)) return false; |
| 91 | + // Traverse up prototype chain and check for matching name |
| 92 | + let prototype = value; |
| 93 | + while ((prototype = Object.getPrototypeOf(prototype)) !== null) { |
| 94 | + if (prototype.name === name) return true; |
| 95 | + } |
| 96 | + return false; |
| 97 | + }; |
| 98 | +} |
| 99 | + |
| 100 | +const types = { |
| 101 | + isObject, |
| 102 | + isFunction, |
| 103 | + isArray: Array.isArray, |
| 104 | + isPromise: util.types.isPromise, |
| 105 | + isRegExp: util.types.isRegExp, |
| 106 | + isError, |
| 107 | +}; |
| 108 | + |
| 109 | +const defineHasInstancesScript = new vm.Script( |
| 110 | + `(function(types) { |
| 111 | + // Only define properties once, will throw if we try doing this twice |
| 112 | + if (Object[Symbol.hasInstance] === types.isObject) return; |
| 113 | + Object.defineProperty(Object, Symbol.hasInstance, { value: types.isObject }); |
| 114 | + Object.defineProperty(Function, Symbol.hasInstance, { value: types.isFunction }); |
| 115 | + Object.defineProperty(Array, Symbol.hasInstance, { value: types.isArray }); |
| 116 | + Object.defineProperty(Promise, Symbol.hasInstance, { value: types.isPromise }); |
| 117 | + Object.defineProperty(RegExp, Symbol.hasInstance, { value: types.isRegExp }); |
| 118 | + Object.defineProperty(Error, Symbol.hasInstance, { value: types.isError(Error) }); |
| 119 | + Object.defineProperty(EvalError, Symbol.hasInstance, { value: types.isError(EvalError) }); |
| 120 | + Object.defineProperty(RangeError, Symbol.hasInstance, { value: types.isError(RangeError) }); |
| 121 | + Object.defineProperty(ReferenceError, Symbol.hasInstance, { value: types.isError(ReferenceError) }); |
| 122 | + Object.defineProperty(SyntaxError, Symbol.hasInstance, { value: types.isError(SyntaxError) }); |
| 123 | + Object.defineProperty(TypeError, Symbol.hasInstance, { value: types.isError(TypeError) }); |
| 124 | + Object.defineProperty(URIError, Symbol.hasInstance, { value: types.isError(URIError) }); |
| 125 | +})`, |
| 126 | + { filename: "<defineHasInstancesScript>" } |
| 127 | +); |
| 128 | + |
| 129 | +// This is called on each new vm.Context before executing arbitrary user code |
| 130 | +export function defineHasInstances(ctx: vm.Context): void { |
| 131 | + defineHasInstancesScript.runInContext(ctx)(types); |
| 132 | +} |
0 commit comments