CVE-2021-21413 (Markdown, published 2021-03-30T23:15Z)
Description: value: isolated-vm is a library for nodejs which gives you access to v8's Isolate interface. Versions of isolated-vm before v4.0.0 have API pitfalls which may make it easy for implementers to expose supposed secure isolates to the permissions of the main nodejs isolate. Reference objects allow access to the underlying reference's full prototype chain. In an environment where the implementer has exposed a Reference instance to an attacker they would be able to use it to acquire a Reference to the nodejs context's Function object. Similar application-specific attacks could be possible by modifying the local prototype of other API objects. Access to NativeModule objects could allow an attacker to load and run native code from anywhere on the filesystem. If combined with, for example, a file upload API this would allow for arbitrary code execution. This is addressed in v4.0.0 through a series of related changes.
Severity: MEDIUM (CVSS v3 base score: 9.6)
Weakness: CWE-913 \u2014 Improper Control of Dynamically-Managed Code Resources
The product does not properly restrict reading from or writing to dynamically-managed code resources such as variables, objects, classes, attributes, functions, or executable instructions or statements.
Repository: https://github.com/laverdet/isolated-vm
Files changed:
CHANGELOG.md
README.md
isolated-vm.d.ts
src/isolate/strings.h
src/module/reference_handle.cc
src/module/reference_handle.h
tests/reference.js
Commit message: Don't invoke accessors or proxies via Reference functions


Vulnerable code:
- When possible, `reference.get()` will return a function proxy instead of a `Reference`.
  * `inheritUnsafe` *[boolean]* - If enabled then the `get` family of functions will follow the
* **return** `true` or `false`
* **return** `true` or `false`

Returns a boolean indicating whether or not this operation succeeded. I'm actually not really sure
when `false` would be returned, I'm just giving you the result back straight from the v8 API.
		constructor(value: T, options?: { inheritUnsafe?: boolean });
		 delete(property: keyof T): Promise<boolean>;
		 deleteSync(property: keyof T): boolean;
			property: Key, options?: Options): ResultTypeAsync<Options & AsReference, T[Key]>;
			property: Key, options?: Options): ResultTypeSync<Options & AsReference, T[Key]>;
		 *
		 * @return {boolean} Indicating whether or not this operation succeeded. I'm actually not really
		 * sure when false would be returned, I'm just giving you the result back straight from the v8
		 * API.
			property: Key, value: ArgumentType<Options, T[Key]>, options?: Options): Promise<boolean>;
			property: Key, value: ArgumentType<Options, T[Key]>, options?: Options): boolean;
		): ResultTypeBidirectionalAsync<Options & ApplyAsReference, ApplyResult<T>>;
			arguments?: ArgumentsTypeBidirectional<Options & ApplyAsReference, ApplyArguments<T>>,
		): ResultTypeBidirectionalSync<Options & ApplyAsReference, ApplyResult<T>>;
		): ResultTypeBidirectionalSync<Options & ApplyAsReference, ApplyResult<T>>;
		String inheritUnsafe{"inheritUnsafe"};
	auto inherit = ReadOption<bool>(options, StringTable::Get().inheritUnsafe, false);
 * Get a property from this reference, returned as another reference
