CVE-2023-23557 (published 2023-05-18T22:15Z)

An error in Hermes' algorithm for copying objects properties prior to commit a00d237346894c6067a594983be6634f4168c9ad could be used by a malicious attacker to execute arbitrary code via type confusion. Note that this is only exploitable in cases where Hermes is used to execute untrusted JavaScript. Hence, most React Native applications are not affected.

CVSS v3 base score: 9.8, CRITICAL (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: CWE-843
cwe_name: Access of Resource Using Incompatible Type ('Type Confusion')
description: The product allocates or initializes a resource such as a pointer, object, or variable using one type, but it later accesses that resource using a type that is incompatible with the original type.
extended_description: 
            When the product accesses the resource using an incompatible type, this could trigger logical errors because the resource does not have expected properties. In languages without memory safety, such as C and C++, type confusion can lead to out-of-bounds memory access.
            While this weakness is frequently associated with unions when parsing data with many different embedded object types in C, it can be present in any application that can interpret the same variable or memory location in multiple ways.
            This weakness is not unique to C and C++. For example, errors in PHP applications can be triggered by providing array parameters when scalars are expected, or vice versa. Languages such as Perl, which perform automatic conversion of a variable of one type when it is accessed as if it were another type, can also contain these issues.
         
         

Fix information:
repo_url: https://github.com/facebook/hermes
commit_details: repo_url: https://github.com/facebook/hermes
author: Michael Anthony Leon
author_date: 2023-01-10 11:43:42-08:00
msg: Use copyDataProperties slow path for objects with accessors

Summary:
Fix a bug present in `hermesBuiltinCopyDataProperties`.
`hermesBuiltinCopyDataProperties` is meant to copy an object. It does
this by iterating over all the properties in the source object, and
then writing the key-value pair into a new object which is returned.The
iteration is done using `JSObject::forEachOwnPropertyWhile`, providing
a callback to process each property. It will take the property id, look
it up in the source object using `getNamedPropertyValue_RJS`, then take
that resulting value and write it into the target. However, that API
explicitly states the following:

> Obviously the callbacks shouldn't be  doing naughty things like modifying
the property map or creating new hidden classes (even implicitly)

But, `getNamedPropertyValue_RJS` can
perform arbitrary JS execution if it is a getter, including
adding/deleting new properties, which would modify the object's
HiddenClass, thus violating `JSObject::forEachOwnPropertyWhile`'s
precondition.Therefore, the fix is to use the correct, but slower path,
whenever there is an accessor on the object. This code path is more 1:1
to the spec. At the beginning, it will simply find all the properties
of the source object using `JSObject::getOwnPropertyKeys` and then
iterate on those, rather than use `JSObject::forEachOwnPropertyWhile`.
This way, we are allowed to manipulate the source object however we
wish, which is necessary in order to call `getNamedPropertyValue_RJS`.

Reviewed By: neildhar

Differential Revision: D41701871

fbshipit-source-id: c4be179f57d52827f12ca26b1ab8d17cdccd7447
repository: repo_url: https://github.com/facebook/hermes
repo_name: facebook/hermes
description: A JavaScript engine optimized for running React Native.
repo_language: C++
owner: facebook
file_changes: filename: HermesBuiltin.cpp
change_type: ModificationType.MODIFY
diff: @@ -251,12 +251,8 @@ CallResult<HermesValue> copyDataPropertiesSlowPath_RJS(
     Handle<JSObject> target,
     Handle<JSObject> from,
     Handle<JSObject> excludedItems) {
-  assert(
-      from->isProxyObject() &&
-      "copyDataPropertiesSlowPath_RJS is only for Proxy");
-
   // 5. Let keys be ? from.[[OwnPropertyKeys]]().
-  auto cr = JSProxy::getOwnPropertyKeys(
+  auto cr = JSObject::getOwnPropertyKeys(
       from,
       runtime,
       OwnKeysFlags()
@@ -310,15 +306,17 @@ CallResult<HermesValue> copyDataPropertiesSlowPath_RJS(
 
     //   i. Let desc be ? from.[[GetOwnProperty]](nextKey).
     ComputedPropertyDescriptor desc;
-    CallResult<bool> crb =
-        JSProxy::getOwnProperty(from, runtime, nextKeyHandle, desc, nullptr);
+    CallResult<bool> crb = JSObject::getOwnComputedDescriptor(
+        from, runtime, nextKeyHandle, tmpSymbolStorage, desc);
     if (LLVM_UNLIKELY(crb == ExecutionStatus::EXCEPTION))
       return ExecutionStatus::EXCEPTION;
     //   ii. If desc is not undefined and desc.[[Enumerable]] is true, then
-    if (*crb && desc.flags.enumerable) {
+    // TODO(T141997867), move this special case behavior for host objects to
+    // getOwnComputedDescriptor.
+    if ((*crb && desc.flags.enumerable) || from->isHostObject()) {
       //     1. Let propValue be ? Get(from, nextKey).
       CallResult<PseudoHandle<>> crv =
-          JSProxy::getComputed(from, runtime, nextKeyHandle, from);
+          JSObject::getComputed_RJS(from, runtime, nextKeyHandle);
       if (LLVM_UNLIKELY(crv == ExecutionStatus::EXCEPTION))
         return ExecutionStatus::EXCEPTION;
       propValueHandle = std::move(*crv);
@@ -380,7 +378,13 @@ hermesBuiltinCopyDataProperties(void *, Runtime &runtime, NativeArgs args) {
       (!excludedItems || !excludedItems->isProxyObject()) &&
       "excludedItems internal List is a Proxy");
 
-  if (source->isProxyObject()) {
+  // We cannot use the fast path if the object is a proxy, host object, or when
+  // there could potentially be an accessor defined on the object. This is
+  // because in order to use JSObject::forEachOwnPropertyWhile, we must not
+  // modify the underlying property map or hidden class. However, if we have an
+  // accessor, we cannot guarantee that condition, so we use the slow path.
+  if (source->isProxyObject() || source->isHostObject() ||
+      source->getClass(runtime)->getMayHaveAccessor()) {
     return copyDataPropertiesSlowPath_RJS(
         runtime, target, source, excludedItems);
   }
@@ -458,12 +462,9 @@ hermesBuiltinCopyDataProperties(void *, Runtime &runtime, NativeArgs args) {
             return true;
         }
 
-        auto cr =
-            JSObject::getNamedPropertyValue_RJS(source, runtime, source, desc);
-        if (LLVM_UNLIKELY(cr == ExecutionStatus::EXCEPTION))
-          return false;
-
-        valueHandle = std::move(*cr);
+        SmallHermesValue shv =
+            JSObject::getNamedSlotValueUnsafe(*source, runtime, desc);
+        valueHandle = runtime.makeHandle(shv.unboxToHV(runtime));
 
         // sym can be an index-like property, so we have to bypass the assert in
         // defineOwnPropertyInternal.

programming_language: C++
filename: regress-spread-operator.js
change_type: ModificationType.ADD
diff: @@ -0,0 +1,32 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+// RUN: %hermes -O %s | %FileCheck %s
+
+// Check that the spread operator behaves correctly when modifying the
+// underlying hidden class while iterating over an object's properties in order
+// to copy it.
+
+var obj = {
+  get foo() {
+    this.tutu = 'hi';
+    delete obj.bar;
+    obj.tutu = 'lol';
+  },
+   bar: 'bar'
+};
+print({ ...obj }.tutu)
+// CHECK: undefined
+
+var obj2 = {
+  get foo() {
+    obj2.bar = 'foo'
+  },
+  bar: 'bar'
+};
+print({ ...obj2 }.bar)
+// CHECK: foo

programming_language: JavaScript
filename: APITest.cpp
change_type: ModificationType.MODIFY
diff: @@ -493,6 +493,30 @@ JSON.stringify(JSON.parse(out).callstack.map(x => x.SourceLocation));
   EXPECT_EQ(callstack, expected);
 }
 
+TEST_F(HermesRuntimeTest, SpreadHostObjectWithOwnProperties) {
+  class HostObjectWithPropertyNames : public HostObject {
+    std::vector<PropNameID> getPropertyNames(Runtime &rt) override {
+      return PropNameID::names(rt, "prop1", "1", "2", "prop2", "3");
+    }
+    Value get(Runtime &runtime, const PropNameID &name) override {
+      return Value();
+    }
+  };
+
+  Object ho = Object::createFromHostObject(
+      *rt, std::make_shared<HostObjectWithPropertyNames>());
+  rt->global().setProperty(*rt, "ho", ho);
+
+  auto res = eval(R"###(
+var spreaded = {...ho};
+var props = Object.getOwnPropertyNames(spreaded);
+props.toString();
+)###")
+                 .getString(*rt)
+                 .utf8(*rt);
+  EXPECT_EQ(res, "1,2,3,prop1,prop2");
+}
+
 TEST_F(HermesRuntimeTest, HostObjectWithOwnProperties) {
   class HostObjectWithPropertyNames : public HostObject {
     std::vector<PropNameID> getPropertyNames(Runtime &rt) override {

programming_language: C++