Skip to content

Instantly share code, notes, and snippets.

@rayepeng
Created October 18, 2022 12:17
Show Gist options
  • Save rayepeng/a5ee47fccfd348d62ae7bbdf5eedb50b to your computer and use it in GitHub Desktop.
Save rayepeng/a5ee47fccfd348d62ae7bbdf5eedb50b to your computer and use it in GitHub Desktop.
function(require, host) { /* global host */
/* eslint-disable block-spacing, no-multi-spaces, brace-style, no-array-constructor, new-cap, no-use-before-define */
'use strict';
// eslint-disable-next-line no-invalid-this, no-shadow
const global = this;
const local = host.Object.create(null);
local.Object = Object;
local.Reflect = Reflect;
// global is originally prototype of host.Object so it can be used to climb up from the sandbox.
Object.setPrototypeOf(global, Object.prototype);
Object.defineProperties(global, {
global: {value: global},
GLOBAL: {value: global},
root: {value: global},
isVM: {value: true}
});
const DEBUG = false;
const OPNA = 'Operation not allowed on contextified object.';
const captureStackTrace = Error.captureStackTrace;
const FROZEN_TRAPS = host.Object.create(null);
FROZEN_TRAPS.set = (target, key) => false;
FROZEN_TRAPS.setPrototypeOf = (target, key) => false;
FROZEN_TRAPS.defineProperty = (target, key) => false;
FROZEN_TRAPS.deleteProperty = (target, key) => false;
FROZEN_TRAPS.isExtensible = (target, key) => false;
FROZEN_TRAPS.preventExtensions = (target) => false;
// Map of contextified objects to original objects
const Contextified = new host.WeakMap();
const Decontextified = new host.WeakMap();
// Fake setters make sure we use correctly scoped definer for getter/setter definition
function fakeDefineGetter(receiver, useLocalDefiner) {
return function __defineGetter__(key, value) {
(useLocalDefiner ? local.Object : host.Object).defineProperty(receiver, key, {get: value, enumerable: true, configurable: true});
};
}
function fakeDefineSetter(receiver, useLocalDefiner) {
return function __defineSetter__(key, value) {
(useLocalDefiner ? local.Object : host.Object).defineProperty(receiver, key, {set: value, enumerable: true, configurable: true});
};
}
function fakeLookupGetter(receiver, useLocalLookup) {
return function __lookupGetter__(key, value) {
const descriptor = (useLocalLookup ? local.Object : host.Object).getOwnPropertyDescriptor(receiver, key);
return descriptor && descriptor.get;
};
}
function fakeLookupSetter(receiver, useLocalLookup) {
return function __lookupSetter__(key, value) {
const descriptor = (useLocalLookup ? local.Object : host.Object).getOwnPropertyDescriptor(receiver, key);
return descriptor && descriptor.set;
};
}
// We can't use host's hasInstance method
const hasInstance = local.Object[Symbol.hasInstance];
function instanceOf(value, construct) {
return host.Reflect.apply(hasInstance, construct, [value]);
}
/**
* VMError definition.
*/
class VMError extends Error {
constructor(message, code) {
super(message);
this.name = 'VMError';
this.code = code;
captureStackTrace(this, this.constructor);
}
}
global.VMError = VMError;
/**
* Decontextify.
*/
const Decontextify = host.Object.create(null);
Decontextify.proxies = new host.WeakMap();
Decontextify.arguments = args => {
if (!host.Array.isArray(args)) return new host.Array();
const arr = new host.Array();
for (let i = 0, l = args.length; i < l; i++) arr[i] = Decontextify.value(args[i]);
return arr;
};
Decontextify.instance = (instance, klass, deepTraps, flags) => {
if (typeof instance === 'function') return Decontextify.function(instance);
// We must not use normal object because there's a chance object already contains malicious code in the prototype
const base = host.Object.create(null);
base.get = (target, key, receiver) => {
if (key === 'vmProxyTarget' && DEBUG) return instance;
if (key === 'isVMProxy') return true;
if (key === 'constructor') return klass;
if (key === '__proto__') return klass.prototype;
if (key === '__defineGetter__') return fakeDefineGetter(receiver);
if (key === '__defineSetter__') return fakeDefineSetter(receiver);
if (key === '__lookupGetter__') return fakeLookupGetter(receiver);
if (key === '__lookupSetter__') return fakeLookupSetter(receiver);
try {
return Decontextify.value(instance[key], null, deepTraps, flags);
} catch (e) {
throw Decontextify.value(e);
}
};
base.getPrototypeOf = (target) => {
return klass && klass.prototype;
};
return Decontextify.object(instance, base, deepTraps, flags);
};
Decontextify.function = (fnc, traps, deepTraps, flags, mock) => {
// We must not use normal object because there's a chance object already contains malicious code in the prototype
const base = host.Object.create(null);
let proxy;
base.apply = (target, context, args) => {
try {
context = Contextify.value(context);
// Set context of all arguments to vm's context.
return Decontextify.value(fnc.apply(context, Contextify.arguments(args)));
} catch (e) {
throw Decontextify.value(e);
}
};
base.construct = (target, args, newTarget) => {
try {
return Decontextify.instance(new fnc(...Contextify.arguments(args)), proxy, deepTraps, flags);
} catch (e) {
throw Decontextify.value(e);
}
};
base.get = (target, key, receiver) => {
if (key === 'vmProxyTarget' && DEBUG) return fnc;
if (key === 'isVMProxy') return true;
if (mock && host.Object.prototype.hasOwnProperty.call(mock, key)) return mock[key];
if (key === 'constructor') return host.Function;
if (key === '__proto__') return host.Function.prototype;
if (key === '__defineGetter__') return fakeDefineGetter(receiver);
if (key === '__defineSetter__') return fakeDefineSetter(receiver);
if (key === '__lookupGetter__') return fakeLookupGetter(receiver);
if (key === '__lookupSetter__') return fakeLookupSetter(receiver);
try {
return Decontextify.value(fnc[key], null, deepTraps, flags);
} catch (e) {
throw Decontextify.value(e);
}
};
base.getPrototypeOf = (target) => {
return host.Function.prototype;
};
proxy = Decontextify.object(fnc, host.Object.assign(base, traps), deepTraps);
return proxy;
};
Decontextify.object = (object, traps, deepTraps, flags, mock) => {
// We must not use normal object because there's a chance object already contains malicious code in the prototype
const base = host.Object.create(null);
base.get = (target, key, receiver) => {
if (key === 'vmProxyTarget' && DEBUG) return object;
if (key === 'isVMProxy') return true;
if (mock && host.Object.prototype.hasOwnProperty.call(mock, key)) return mock[key];
if (key === 'constructor') return host.Object;
if (key === '__proto__') return host.Object.prototype;
if (key === '__defineGetter__') return fakeDefineGetter(receiver);
if (key === '__defineSetter__') return fakeDefineSetter(receiver);
if (key === '__lookupGetter__') return fakeLookupGetter(receiver);
if (key === '__lookupSetter__') return fakeLookupSetter(receiver);
try {
return Decontextify.value(object[key], null, deepTraps, flags);
} catch (e) {
throw Decontextify.value(e);
}
};
base.set = (target, key, value, receiver) => {
try {
object[key] = Contextify.value(value);
return true;
} catch (e) {
throw Decontextify.value(e);
}
};
base.getOwnPropertyDescriptor = (target, prop) => {
let def;
try {
def = host.Object.getOwnPropertyDescriptor(object, prop);
} catch (e) {
throw Decontextify.value(e);
}
// Following code prevents V8 to throw
// TypeError: 'getOwnPropertyDescriptor' on proxy: trap reported non-configurability for property '<prop>'
// which is either non-existant or configurable in the proxy target
if (!def) {
return undefined;
} else if (def.get || def.set) {
return {
get: Decontextify.value(def.get) || undefined,
set: Decontextify.value(def.set) || undefined,
enumerable: def.enumerable === true,
configurable: def.configurable === true
};
} else {
return {
value: Decontextify.value(def.value),
writable: def.writable === true,
enumerable: def.enumerable === true,
configurable: def.configurable === true
};
}
};
base.defineProperty = (target, key, descriptor) => {
// There's a chance accessing a property throws an error so we must not access them
// in try catch to prevent contextyfing local objects.
const propertyDescriptor = host.Object.create(null);
if (descriptor.get || descriptor.set) {
propertyDescriptor.get = Contextify.value(descriptor.get, null, deepTraps, flags) || undefined;
propertyDescriptor.set = Contextify.value(descriptor.set, null, deepTraps, flags) || undefined;
propertyDescriptor.enumerable = descriptor.enumerable === true;
propertyDescriptor.configurable = descriptor.configurable === true;
} else {
propertyDescriptor.value = Contextify.value(descriptor.value, null, deepTraps, flags);
propertyDescriptor.writable = descriptor.writable === true;
propertyDescriptor.enumerable = descriptor.enumerable === true;
propertyDescriptor.configurable = descriptor.configurable === true;
}
try {
return host.Object.defineProperty(target, key, propertyDescriptor);
} catch (e) {
throw Decontextify.value(e);
}
};
base.getPrototypeOf = (target) => {
return host.Object.prototype;
};
base.setPrototypeOf = (target) => {
throw new host.Error(OPNA);
};
base.has = (target, key) => {
try {
return key in object;
} catch (e) {
throw Decontextify.value(e);
}
};
base.isExtensible = target => {
try {
return Decontextify.value(local.Object.isExtensible(object));
} catch (e) {
throw Decontextify.value(e);
}
};
base.ownKeys = target => {
try {
return Decontextify.value(local.Reflect.ownKeys(object));
} catch (e) {
throw Decontextify.value(e);
}
};
base.preventExtensions = target => {
try {
local.Object.preventExtensions(object);
return true;
} catch (e) {
throw Decontextify.value(e);
}
};
base.enumerate = target => {
try {
return Decontextify.value(local.Reflect.enumerate(object));
} catch (e) {
throw Decontextify.value(e);
}
};
const proxy = new host.Proxy(object, host.Object.assign(base, traps, deepTraps));
Decontextify.proxies.set(object, proxy);
Decontextified.set(proxy, object);
return proxy;
};
Decontextify.value = (value, traps, deepTraps, flags, mock) => {
if (Contextified.has(value)) {
// Contextified object has returned back from vm
return Contextified.get(value);
} else if (Decontextify.proxies.has(value)) {
// Decontextified proxy already exists, reuse
return Decontextify.proxies.get(value);
}
try {
// If for some reason we get already scoped value, get out.
// Decontextifying already scoped value breaks the security.
if (instanceOf(value, host.Object)) return value;
} catch (e) {
throw new VMError('Failed to decontextify object.');
}
switch (typeof value) {
case 'object':
try {
if (value === null) {
return null;
} else if (instanceOf(value, Number)) { return host.Number(value);
} else if (instanceOf(value, String)) { return host.String(value);
} else if (instanceOf(value, Boolean)) { return host.Boolean(value);
} else if (instanceOf(value, Date)) { return Decontextify.instance(value, host.Date, deepTraps, flags);
} else if (instanceOf(value, RangeError)) { return Decontextify.instance(value, host.RangeError, deepTraps, flags);
} else if (instanceOf(value, ReferenceError)) { return Decontextify.instance(value, host.ReferenceError, deepTraps, flags);
} else if (instanceOf(value, SyntaxError)) { return Decontextify.instance(value, host.SyntaxError, deepTraps, flags);
} else if (instanceOf(value, TypeError)) { return Decontextify.instance(value, host.TypeError, deepTraps, flags);
} else if (instanceOf(value, VMError)) { return Decontextify.instance(value, host.VMError, deepTraps, flags);
} else if (instanceOf(value, Error)) { return Decontextify.instance(value, host.Error, deepTraps, flags);
} else if (instanceOf(value, Array)) { return Decontextify.instance(value, host.Array, deepTraps, flags);
} else if (instanceOf(value, RegExp)) { return Decontextify.instance(value, host.RegExp, deepTraps, flags);
} else if (instanceOf(value, Map)) { return Decontextify.instance(value, host.Map, deepTraps, flags);
} else if (instanceOf(value, WeakMap)) { return Decontextify.instance(value, host.WeakMap, deepTraps, flags);
} else if (instanceOf(value, Set)) { return Decontextify.instance(value, host.Set, deepTraps, flags);
} else if (instanceOf(value, WeakSet)) { return Decontextify.instance(value, host.WeakSet, deepTraps, flags);
} else if (Promise && instanceOf(value, Promise)) { return Decontextify.instance(value, host.Promise, deepTraps, flags);
} else if (host.Object.getPrototypeOf(value) === null) {
return Decontextify.instance(value, null, deepTraps, flags);
} else {
return Decontextify.object(value, traps, deepTraps, flags, mock);
}
} catch (e) {
throw Decontextify.value(e);
}
case 'function':
return Decontextify.function(value, traps, deepTraps, flags, mock);
case 'undefined':
return undefined;
default: // string, number, boolean, symbol
return value;
}
};
/**
* Contextify.
*/
const Contextify = host.Object.create(null);
Contextify.proxies = new host.WeakMap();
Contextify.arguments = args => {
if (!host.Array.isArray(args)) return new Array();
const arr = new Array();
for (let i = 0, l = args.length; i < l; i++) arr[i] = Contextify.value(args[i]);
return arr;
};
Contextify.instance = (instance, klass, deepTraps, flags) => {
if (typeof instance === 'function') return Contextify.function(instance);
// We must not use normal object because there's a chance object already contains malicious code in the prototype
const base = host.Object.create(null);
base.get = (target, key, receiver) => {
if (key === 'vmProxyTarget' && DEBUG) return instance;
if (key === 'isVMProxy') return true;
if (key === 'constructor') return klass;
if (key === '__proto__') return klass.prototype;
if (key === '__defineGetter__') return fakeDefineGetter(receiver, true);
if (key === '__defineSetter__') return fakeDefineSetter(receiver, true);
if (key === '__lookupGetter__') return fakeLookupGetter(receiver, true);
if (key === '__lookupSetter__') return fakeLookupSetter(receiver, true);
try {
return Contextify.value(instance[key], null, deepTraps, flags);
} catch (e) {
throw Contextify.value(e);
}
};
base.getPrototypeOf = (target) => {
return klass && klass.prototype;
};
return Contextify.object(instance, base, deepTraps, flags);
};
Contextify.function = (fnc, traps, deepTraps, flags, mock) => {
// We must not use normal object because there's a chance object already contains malicious code in the prototype
const base = host.Object.create(null);
let proxy;
base.apply = (target, context, args) => {
try {
context = Decontextify.value(context);
// Set context of all arguments to host's context.
return Contextify.value(fnc.apply(context, Decontextify.arguments(args)));
} catch (e) {
throw Contextify.value(e);
}
};
base.construct = (target, args, newTarget) => {
// Fixes buffer unsafe allocation for node v6/7
if (host.version < 8 && fnc === host.Buffer && 'number' === typeof args[0]) {
args[0] = new Array(args[0]).fill(0);
}
try {
return Contextify.instance(new fnc(...Decontextify.arguments(args)), proxy, deepTraps, flags);
} catch (e) {
throw Contextify.value(e);
}
};
base.get = (target, key, receiver) => {
if (key === 'vmProxyTarget' && DEBUG) return fnc;
if (key === 'isVMProxy') return true;
if (mock && host.Object.prototype.hasOwnProperty.call(mock, key)) return mock[key];
if (key === 'constructor') return Function;
if (key === '__proto__') return Function.prototype;
if (key === '__defineGetter__') return fakeDefineGetter(receiver, true);
if (key === '__defineSetter__') return fakeDefineSetter(receiver, true);
if (key === '__lookupGetter__') return fakeLookupGetter(receiver, true);
if (key === '__lookupSetter__') return fakeLookupSetter(receiver, true);
try {
return Contextify.value(fnc[key], null, deepTraps, flags);
} catch (e) {
throw Contextify.value(e);
}
};
base.getPrototypeOf = (target) => {
return Function.prototype;
};
proxy = Contextify.object(fnc, host.Object.assign(base, traps), deepTraps);
return proxy;
};
Contextify.object = (object, traps, deepTraps, flags, mock) => {
// We must not use normal object because there's a chance object already contains malicious code in the prototype
const base = host.Object.create(null);
base.get = (target, key, receiver) => {
if (key === 'vmProxyTarget' && DEBUG) return object;
if (key === 'isVMProxy') return true;
if (mock && host.Object.prototype.hasOwnProperty.call(mock, key)) return mock[key];
if (key === 'constructor') return Object;
if (key === '__proto__') return Object.prototype;
if (key === '__defineGetter__') return fakeDefineGetter(receiver, true);
if (key === '__defineSetter__') return fakeDefineSetter(receiver, true);
if (key === '__lookupGetter__') return fakeLookupGetter(receiver, true);
if (key === '__lookupSetter__') return fakeLookupSetter(receiver, true);
try {
return Contextify.value(object[key], null, deepTraps, flags);
} catch (e) {
throw Contextify.value(e);
}
};
base.set = (target, key, value, receiver) => {
if (flags && flags.protected && typeof value === 'function') return false;
try {
object[key] = Decontextify.value(value);
return true;
} catch (e) {
throw Contextify.value(e);
}
};
base.getOwnPropertyDescriptor = (target, prop) => {
let def;
try {
def = host.Object.getOwnPropertyDescriptor(object, prop);
} catch (e) {
throw Contextify.value(e);
}
// Following code prevents V8 to throw
// TypeError: 'getOwnPropertyDescriptor' on proxy: trap reported non-configurability for property '<prop>'
// which is either non-existant or configurable in the proxy target
if (!def) {
return undefined;
} else if (def.get || def.set) {
return {
get: Contextify.value(def.get, null, deepTraps, flags) || undefined,
set: Contextify.value(def.set, null, deepTraps, flags) || undefined,
enumerable: def.enumerable === true,
configurable: def.configurable === true
};
} else {
return {
value: Contextify.value(def.value, null, deepTraps, flags),
writable: def.writable === true,
enumerable: def.enumerable === true,
configurable: def.configurable === true
};
}
};
base.defineProperty = (target, key, descriptor) => {
// There's a chance accessing a property throws an error so we must not access them
// in try catch to prevent contextyfing local objects.
if (flags && flags.protected) {
if (descriptor.get || descriptor.set || typeof descriptor.value === 'function') return false;
}
const propertyDescriptor = host.Object.create(null);
if (descriptor.get || descriptor.set) {
propertyDescriptor.get = Decontextify.value(descriptor.get, null, deepTraps, flags) || undefined;
propertyDescriptor.set = Decontextify.value(descriptor.set, null, deepTraps, flags) || undefined;
propertyDescriptor.enumerable = descriptor.enumerable === true;
propertyDescriptor.configurable = descriptor.configurable === true;
} else {
propertyDescriptor.value = Decontextify.value(descriptor.value, null, deepTraps, flags);
propertyDescriptor.writable = descriptor.writable === true;
propertyDescriptor.enumerable = descriptor.enumerable === true;
propertyDescriptor.configurable = descriptor.configurable === true;
}
try {
return host.Object.defineProperty(object, key, propertyDescriptor);
} catch (e) {
throw Contextify.value(e);
}
};
base.getPrototypeOf = (target) => {
return local.Object.prototype;
};
base.setPrototypeOf = (target) => {
throw new VMError(OPNA);
};
base.has = (target, key) => {
try {
return key in object;
} catch (e) {
throw Contextify.value(e);
}
};
base.isExtensible = target => {
try {
return Contextify.value(host.Object.isExtensible(object));
} catch (e) {
throw Contextify.value(e);
}
};
base.ownKeys = target => {
try {
return Contextify.value(host.Reflect.ownKeys(object));
} catch (e) {
throw Contextify.value(e);
}
};
base.preventExtensions = target => {
try {
host.Object.preventExtensions(object);
return true;
} catch (e) {
throw Contextify.value(e);
}
};
base.enumerate = target => {
try {
return Contextify.value(host.Reflect.enumerate(object));
} catch (e) {
throw Contextify.value(e);
}
};
const proxy = new host.Proxy(object, host.Object.assign(base, traps, deepTraps));
Contextify.proxies.set(object, proxy);
Contextified.set(proxy, object);
return proxy;
};
Contextify.value = (value, traps, deepTraps, flags, mock) => {
if (Decontextified.has(value)) {
// Decontextified object has returned back to vm
return Decontextified.get(value);
} else if (Contextify.proxies.has(value)) {
// Contextified proxy already exists, reuse
return Contextify.proxies.get(value);
}
try {
// If for some reason we get already scoped value, get out.
// Contextifying already scoped value breaks the security.
if (instanceOf(value, local.Object)) return value;
} catch (e) {
throw new VMError('Failed to contextify object.');
}
switch (typeof value) {
case 'object':
try {
if (value === null) {
return null;
} else if (instanceOf(value, host.Number)) { return host.Number(value);
} else if (instanceOf(value, host.String)) { return host.String(value);
} else if (instanceOf(value, host.Boolean)) { return host.Boolean(value);
} else if (instanceOf(value, host.Date)) { return Contextify.instance(value, Date, deepTraps, flags);
} else if (instanceOf(value, host.RangeError)) { return Contextify.instance(value, RangeError, deepTraps, flags);
} else if (instanceOf(value, host.ReferenceError)) { return Contextify.instance(value, ReferenceError, deepTraps, flags);
} else if (instanceOf(value, host.SyntaxError)) { return Contextify.instance(value, SyntaxError, deepTraps, flags);
} else if (instanceOf(value, host.TypeError)) { return Contextify.instance(value, TypeError, deepTraps, flags);
} else if (instanceOf(value, host.VMError)) { return Contextify.instance(value, VMError, deepTraps, flags);
} else if (instanceOf(value, host.Error)) { return Contextify.instance(value, Error, deepTraps, flags);
} else if (instanceOf(value, host.Array)) { return Contextify.instance(value, Array, deepTraps, flags);
} else if (instanceOf(value, host.RegExp)) { return Contextify.instance(value, RegExp, deepTraps, flags);
} else if (instanceOf(value, host.Map)) { return Contextify.instance(value, Map, deepTraps, flags);
} else if (instanceOf(value, host.WeakMap)) { return Contextify.instance(value, WeakMap, deepTraps, flags);
} else if (instanceOf(value, host.Set)) { return Contextify.instance(value, Set, deepTraps, flags);
} else if (instanceOf(value, host.WeakSet)) { return Contextify.instance(value, WeakSet, deepTraps, flags);
} else if (instanceOf(value, host.Promise)) { return Contextify.instance(value, Promise, deepTraps, flags);
} else if (instanceOf(value, host.Buffer)) { return Contextify.instance(value, LocalBuffer, deepTraps, flags);
} else if (host.Object.getPrototypeOf(value) === null) {
return Contextify.instance(value, null, deepTraps, flags);
} else {
return Contextify.object(value, traps, deepTraps, flags, mock);
}
} catch (e) {
throw Contextify.value(e);
}
case 'function':
return Contextify.function(value, traps, deepTraps, flags, mock);
case 'undefined':
return undefined;
default: // string, number, boolean, symbol
return value;
}
};
Contextify.globalValue = (value, name) => {
return (global[name] = Contextify.value(value));
};
Contextify.readonly = (value, mock) => {
return Contextify.value(value, null, FROZEN_TRAPS, null, mock);
};
Contextify.protected = (value, mock) => {
return Contextify.value(value, null, null, {protected: true}, mock);
};
const LocalBuffer = global.Buffer = Contextify.readonly(host.Buffer, {
allocUnsafe: function allocUnsafe(size) {
return this.alloc(size);
},
allocUnsafeSlow: function allocUnsafeSlow(size) {
return this.alloc(size);
}
});
return {
Contextify,
Decontextify,
Buffer: LocalBuffer
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment