CVE-2021-23449: Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution') (CWE-1321)
value: This affects the package vm2 before 3.9.4 via a Prototype Pollution attack vector, which can lead to execution of arbitrary code on the host machine.
CWE description: The product receives input from an upstream component that specifies attributes that are to be initialized or updated in an object, but it does not properly control modifications of attributes of the object prototype.
CVSS v3 base score: 10
Published: 2021-10-18T17:15Z
Repository: https://github.com/patriksimek/vm2
Language: JavaScript
Files changed: lib/contextify.js, lib/main.js, lib/sandbox.js, test/vm.js
Commit date: 2021-10-12 19:05:01 +0200
Commit message: Security Fixes

Diff: diff --git a/lib/contextify.js b/lib/contextify.js
index 621d4d4..49e404a 100644
--- a/lib/contextify.js
+++ b/lib/contextify.js
@@ -22,15 +22,15 @@ local.Reflect.defineProperty = Reflect.defineProperty;
 local.Reflect.setPrototypeOf = Reflect.setPrototypeOf;
 local.Reflect.isExtensible = Reflect.isExtensible;
 local.Reflect.preventExtensions = Reflect.preventExtensions;
 local.Reflect.getOwnPropertyDescriptor = Reflect.getOwnPropertyDescriptor;
 
-function curry(func) {
+function uncurryThis(func) {
 	return (thiz, args) => local.Reflect.apply(func, thiz, args);
 }
 
-const FunctionBind = curry(Function.prototype.bind);
+const FunctionBind = uncurryThis(Function.prototype.bind);
 
 // 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, {
@@ -67,11 +67,11 @@ const FROZEN_TRAPS = {
 // Map of contextified objects to original objects
 const Contextified = new host.WeakMap();
 const Decontextified = new host.WeakMap();
 
 // We can't use host's hasInstance method
-const ObjectHasInstance = curry(local.Object[Symbol.hasInstance]);
+const ObjectHasInstance = uncurryThis(local.Object[Symbol.hasInstance]);
 function instanceOf(value, construct) {
 	try {
 		return ObjectHasInstance(construct, [value]);
 	} catch (ex) {
 		// Never pass the handled exception through!
@@ -999,10 +999,135 @@ BufferOverride.inspect = function inspect(recurseTimes, ctx) {
 	if (remaining > 0) str += ` ... ${remaining} more byte${remaining > 1 ? 's' : ''}`;
 	return `<${this.constructor.name} ${str}>`;
 };
 const LocalBuffer = global.Buffer = Contextify.readonly(host.Buffer, BufferMock);
 Contextify.connect(host.Buffer.prototype.inspect, BufferOverride.inspect);
+Contextify.connect(host.Function.prototype.bind, Function.prototype.bind);
+
+const oldPrepareStackTraceDesc = Reflect.getOwnPropertyDescriptor(Error, 'prepareStackTrace');
+
+let currentPrepareStackTrace = Error.prepareStackTrace;
+const wrappedPrepareStackTrace = new host.WeakMap();
+if (typeof currentPrepareStackTrace === 'function') {
+	wrappedPrepareStackTrace.set(currentPrepareStackTrace, currentPrepareStackTrace);
+}
+
+let OriginalCallSite;
+Error.prepareStackTrace = (e, sst) => {
+	OriginalCallSite = sst[0].constructor;
+};
+new Error().stack;
+if (typeof OriginalCallSite === 'function') {
+	Error.prepareStackTrace = undefined;
+
+	function makeCallSiteGetters(list) {
+		const callSiteGetters = [];
+		for (let i=0; i<list.length; i++) {
+			const name = list[i];
+			const func = OriginalCallSite.prototype[name];
+			callSiteGetters[i] = {__proto__: null,
+				name,
+				propName: '_' + name,
+				func: (thiz) => {
+					return local.Reflect.apply(func, thiz, []);
+				}
+			};
+		}
+		return callSiteGetters;
+	}
+
+	function applyCallSiteGetters(callSite, getters) {
+		const properties = {__proto__: null};
+		for (let i=0; i<getters.length; i++) {
+			const getter = getters[i];
+			properties[getter.propName] = {
+				__proto__: null,
+				value: getter.func(callSite)
+			};
+		}
+		return properties;
+	}
+
+	const callSiteGetters = makeCallSiteGetters([
+		'getTypeName',
+		'getFunctionName',
+		'getMethodName',
+		'getFileName',
+		'getLineNumber',
+		'getColumnNumber',
+		'getEvalOrigin',
+		'isToplevel',
+		'isEval',
+		'isNative',
+		'isConstructor',
+		'isAsync',
+		'isPromiseAll',
+		'getPromiseIndex'
+	]);
+
+	class CallSite {
+		constructor(callSite) {
+			Object.defineProperties(this, applyCallSiteGetters(callSite, callSiteGetters));
+		}
+		getThis() {return undefined;}
+		getFunction() {return undefined;}
+		toString() {return 'CallSite {}';}
+	}
+
+	(function setupCallSite() {
+		for (let i=0; i<callSiteGetters.length; i++) {
+			const name = callSiteGetters[i].name;
+			const propertyName = callSiteGetters[i].propName;
+			const func = {func() {
+				return this[propertyName];
+			}}.func;
+			const nameProp = Object.getOwnPropertyDescriptor(func, 'name');
+			nameProp.value = name;
+			Object.defineProperty(func, 'name', nameProp);
+			const funcProp = Object.getOwnPropertyDescriptor(OriginalCallSite.prototype, name);
+			funcProp.value = func;
+			Object.defineProperty(CallSite.prototype, name, funcProp);
+		}
+	})();
+
+	Object.defineProperty(Error, 'prepareStackTrace', {
+		configurable: false,
+		enumerable: false,
+		get() {
+			return currentPrepareStackTrace;
+		},
+		set(value) {
+			if (typeof(value) !== 'function') {
+				currentPrepareStackTrace = value;
+				return;
+			}
+			const wrapped = wrappedPrepareStackTrace.get(value);
+			if (wrapped) {
+				currentPrepareStackTrace = wrapped;
+				return;
+			}
+			const newWrapped = (error, sst) => {
+				if (host.Array.isArray(sst)) {
+					for (let i=0; i<sst.length; i++) {
+						const cs = sst[i];
+						if (typeof cs === 'object' && local.Reflect.getPrototypeOf(cs) === OriginalCallSite.prototype) {
+							sst[i] = new CallSite(cs);
+						}
+					}
+				}
+				return value(error, sst);
+			};
+			wrappedPrepareStackTrace.set(value, newWrapped);
+			wrappedPrepareStackTrace.set(newWrapped, newWrapped);
+			currentPrepareStackTrace = newWrapped;
+		}
+	});
+} else if (oldPrepareStackTraceDesc) {
+	Reflect.defineProperty(Error, 'prepareStackTrace', oldPrepareStackTraceDesc);
+} else {
+	Reflect.deleteProperty(Error, 'prepareStackTrace');
+}
 
 
 const exportsMap = host.Object.create(null);
 exportsMap.Contextify = Contextify;
 exportsMap.Decontextify = Decontextify;
diff --git a/lib/main.js b/lib/main.js
index 05ed86f..1b0d867 100644
--- a/lib/main.js
+++ b/lib/main.js
@@ -542,51 +542,64 @@ function doWithTimeout(fn, timeout) {
 	} finally {
 		ctx.fn = null;
 	}
 }
 
-/**
- * Creates the hook to check for the use of async.
- * 
- * @private
- * @param {*} internal - The internal vm object.
- * @return {*} The hook function
- */
-function makeCheckAsync(internal) {
+function tryCompile(args) {
+	const code = args[args.length - 1];
+	const params = args.slice(0, -1);
+	vm.compileFunction(code, params);
+}
+
+function makeCheckHook(checkAsync, checkImport) {
+	if (!checkAsync && !checkImport) return null;
 	return (hook, args) => {
-		if (hook === 'function' || hook === 'generator_function' || hook === 'eval' || hook === 'run') {
-			const funcConstructor = internal.Function;
+		if (hook === 'function' || hook === 'generator_function' || hook === 'eval' || hook === 'run' ||
+			(!checkAsync && (hook === 'async_function' || hook === 'async_generator_function'))) {
 			if (hook === 'eval') {
 				const script = args[0];
 				args = [script];
 				if (typeof(script) !== 'string') return args;
 			} else {
 				// Next line throws on Symbol, this is the same behavior as function constructor calls
 				args = args.map(arg => `${arg}`);
 			}
-			if (args.findIndex(arg => /\basync\b/.test(arg)) === -1) return args;
-			const asyncMapped = args.map(arg => arg.replace(/async/g, 'a\\u0073ync'));
+			const hasAsync = checkAsync && args.findIndex(arg => /\basync\b/.test(arg)) !== -1;
+			const hasImport = checkImport && args.findIndex(arg => /\bimport\b/.test(arg)) !== -1;
+			if (!hasAsync && !hasImport) return args;
+			const mapped = args.map(arg => {
+				if (hasAsync) arg = arg.replace(/async/g, 'a\\u0073ync');
+				if (hasImport) arg = arg.replace(/import/g, 'i\\u006dport');
+				return arg;
+			});
 			try {
-				// Note: funcConstructor is a Sandbox object, however, asyncMapped are only strings.
-				funcConstructor(...asyncMapped);
+				tryCompile(mapped);
 			} catch (u) {
-				// u is a sandbox object
-				// Some random syntax error or error because of async.
+				// Some random syntax error or error because of async or import.
 
 				// First report real syntax errors
-				try {
-					// Note: funcConstructor is a Sandbox object, however, args are only strings.
-					funcConstructor(...args);
-				} catch (e) {
-					throw internal.Decontextify.value(e);
+				tryCompile(args);
+
+				if (hasAsync && hasImport) {
+					const mapped2 = args.map(arg => arg.replace(/async/g, 'a\\u0073ync'));
+					try {
+						tryCompile(mapped2);
+					} catch (e) {
+						throw new VMError('Async not available');
+					}
+					throw new VMError('Dynamic Import not supported');
+				}
+				if (hasAsync) {
+					// Then async error
+					throw new VMError('Async not available');
 				}
-				// Then async error
-				throw new VMError('Async not available');
+				throw new VMError('Dynamic Import not supported');
 			}
 			return args;
 		}
-		throw new VMError('Async not available');
+		if (checkAsync) throw new VMError('Async not available');
+		return args;
 	};
 }
 
 /**
  * Class VM.
@@ -706,11 +719,11 @@ class VM extends EventEmitter {
 		});
 
 		// Create the bridge between the host and the sandbox.
 		const _internal = CACHE.contextifyScript.runInContext(_context, DEFAULT_RUN_OPTIONS).call(_context, require, HOST);
 
-		const hook = fixAsync ? makeCheckAsync(_internal) : null;
+		const hook = makeCheckHook(fixAsync, true);
 
 		// Define the properties of this object.
 		// Use Object.defineProperties here to be able to
 		// hide and set properties write only.
 		Object.defineProperties(this, {
@@ -1170,11 +1183,26 @@ class NodeVM extends VM {
 		let dirname;
 		let resolvedFilename;
 		let script;
 
 		if (code instanceof VMScript) {
-			script = this.options.strict ? code._compileNodeVMStrict() : code._compileNodeVM();
+			if (this._hook) {
+				const prefix = this.options.strict ? STRICT_MODULE_PREFIX : MODULE_PREFIX;
+				const scriptCode = prefix + code.getCompiledCode() + MODULE_SUFFIX;
+				const changed = this._hook('run', [scriptCode])[0];
+				if (changed === scriptCode) {
+					script = this.options.strict ? code._compileNodeVMStrict() : code._compileNodeVM();
+				} else {
+					script = new vm.Script(changed, {
+						filename: code.filename,
+						displayErrors: false,
+						importModuleDynamically
+					});
+				}
+			} else {
+				script = this.options.strict ? code._compileNodeVMStrict() : code._compileNodeVM();
+			}
 			resolvedFilename = pa.resolve(code.filename);
 			dirname = pa.dirname(resolvedFilename);
 		} else {
 			const unresolvedFilename = filename || 'vm.js';
 			if (filename) {
@@ -1183,12 +1211,15 @@ class NodeVM extends VM {
 			} else {
 				resolvedFilename = null;
 				dirname = null;
 			}
 			const prefix = this.options.strict ? STRICT_MODULE_PREFIX : MODULE_PREFIX;
-			script = new vm.Script(prefix +
-					this._compiler(code, unresolvedFilename) + MODULE_SUFFIX, {
+			let scriptCode = prefix + this._compiler(code, unresolvedFilename) + MODULE_SUFFIX;
+			if (this._hook) {
+				scriptCode = this._hook('run', [scriptCode])[0];
+			}
+			script = new vm.Script(scriptCode, {
 				filename: unresolvedFilename,
 				displayErrors: false,
 				importModuleDynamically
 			});
 		}
diff --git a/lib/sandbox.js b/lib/sandbox.js
index 391f707..1a60eb5 100644
--- a/lib/sandbox.js
+++ b/lib/sandbox.js
@@ -66,12 +66,14 @@ return ((vm, host) => {
 					let contents = fs.readFileSync(filename, 'utf8');
 					contents = vm._compiler(contents, filename);
 
 					const code = host.STRICT_MODULE_PREFIX + contents + host.MODULE_SUFFIX;
 
+					const ccode = vm._hook('run', [code]);
+
 					// Precompile script
-					script = new Script(code, {
+					script = new Script(ccode, {
 						__proto__: null,
 						filename: filename || 'vm.js',
 						displayErrors: false,
 						importModuleDynamically
 					});
diff --git a/test/vm.js b/test/vm.js
index d4915b3..2c4738a 100644
--- a/test/vm.js
+++ b/test/vm.js
@@ -908,35 +908,27 @@ describe('VM', () => {
 			})()
 		`), /e is not a function/);
 	});
 
 	if (NODE_VERSION >= 10) {
-		it('Dynamic import attack', (done) => {
-			process.once('unhandledRejection', (reason) => {
-				assert.strictEqual(reason.message, 'process is not defined');
-				done();
-			});
+		it('Dynamic import attack', () => {
 
 			const vm2 = new VM();
 
-			vm2.run(`
-				(async () => {
-					try {
-						await import('oops!');
-					} catch (ex) {
-						// ex is an instance of NodeError which is not proxied;
-						const process = ex.constructor.constructor('return process')();
-						const require = process.mainModule.require;
-						const child_process = require('child_process');
-						const output = child_process.execSync('id');
-						process.stdout.write(output);
-					}
-				})();
-			`);
+			assert.throws(()=>vm2.run(`
+				const process = import('oops!').constructor.constructor('return process')();
+			`), /VMError: Dynamic Import not supported/);
 		});
 	}
 
+	it('Error.prepareStackTrace attack', () => {
+		const vm2 = new VM();
+		const sst = vm2.run('Error.prepareStackTrace = (e,sst)=>sst;const sst = new Error().stack;Error.prepareStackTrace = undefined;sst');
+		assert.strictEqual(vm2.run('sst=>Object.getPrototypeOf(sst)')(sst), vm2.run('Array.prototype'));
+		assert.throws(()=>vm2.run('sst=>sst[0].getThis().constructor.constructor')(sst), /TypeError: Cannot read property 'constructor' of undefined/);
+	});
+
 	after(() => {
 		vm = null;
 	});
 });
 

Vulnerable code: function curry(func) {
const FunctionBind = curry(Function.prototype.bind);
const ObjectHasInstance = curry(local.Object[Symbol.hasInstance]);
/**
 * Creates the hook to check for the use of async.
 * 
 * @private
 * @param {*} internal - The internal vm object.
 * @return {*} The hook function
 */
function makeCheckAsync(internal) {
		if (hook === 'function' || hook === 'generator_function' || hook === 'eval' || hook === 'run') {
			const funcConstructor = internal.Function;
			if (args.findIndex(arg => /\basync\b/.test(arg)) === -1) return args;
			const asyncMapped = args.map(arg => arg.replace(/async/g, 'a\\u0073ync'));
				// Note: funcConstructor is a Sandbox object, however, asyncMapped are only strings.
				funcConstructor(...asyncMapped);
				// u is a sandbox object
				// Some random syntax error or error because of async.
				try {
					// Note: funcConstructor is a Sandbox object, however, args are only strings.
					funcConstructor(...args);
				} catch (e) {
					throw internal.Decontextify.value(e);
				// Then async error
				throw new VMError('Async not available');
		throw new VMError('Async not available');
		const hook = fixAsync ? makeCheckAsync(_internal) : null;
			script = this.options.strict ? code._compileNodeVMStrict() : code._compileNodeVM();
			script = new vm.Script(prefix +
					this._compiler(code, unresolvedFilename) + MODULE_SUFFIX, {
					script = new Script(code, {
		it('Dynamic import attack', (done) => {
			process.once('unhandledRejection', (reason) => {
				assert.strictEqual(reason.message, 'process is not defined');
				done();
			});
			vm2.run(`
				(async () => {
					try {
						await import('oops!');
					} catch (ex) {
						// ex is an instance of NodeError which is not proxied;
						const process = ex.constructor.constructor('return process')();
						const require = process.mainModule.require;
						const child_process = require('child_process');
						const output = child_process.execSync('id');
						process.stdout.write(output);
					}
				})();
			`);
Fixed code: function uncurryThis(func) {
const FunctionBind = uncurryThis(Function.prototype.bind);
const ObjectHasInstance = uncurryThis(local.Object[Symbol.hasInstance]);
Contextify.connect(host.Function.prototype.bind, Function.prototype.bind);

const oldPrepareStackTraceDesc = Reflect.getOwnPropertyDescriptor(Error, 'prepareStackTrace');

let currentPrepareStackTrace = Error.prepareStackTrace;
const wrappedPrepareStackTrace = new host.WeakMap();
if (typeof currentPrepareStackTrace === 'function') {
	wrappedPrepareStackTrace.set(currentPrepareStackTrace, currentPrepareStackTrace);
}

let OriginalCallSite;
Error.prepareStackTrace = (e, sst) => {
	OriginalCallSite = sst[0].constructor;
};
new Error().stack;
if (typeof OriginalCallSite === 'function') {
	Error.prepareStackTrace = undefined;

	function makeCallSiteGetters(list) {
		const callSiteGetters = [];
		for (let i=0; i<list.length; i++) {
			const name = list[i];
			const func = OriginalCallSite.prototype[name];
			callSiteGetters[i] = {__proto__: null,
				name,
				propName: '_' + name,
				func: (thiz) => {
					return local.Reflect.apply(func, thiz, []);
				}
			};
		}
		return callSiteGetters;
	}

	function applyCallSiteGetters(callSite, getters) {
		const properties = {__proto__: null};
		for (let i=0; i<getters.length; i++) {
			const getter = getters[i];
			properties[getter.propName] = {
				__proto__: null,
				value: getter.func(callSite)
			};
		}
		return properties;
	}

	const callSiteGetters = makeCallSiteGetters([
		'getTypeName',
		'getFunctionName',
		'getMethodName',
		'getFileName',
		'getLineNumber',
		'getColumnNumber',
		'getEvalOrigin',
		'isToplevel',
		'isEval',
		'isNative',
		'isConstructor',
		'isAsync',
		'isPromiseAll',
		'getPromiseIndex'
	]);

	class CallSite {
		constructor(callSite) {
			Object.defineProperties(this, applyCallSiteGetters(callSite, callSiteGetters));
		}
		getThis() {return undefined;}
		getFunction() {return undefined;}
		toString() {return 'CallSite {}';}
	}

	(function setupCallSite() {
		for (let i=0; i<callSiteGetters.length; i++) {
			const name = callSiteGetters[i].name;
			const propertyName = callSiteGetters[i].propName;
			const func = {func() {
				return this[propertyName];
			}}.func;
			const nameProp = Object.getOwnPropertyDescriptor(func, 'name');
			nameProp.value = name;
			Object.defineProperty(func, 'name', nameProp);
			const funcProp = Object.getOwnPropertyDescriptor(OriginalCallSite.prototype, name);
			funcProp.value = func;
			Object.defineProperty(CallSite.prototype, name, funcProp);
		}
	})();

	Object.defineProperty(Error, 'prepareStackTrace', {
		configurable: false,
		enumerable: false,
		get() {
			return currentPrepareStackTrace;
		},
		set(value) {
			if (typeof(value) !== 'function') {
				currentPrepareStackTrace = value;
				return;
			}
			const wrapped = wrappedPrepareStackTrace.get(value);
			if (wrapped) {
				currentPrepareStackTrace = wrapped;
				return;
			}
			const newWrapped = (error, sst) => {
				if (host.Array.isArray(sst)) {
					for (let i=0; i<sst.length; i++) {
						const cs = sst[i];
						if (typeof cs === 'object' && local.Reflect.getPrototypeOf(cs) === OriginalCallSite.prototype) {
							sst[i] = new CallSite(cs);
						}
					}
				}
				return value(error, sst);
			};
			wrappedPrepareStackTrace.set(value, newWrapped);
			wrappedPrepareStackTrace.set(newWrapped, newWrapped);
			currentPrepareStackTrace = newWrapped;
		}
	});
} else if (oldPrepareStackTraceDesc) {
	Reflect.defineProperty(Error, 'prepareStackTrace', oldPrepareStackTraceDesc);
} else {
	Reflect.deleteProperty(Error, 'prepareStackTrace');
}
function tryCompile(args) {
	const code = args[args.length - 1];
	const params = args.slice(0, -1);
	vm.compileFunction(code, params);
}

function makeCheckHook(checkAsync, checkImport) {
	if (!checkAsync && !checkImport) return null;
		if (hook === 'function' || hook === 'generator_function' || hook === 'eval' || hook === 'run' ||
			(!checkAsync && (hook === 'async_function' || hook === 'async_generator_function'))) {
			const hasAsync = checkAsync && args.findIndex(arg => /\basync\b/.test(arg)) !== -1;
			const hasImport = checkImport && args.findIndex(arg => /\bimport\b/.test(arg)) !== -1;
			if (!hasAsync && !hasImport) return args;
			const mapped = args.map(arg => {
				if (hasAsync) arg = arg.replace(/async/g, 'a\\u0073ync');
				if (hasImport) arg = arg.replace(/import/g, 'i\\u006dport');
				return arg;
			});
				tryCompile(mapped);
				// Some random syntax error or error because of async or import.
				tryCompile(args);

				if (hasAsync && hasImport) {
					const mapped2 = args.map(arg => arg.replace(/async/g, 'a\\u0073ync'));
					try {
						tryCompile(mapped2);
					} catch (e) {
						throw new VMError('Async not available');
					}
					throw new VMError('Dynamic Import not supported');
				}
				if (hasAsync) {
					// Then async error
					throw new VMError('Async not available');
				throw new VMError('Dynamic Import not supported');
		if (checkAsync) throw new VMError('Async not available');
		return args;
		const hook = makeCheckHook(fixAsync, true);
			if (this._hook) {
				const prefix = this.options.strict ? STRICT_MODULE_PREFIX : MODULE_PREFIX;
				const scriptCode = prefix + code.getCompiledCode() + MODULE_SUFFIX;
				const changed = this._hook('run', [scriptCode])[0];
				if (changed === scriptCode) {
					script = this.options.strict ? code._compileNodeVMStrict() : code._compileNodeVM();
				} else {
					script = new vm.Script(changed, {
						filename: code.filename,
						displayErrors: false,
						importModuleDynamically
					});
				}
			} else {
				script = this.options.strict ? code._compileNodeVMStrict() : code._compileNodeVM();
			}
			let scriptCode = prefix + this._compiler(code, unresolvedFilename) + MODULE_SUFFIX;
			if (this._hook) {
				scriptCode = this._hook('run', [scriptCode])[0];
			}
			script = new vm.Script(scriptCode, {
					const ccode = vm._hook('run', [code]);

					script = new Script(ccode, {
		it('Dynamic import attack', () => {
			assert.throws(()=>vm2.run(`
				const process = import('oops!').constructor.constructor('return process')();
			`), /VMError: Dynamic Import not supported/);
	it('Error.prepareStackTrace attack', () => {
		const vm2 = new VM();
		const sst = vm2.run('Error.prepareStackTrace = (e,sst)=>sst;const sst = new Error().stack;Error.prepareStackTrace = undefined;sst');
		assert.strictEqual(vm2.run('sst=>Object.getPrototypeOf(sst)')(sst), vm2.run('Array.prototype'));
		assert.throws(()=>vm2.run('sst=>sst[0].getThis().constructor.constructor')(sst), /TypeError: Cannot read property 'constructor' of undefined/);
	});