class GetRunner : public ThreePhaseTask {
		GetRunner(
			const ReferenceHandle& that,
			Local<Value> key_handle,
			MaybeLocal<Object> maybe_options,
			bool inherit
		) :
				context{that.context},
				reference{that.reference},
				options{maybe_options, inherit ?
					TransferOptions::Type::DeepReference : TransferOptions::Type::Reference},
				inherit{inherit} {
			that.CheckDisposed();
			key = ExternalCopy::CopyIfPrimitive(key_handle);
			if (!key) {
			Local<Context> context_handle = Deref(context);
			Context::Scope context_scope{context_handle};
			Local<Value> key_inner = key->CopyInto();
			Local<Object> object = Local<Object>::Cast(Deref(reference));
			bool allow = [&]() {
				if (!inherit) {
					if (key_inner->IsName()) {
						return Unmaybe(object->HasRealNamedProperty(context_handle, key_inner.As<Name>()));
					} else if (key_inner->IsNumber()) {
						return Unmaybe(object->HasRealIndexedProperty(context_handle, HandleCast<uint32_t>(key_inner)));
					} else {
						return false;
					}
				return true;
			}();
			Local<Value> value = allow ?
				Unmaybe(object->Get(context_handle, key_inner)) :
				Undefined(Isolate::GetCurrent()).As<Value>();
			ret = TransferOut(value, options);
		unique_ptr<ExternalCopy> key;
		RemoteHandle<Context> context;
		RemoteHandle<Value> reference;
	return ThreePhaseTask::Run<async, GetRunner>(*isolate, *this, key_handle, maybe_options, inherit);
class DeleteRunner : public ThreePhaseTask {
				key{ExternalCopy::CopyIfPrimitive(key_handle)},
				context{that.context},
				reference{that.reference} {
			that.CheckDisposed();
			if (!key) {
				throw RuntimeTypeError("Invalid `key`");
			}
		}
			Local<Context> context_handle = Deref(context);
			Context::Scope context_scope{context_handle};
			Local<Object> object = Local<Object>::Cast(Deref(reference));
			result = Unmaybe(object->Delete(context_handle, key->CopyInto()));
			return Boolean::New(Isolate::GetCurrent(), result);

	private:
		unique_ptr<ExternalCopy> key;
		RemoteHandle<Context> context;
		RemoteHandle<Value> reference;
		bool result = false;
class SetRunner : public ThreePhaseTask {
				key{ExternalCopy::CopyIfPrimitive(key_handle)},
				val{TransferOut(val_handle, TransferOptions{maybe_options})},
				context{that.context},
				reference{that.reference} {
			that.CheckDisposed();
			if (!key) {
				throw RuntimeTypeError("Invalid `key`");
			}
		}
			Local<Context> context_handle = Deref(context);
			Context::Scope context_scope{context_handle};
			Local<Value> key_inner = key->CopyInto();
			Local<Object> object = Local<Object>::Cast(Deref(reference));
			Unmaybe(object->Delete(context_handle, key_inner));
			Local<Value> val_inner = val->TransferIn();
			did_set = Unmaybe(object->Set(context_handle, key_inner, val_inner));
			return Boolean::New(Isolate::GetCurrent(), did_set);
		unique_ptr<ExternalCopy> key;
		RemoteHandle<Context> context;
		RemoteHandle<Value> reference;
		bool did_set = false;
	friend class DeleteRunner;
	friend class SetRunner;
const { strictEqual } = require('assert');
const foo = { foo: 1 };
const bar = Object.create(foo);
bar.bar = 2;
const etc = Object.create(bar);
etc.etc = bar;
	const ref = new ivm.Reference(etc);
	strictEqual(ref.getSync('bar'), undefined);
	strictEqual(ref.getSync('etc').getSync('foo'), undefined);
	strictEqual(ref.getSync('etc').getSync('bar'), 2);
	const ref = new ivm.Reference(etc, { inheritUnsafe: true });
	strictEqual(ref.getSync('bar'), 2);
	strictEqual(ref.getSync('etc').getSync('foo'), 1);
	strictEqual(ref.getSync('etc').getSync('bar'), 2);

Fixed code:
- When possible, `reference.get()` will return a function delegate instead of a `Reference`.
- Proxies and accessors are no longer tolerated via `reference.get`, and related functions.
  * `unsafeInherit` *[boolean]* - If enabled then the `get` family of functions will follow the
If the object is a proxy, or if the property is a getter, this method will throw.

		| Callback<any>
		| ((...args: any[]) => any)
		constructor(value: T, options?: { unsafeInherit?: boolean });
		 delete(property: keyof T): Promise<void>;
		 deleteSync(property: keyof T): void;
		 *
		 * If the object is a proxy, or if the property is a getter, this method will throw.
			property: Key, options?: Options): ResultTypeAsync<Options & FallbackReference, T[Key]>;
			property: Key, options?: Options): ResultTypeSync<Options & FallbackReference, T[Key]>;
			property: Key, value: ArgumentType<Options, T[Key]>, options?: Options): Promise<void>;
			property: Key, value: ArgumentType<Options, T[Key]>, options?: Options): void;
		): ResultTypeBidirectionalAsync<Options & FallbackReference, ApplyResult<T>>;
			arguments?: ArgumentsTypeBidirectional<Options & FallbackReference, ApplyArguments<T>>,
		): ResultTypeBidirectionalSync<Options & FallbackReference, ApplyResult<T>>;
		): ResultTypeBidirectionalSync<Options & FallbackReference, ApplyResult<T>>;
	type FallbackReference = { _reference: true };
		Options extends FallbackReference ? Reference<Result> :
		String unsafeInherit{"unsafeInherit"};
	auto inherit = ReadOption<bool>(options, StringTable::Get().unsafeInherit, false);
 * Base class for get, set, and delete runners
