CVE-2022-43286 (C, published 2022-10-28T21:15Z)
Description: value: Nginx NJS v0.7.2 was discovered to contain a heap-use-after-free bug caused by illegal memory copy in the function njs_json_parse_iterator_call at njs_json.c.
Severity: nan (CVSS v3 base score: 9.8)
Weakness: CWE-416 \u2014 Use After Free
The product reuses or references memory after it has been freed. At some point afterward, the memory may be allocated again and saved in another pointer, while the original pointer references a location somewhere within the new allocation. Any operations using the original pointer are no longer valid because the memory "belongs" to the code that operates on the new pointer.
Repository: https://github.com/nginx/njs
Files changed:
src/njs_json.c
src/test/njs_benchmark.c
src/test/njs_unit_test.c
Commit message: Fixed JSON.parse() when reviver function is provided.

This closes #480 issue on Github.


Vulnerable code:
    njs_object_prop_t          *prop;
typedef struct {
    njs_value_t                retval;

    njs_uint_t                 depth;
#define NJS_JSON_MAX_DEPTH     32
    njs_json_state_t           states[NJS_JSON_MAX_DEPTH];

    njs_function_t             *function;
} njs_json_parse_t;


static njs_int_t njs_json_parse_iterator(njs_vm_t *vm, njs_json_parse_t *parse,
    njs_value_t *value);
