CVE-2021-46463: Access of Resource Using Incompatible Type ('Type Confusion') (CWE-843)
value: njs through 0.7.1, used in NGINX, was discovered to contain a control flow hijack caused by a Type Confusion vulnerability in njs_promise_perform_then().
CWE 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.
CVSS v3 base score: 9.8
Published: 2022-02-14T22:15Z
Repository: https://github.com/nginx/njs
Language: JavaScript
Files changed: src/njs_promise.c, src/njs_vmcode.c, test/js/promise_prototype_reject_type_confusion.t.js, test/js/promise_prototype_then_type_confusion.t.js
Commit date: 2022-01-19 13:12:09 +0000
Commit message: Fixed type confusion bug while resolving promises.

Previously, the internal function njs_promise_perform_then() which
implements PerformPromiseThen() expects its first argument to always be
a promise instance.  This assertion might be invalid because the
functions corresponding to Promise.prototype.then() and
Promise.resolve() incorrectly verified their arguments.

Specifically, the functions recognized their first argument as promise
if it was an object which was an Promise or had Promise object in its
prototype chain.  The later condition is not correct because internal
slots are not inherited according to the spec.

This closes #447 issue in Github.

Diff: diff --git a/src/njs_promise.c b/src/njs_promise.c
index 45ea4921..52ae5b57 100644
--- a/src/njs_promise.c
+++ b/src/njs_promise.c
@@ -769,29 +769,23 @@ njs_promise_object_resolve(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
 njs_promise_t *
 njs_promise_resolve(njs_vm_t *vm, njs_value_t *constructor, njs_value_t *x)
 {
     njs_int_t                 ret;
     njs_value_t               value;
-    njs_object_t              *object;
     njs_promise_capability_t  *capability;
 
     static const njs_value_t  string_constructor = njs_string("constructor");
 
-    if (njs_is_object(x)) {
-        object = njs_object_proto_lookup(njs_object(x), NJS_PROMISE,
-                                         njs_object_t);
-
-        if (object != NULL) {
-            ret = njs_value_property(vm, x, njs_value_arg(&string_constructor),
-                                     &value);
-            if (njs_slow_path(ret == NJS_ERROR)) {
-                return NULL;
-            }
+    if (njs_is_promise(x)) {
+        ret = njs_value_property(vm, x, njs_value_arg(&string_constructor),
+                                 &value);
+        if (njs_slow_path(ret == NJS_ERROR)) {
+            return NULL;
+        }
 
-            if (njs_values_same(&value, constructor)) {
-                return njs_promise(x);
-            }
+        if (njs_values_same(&value, constructor)) {
+            return njs_promise(x);
         }
     }
 
     capability = njs_promise_new_capability(vm, constructor);
     if (njs_slow_path(capability == NULL)) {
@@ -873,23 +867,16 @@ static njs_int_t
 njs_promise_prototype_then(njs_vm_t *vm, njs_value_t *args, njs_uint_t nargs,
     njs_index_t unused)
 {
     njs_int_t                 ret;
     njs_value_t               *promise, *fulfilled, *rejected, constructor;
-    njs_object_t              *object;
     njs_function_t            *function;
     njs_promise_capability_t  *capability;
 
     promise = njs_argument(args, 0);
 
-    if (njs_slow_path(!njs_is_object(promise))) {
-        goto failed;
-    }
-
-    object = njs_object_proto_lookup(njs_object(promise), NJS_PROMISE,
-                                     njs_object_t);
-    if (njs_slow_path(object == NULL)) {
+    if (njs_slow_path(!njs_is_promise(promise))) {
         goto failed;
     }
 
     function = njs_promise_create_function(vm, sizeof(njs_promise_context_t));
     function->u.native = njs_promise_constructor;
@@ -931,10 +918,12 @@ njs_promise_perform_then(njs_vm_t *vm, njs_value_t *value,
     njs_promise_t           *promise;
     njs_function_t          *function;
     njs_promise_data_t      *data;
     njs_promise_reaction_t  *fulfilled_reaction, *rejected_reaction;
 
+    njs_assert(njs_is_promise(value));
+
     if (!njs_is_function(fulfilled)) {
         fulfilled = njs_value_arg(&njs_value_undefined);
     }
 
     if (!njs_is_function(rejected)) {
diff --git a/src/njs_vmcode.c b/src/njs_vmcode.c
index 75911255..b371c374 100644
--- a/src/njs_vmcode.c
+++ b/src/njs_vmcode.c
@@ -1893,11 +1893,11 @@ njs_vmcode_await(njs_vm_t *vm, njs_vmcode_await_t *await)
 
     rejected->context = ctx;
     rejected->args_count = 1;
     rejected->u.native = njs_await_rejected;
 
-    njs_set_object(&val, &promise->object);
+    njs_set_promise(&val, promise);
     njs_set_function(&on_fulfilled, fulfilled);
     njs_set_function(&on_rejected, rejected);
 
     ret = njs_promise_perform_then(vm, &val, &on_fulfilled, &on_rejected, NULL);
     if (njs_slow_path(ret != NJS_OK)) {
diff --git a/test/js/promise_prototype_reject_type_confusion.t.js b/test/js/promise_prototype_reject_type_confusion.t.js
new file mode 100644
index 00000000..0201f29c
--- /dev/null
+++ b/test/js/promise_prototype_reject_type_confusion.t.js
@@ -0,0 +1,11 @@
+/*---
+includes: []
+flags: [async]
+---*/
+
+Symbol.__proto__ = new Promise(()=>{});
+
+Promise.reject(Symbol)
+.then(v => $DONOTEVALUATE())
+.catch(err => assert.sameValue(err.name, 'Symbol'))
+.then($DONE, $DONE);
diff --git a/test/js/promise_prototype_then_type_confusion.t.js b/test/js/promise_prototype_then_type_confusion.t.js
new file mode 100644
index 00000000..72b687cf
--- /dev/null
+++ b/test/js/promise_prototype_then_type_confusion.t.js
@@ -0,0 +1,11 @@
+/*---
+includes: []
+flags: [async]
+---*/
+
+Symbol.__proto__ = new Promise(()=>{});
+
+Promise.resolve(Symbol)
+.then(v => $DONOTEVALUATE())
+.catch(err => assert.sameValue(err.name, 'TypeError'))
+.then($DONE, $DONE);

Vulnerable code:     njs_object_t              *object;
    if (njs_is_object(x)) {
        object = njs_object_proto_lookup(njs_object(x), NJS_PROMISE,
                                         njs_object_t);

        if (object != NULL) {
            ret = njs_value_property(vm, x, njs_value_arg(&string_constructor),
                                     &value);
            if (njs_slow_path(ret == NJS_ERROR)) {
                return NULL;
            }
            if (njs_values_same(&value, constructor)) {
                return njs_promise(x);
            }
    njs_object_t              *object;
    if (njs_slow_path(!njs_is_object(promise))) {
        goto failed;
    }

    object = njs_object_proto_lookup(njs_object(promise), NJS_PROMISE,
                                     njs_object_t);
    if (njs_slow_path(object == NULL)) {
    njs_set_object(&val, &promise->object);
Fixed code:     if (njs_is_promise(x)) {
        ret = njs_value_property(vm, x, njs_value_arg(&string_constructor),
                                 &value);
        if (njs_slow_path(ret == NJS_ERROR)) {
            return NULL;
        }
        if (njs_values_same(&value, constructor)) {
            return njs_promise(x);
    if (njs_slow_path(!njs_is_promise(promise))) {
    njs_assert(njs_is_promise(value));

    njs_set_promise(&val, promise);
/*---
includes: []
flags: [async]
---*/

Symbol.__proto__ = new Promise(()=>{});

Promise.reject(Symbol)
.then(v => $DONOTEVALUATE())
.catch(err => assert.sameValue(err.name, 'Symbol'))
.then($DONE, $DONE);
/*---
includes: []
flags: [async]
---*/

Symbol.__proto__ = new Promise(()=>{});

Promise.resolve(Symbol)
.then(v => $DONOTEVALUATE())
.catch(err => assert.sameValue(err.name, 'TypeError'))
.then($DONE, $DONE);