class AccessorRunner : public ThreePhaseTask {
		AccessorRunner(ReferenceHandle& target, Local<Value> key_handle) :
		context{target.context},
		target{target.reference},
		key{ExternalCopy::CopyIfPrimitive(key_handle)} {
			target.CheckDisposed();
			if (!key || (!key_handle->IsName() && !key_handle->IsUint32())) {
			} else if (target.type_of != decltype(target.type_of)::Object) {
				throw RuntimeTypeError("Reference is not an object");
			}
		}

	protected:
		auto GetTargetAndAlsoCheckForProxy() -> Local<Object> {
			auto object = Local<Object>::Cast(Deref(target));
			if (HasProxy(object)) {
				throw RuntimeTypeError("Object is or has proxy");
			}
			return object;
		}

		auto GetKey(Local<Context> context) -> Local<Name> {
			auto key_inner = key->CopyInto();
			return (key_inner->IsString() || key_inner->IsSymbol()) ?
				key_inner.As<Name>() : Unmaybe(key_inner->ToString(context)).As<Name>();
		}

		RemoteHandle<Context> context;

	private:
		static auto HasProxy(Local<Object> object) -> bool {
			if (object->IsProxy()) {
				return true;
			} else {
				auto proto = object->GetPrototype();
				if (proto->IsNullOrUndefined()) {
					return false;
				} else {
					return HasProxy(proto.As<Object>());
				}
		RemoteHandle<Value> target;
		unique_ptr<ExternalCopy> key;
};

/**
 * Get a property from this reference, returned as another reference
 */
class GetRunner final : public AccessorRunner {
	public:
		GetRunner(ReferenceHandle& target, Local<Value> key_handle, MaybeLocal<Object> maybe_options) :
		AccessorRunner{target, key_handle},
		options{maybe_options, target.inherit ?
			TransferOptions::Type::DeepReference : TransferOptions::Type::Reference},
		inherit{target.inherit} {}

			// Setup
			auto* isolate = Isolate::GetCurrent();
			auto context = Deref(this->context);
			Context::Scope context_scope{context};
			auto name = GetKey(context);
			auto object = GetTargetAndAlsoCheckForProxy();

			// Get property
			ret = TransferOut([&]() {
				if (inherit) {
					// To avoid accessors I guess we have to walk the prototype chain ourselves
					auto target = object;
					do {
						if (Unmaybe(target->HasOwnProperty(context, name))) {
							if (Unmaybe(target->HasRealNamedCallbackProperty(context, name))) {
								throw RuntimeTypeError("Property is getter");
							}
							return Unmaybe(target->GetRealNamedProperty(context, name));
						}
						auto next = target->GetPrototype();
						if (next->IsNullOrUndefined()) {
							return Undefined(isolate).As<Value>();
						}
						target = next.As<Object>();
					} while (true);
				} else if (!Unmaybe(object->HasOwnProperty(context, name))) {
					return Undefined(isolate).As<Value>();
				} else if (Unmaybe(object->HasRealNamedCallbackProperty(context, name))) {
					throw RuntimeTypeError("Property is getter");
				} else {
					return Unmaybe(object->Get(context, name));
			}(), options);
	return ThreePhaseTask::Run<async, GetRunner>(*isolate, *this, key_handle, maybe_options);
class DeleteRunner final : public AccessorRunner {
		AccessorRunner{that, key_handle} {}
			auto context = Deref(this->context);
			Context::Scope context_scope{context};
			auto object = GetTargetAndAlsoCheckForProxy();
			if (!Unmaybe(object->Delete(context, GetKey(context)))) {
				throw RuntimeTypeError("Delete failed");
			}
			return Undefined(Isolate::GetCurrent());
class SetRunner final : public AccessorRunner {
		AccessorRunner{that, key_handle},
		val{TransferOut(val_handle, TransferOptions{maybe_options})} {}
			auto context = Deref(this->context);
			Context::Scope context_scope{context};
			auto name = GetKey(context);
			auto object = GetTargetAndAlsoCheckForProxy();
			Unmaybe(object->Delete(context, name));
			auto val_inner = val->TransferIn();
			if (!Unmaybe(object->CreateDataProperty(context, GetKey(context), val_inner))) {
				throw RuntimeTypeError("Set failed");
			}
			return Undefined(Isolate::GetCurrent());
	friend class AccessorRunner;
	friend class AccessorRunner;
'use strict';
const { strictEqual, throws } = require('assert');
let trap = false;
	// Set up inheritance
	const foo = { foo: 1 };
	const bar = Object.create(foo);
	bar.bar = 2;
	const etc = Object.create(bar);
	etc.etc = bar;

	{
		// Test without inheritance
		const ref = new ivm.Reference(etc);
		strictEqual(ref.getSync('bar'), undefined);
		strictEqual(ref.getSync('etc').getSync('foo'), undefined);
		strictEqual(ref.getSync('etc').getSync('bar'), 2);
		ref.setSync('prop', 1);
		strictEqual(ref.getSync('prop'), 1);
		ref.deleteSync('prop');
		strictEqual(ref.getSync('prop'), undefined);
	}

	{
		// Test with inheritance
		const ref = new ivm.Reference(etc, { unsafeInherit: true });
		strictEqual(ref.getSync('bar'), 2);
		strictEqual(ref.getSync('etc').getSync('foo'), 1);
		strictEqual(ref.getSync('etc').getSync('bar'), 2);
		ref.getSync('etc').setSync('prop', 1);
		ref.setSync('prop', 2);
		strictEqual(ref.getSync('prop'), 2);
		ref.deleteSync('prop');
		strictEqual(ref.getSync('prop'), 1);
	}
}

{
	// Set up getter / setters
	let setter = 0;
	const foo = {
		get str() { trap = true; return 'got' },
		set str(_) { trap = true },
	};
	Object.defineProperty(foo, 0, {
		get() { trap = true; return 'got' },
		set() { trap = true },
	});

	{
		// Test plain accessors
		const ref = new ivm.Reference(foo);
		throws(() => ref.getSync('str'));
		throws(() => ref.getSync(0));
		throws(() => ref.setSync(0, undefined));
	}

	{
		// Test accessors + inheritance
		const ref = new ivm.Reference(Object.create(foo), { unsafeInherit: true });
		throws(() => ref.getSync('str'));
		ref.setSync('str', undefined);
		throws(() => ref.getSync(0));
	}
}

{
	// Set up evil proxy
	const val = { prop: 1 };
	const prox = new Proxy(val, {
		get() { trap = true },
    set() { trap = true },
    getOwnPropertyDescriptor() { trap = true },
    getPrototypeOf() { trap = true },
    has() { trap = true },
    ownKeys() { trap = true },
	});
	const inherited = Object.create(prox);

	{
		// Test proxy
		const ref = new ivm.Reference(prox);
		throws(() => ref.getSync('prop'));
	}

	{
		// Test inherited proxy
		const ref = new ivm.Reference(inherited);
		throws(() => ref.getSync('prop'),);
	}

	{
		// Test deep inherited proxy
		const ref = new ivm.Reference(inherited, { unsafeInherit: true });
		throws(() => ref.getSync('prop'));
	}

	// Test Array (numeric indices)
	const val = [ 1, 2, 3 ];
	Object.defineProperty(val, 1, { get() { trap = true; return 'got' }});
	const ref = new ivm.Reference(val);
	strictEqual(ref.getSync(0), 1);
	throws(() => ref.getSync(1));
{
	// Test ArrayBuffer (numeric indices)
	const val = new Uint8Array([ 1, 2, 3 ]);
	const ref = new ivm.Reference(val);
	strictEqual(ref.getSync(0), 1);
}

if (trap) {
	console.log('fail');
}