static njs_int_t njs_json_parse_iterator_call(njs_vm_t *vm,
    njs_json_parse_t *parse, njs_json_state_t *state);
    njs_value_t           *text, value, lvalue;
    njs_json_parse_t      *parse, json_parse;
    parse = &json_parse;

    if (njs_slow_path(njs_is_function(reviver) && njs_is_object(&value))) {
        parse->function = njs_function(reviver);
        parse->depth = 0;
        return njs_json_parse_iterator(vm, parse, &value);
static njs_json_state_t *
njs_json_push_parse_state(njs_vm_t *vm, njs_json_parse_t *parse,
    njs_value_t *value)
{
    njs_json_state_t  *state;

    if (njs_slow_path(parse->depth >= NJS_JSON_MAX_DEPTH)) {
        njs_type_error(vm, "Nested too deep or a cyclic structure");
        return NULL;
    }

    state = &parse->states[parse->depth++];
    state->value = *value;
    state->index = 0;
    state->prop = NULL;
    state->keys = njs_value_own_enumerate(vm, value, NJS_ENUM_KEYS,
                                          NJS_ENUM_STRING, 0);
    if (state->keys == NULL) {
        return NULL;
    }

    return state;
}


njs_inline njs_json_state_t *
njs_json_pop_parse_state(njs_vm_t *vm, njs_json_parse_t *parse)
{
    njs_json_state_t  *state;

    state = &parse->states[parse->depth - 1];
    njs_array_destroy(vm, state->keys);
    state->keys = NULL;

    if (parse->depth > 1) {
        parse->depth--;
        return &parse->states[parse->depth - 1];
    }

    return NULL;
}


njs_json_parse_iterator(njs_vm_t *vm, njs_json_parse_t *parse,
    njs_value_t *object)
    njs_int_t             ret;
    njs_value_t           *key, wrapper;
    njs_object_t          *obj;
    njs_json_state_t      *state;
    njs_object_prop_t     *prop;
    njs_property_query_t  pq;
    obj = njs_json_wrap_value(vm, &wrapper, object);
    if (njs_slow_path(obj == NULL)) {
    state = njs_json_push_parse_state(vm, parse, &wrapper);
    if (njs_slow_path(state == NULL)) {
    for ( ;; ) {
        if (state->index < state->keys->length) {
            njs_property_query_init(&pq, NJS_PROPERTY_QUERY_SET, 0);

            key = &state->keys->start[state->index];

            ret = njs_property_query(vm, &pq, &state->value, key);
            if (njs_slow_path(ret != NJS_OK)) {
                if (ret == NJS_DECLINED) {
                    state->index++;
                    continue;
                }
            prop = pq.lhq.value;

            if (prop->type == NJS_WHITEOUT) {
                state->index++;
                continue;
            }

            state->prop = prop;
            if (prop->type == NJS_PROPERTY && njs_is_object(&prop->value)) {
                state = njs_json_push_parse_state(vm, parse, &prop->value);
                if (state == NULL) {
                    return NJS_ERROR;
                continue;
            }
            if (prop->type == NJS_PROPERTY_REF
                && njs_is_object(prop->value.data.u.value))
            {
                state = njs_json_push_parse_state(vm, parse,
                                                  prop->value.data.u.value);
                if (state == NULL) {
                    return NJS_ERROR;
                continue;
            state = njs_json_pop_parse_state(vm, parse);
            if (state == NULL) {
                vm->retval = parse->retval;
                return NJS_OK;
            }
        }

        ret = njs_json_parse_iterator_call(vm, parse, state);
        if (njs_slow_path(ret != NJS_OK)) {
            return ret;
        }
    }
}

static njs_int_t
njs_json_parse_iterator_call(njs_vm_t *vm, njs_json_parse_t *parse,
    njs_json_state_t *state)
{
    njs_int_t          ret;
    njs_value_t        arguments[3], *value;
    njs_object_prop_t  *prop;

    prop = state->prop;

    arguments[0] = state->value;
    arguments[1] = state->keys->start[state->index++];

    switch (prop->type) {
    case NJS_PROPERTY:
        arguments[2] = prop->value;
        ret = njs_function_apply(vm, parse->function, arguments, 3,
                                 &parse->retval);
        if (njs_slow_path(ret != NJS_OK)) {
            return ret;
        }
        if (njs_is_undefined(&parse->retval)) {
            prop->type = NJS_WHITEOUT;
        } else {
            prop->value = parse->retval;
        }
        break;
    case NJS_PROPERTY_REF:
        value = prop->value.data.u.value;
        arguments[2] = *value;
        ret = njs_function_apply(vm, parse->function, arguments, 3,
                                 &parse->retval);
        if (njs_slow_path(ret != NJS_OK)) {
            return ret;
        if (njs_is_undefined(&parse->retval)) {
            ret = njs_value_property_i64_delete(vm, &state->value,
                                                state->index - 1, NULL);

        } else {
            ret = njs_value_property_i64_set(vm, &state->value,
                                             state->index - 1, &parse->retval);
        }
        if (njs_slow_path(ret == NJS_ERROR)) {
            return NJS_ERROR;
        }
        break;
    default:
        njs_internal_error(vm, "njs_json_parse_iterator_call() unexpected "
                         "property type:%s", njs_prop_type_string(prop->type));
    return NJS_OK;

Fixed code:
    njs_value_t                prop;
#define NJS_JSON_MAX_DEPTH     32
static njs_int_t njs_json_internalize_property(njs_vm_t *vm,
    njs_function_t *reviver, njs_value_t *holder, njs_value_t *name,
    njs_int_t depth, njs_value_t *retval);
    njs_value_t           *text, value, lvalue, wrapper;
    njs_object_t          *obj;
    if (njs_slow_path(njs_is_function(reviver))) {
        obj = njs_json_wrap_value(vm, &wrapper, &value);
        if (njs_slow_path(obj == NULL)) {
            return NJS_ERROR;
        }
        return njs_json_internalize_property(vm, njs_function(reviver),
                                             &wrapper,
                                             njs_value_arg(&njs_string_empty),
                                             0, &vm->retval);
njs_json_internalize_property(njs_vm_t *vm, njs_function_t *reviver,
    njs_value_t *holder, njs_value_t *name, njs_int_t depth,
    njs_value_t *retval)
    int64_t       k, length;
    njs_int_t     ret;
    njs_value_t   val, new_elem, index;
    njs_value_t   arguments[3];
    njs_array_t   *keys;
    if (njs_slow_path(depth++ >= NJS_JSON_MAX_DEPTH)) {
        njs_type_error(vm, "Nested too deep or a cyclic structure");
    ret = njs_value_property(vm, holder, name, &val);
    if (njs_slow_path(ret == NJS_ERROR)) {
    keys = NULL;
    if (njs_is_object(&val)) {
        if (!njs_is_array(&val)) {
            keys = njs_array_keys(vm, &val, 0);
            if (njs_slow_path(keys == NULL)) {
            for (k = 0; k < keys->length; k++) {
                ret = njs_json_internalize_property(vm, reviver, &val,
                                                    &keys->start[k], depth,
                                                    &new_elem);
                if (njs_slow_path(ret != NJS_OK)) {
                    goto done;
                if (njs_is_undefined(&new_elem)) {
                    ret = njs_value_property_delete(vm, &val, &keys->start[k],
                                                    NULL, 0);
                } else {
                    ret = njs_value_property_set(vm, &val, &keys->start[k],
                                                 &new_elem);
                if (njs_slow_path(ret == NJS_ERROR)) {
                    goto done;
                }
            ret = njs_object_length(vm, &val, &length);
            if (njs_slow_path(ret == NJS_ERROR)) {
                return NJS_ERROR;
            }
            for (k = 0; k < length; k++) {
                ret = njs_int64_to_string(vm, &index, k);
                if (njs_slow_path(ret != NJS_OK)) {
                    return NJS_ERROR;
                }
                ret = njs_json_internalize_property(vm, reviver, &val, &index,
                                                    depth, &new_elem);
                if (njs_slow_path(ret != NJS_OK)) {
                    return NJS_ERROR;
                }
                if (njs_is_undefined(&new_elem)) {
                    ret = njs_value_property_delete(vm, &val, &index, NULL, 0);
                } else {
                    ret = njs_value_property_set(vm, &val, &index, &new_elem);
                }
                if (njs_slow_path(ret == NJS_ERROR)) {
                    return NJS_ERROR;
                }
            }
    }
    njs_value_assign(&arguments[0], holder);
    njs_value_assign(&arguments[1], name);
    njs_value_assign(&arguments[2], &val);
    ret = njs_function_apply(vm, reviver, arguments, 3, retval);
done:
    if (keys != NULL) {
        njs_array_destroy(vm, keys);
    return ret;
    { "JSON.parse large",
      njs_str("JSON.parse(JSON.stringify([Array(2**16)]))[0].length"),
      njs_str("65536"),
      10 },

    { "JSON.parse reviver large",
      njs_str("JSON.parse(JSON.stringify([Array(2**16)]), v=>v)"),
      njs_str(""),
      10 },

    { njs_str("JSON.parse(JSON.stringify([Array(2**16)]), v => v)"),
      njs_str("") },

    { njs_str("var order = []; function reviver(k, v) { order.push(k); };"
              "JSON.parse('{\"p1\":0,\"p2\":0,\"p1\":0,\"2\":0,\"1\":0}', reviver);"
              "order"),
      njs_str("1,2,p1,p2,") },

    { njs_str("function reviver(k, v) {"
              "    if (k == '0') Object.defineProperty(this, '1', {configurable: false});"
              "    if (k == '1') return;"
              "    return v;"
              " };"
              "JSON.parse('[1, 2]', reviver)"),
      njs_str("1,2") },

    { njs_str("JSON.parse('0', (k, v) => {throw 'Oops'})"),
      njs_str("Oops") },

    { njs_str("JSON.parse('{\"a\":1}', (k, v) => {if (k == 'a') {throw 'Oops'}; return v;})"),
      njs_str("Oops") },

    { njs_str("JSON.parse('[2,3,43]', (k, v) => {if (v == 43) {throw 'Oops'}; return v;})"),
      njs_str("Oops") },