diff --git a/cJSON.c b/cJSON.c index 88c2d95b3..53e296ff0 100644 --- a/cJSON.c +++ b/cJSON.c @@ -1980,6 +1980,46 @@ static cJSON *get_object_item(const cJSON * const object, const char * const nam return current_element; } +/* qsort comparators for member-pointer arrays (no context pointer, + * so one function per case rule). NULL names sort first. */ +static int compare_member_names_sensitive(const void *a, const void *b) +{ + const char *sa = (*(cJSON * const *)a)->string; + const char *sb = (*(cJSON * const *)b)->string; + if (sa == sb) + { + return 0; + } + if (sa == NULL) + { + return -1; + } + if (sb == NULL) + { + return 1; + } + return strcmp(sa, sb); +} + +static int compare_member_names_insensitive(const void *a, const void *b) +{ + const unsigned char *sa = (const unsigned char*)(*(cJSON * const *)a)->string; + const unsigned char *sb = (const unsigned char*)(*(cJSON * const *)b)->string; + if (sa == sb) + { + return 0; + } + if (sa == NULL) + { + return -1; + } + if (sb == NULL) + { + return 1; + } + return case_insensitive_strcmp(sa, sb); +} + CJSON_PUBLIC(cJSON *) cJSON_GetObjectItem(const cJSON * const object, const char * const string) { return get_object_item(object, string, false); @@ -3069,8 +3109,14 @@ CJSON_PUBLIC(cJSON_bool) cJSON_IsRaw(const cJSON * const item) return (item->type & 0xFF) == cJSON_Raw; } -CJSON_PUBLIC(cJSON_bool) cJSON_Compare(const cJSON * const a, const cJSON * const b, const cJSON_bool case_sensitive) +static cJSON_bool cJSON_Compare_rec(const cJSON * const a, const cJSON * const b, const cJSON_bool case_sensitive, size_t depth) { + if (depth >= CJSON_NESTING_LIMIT) + { + /* nested past the limit: fail safe (CVE-2026-67216). */ + return false; + } + if ((a == NULL) || (b == NULL) || ((a->type & 0xFF) != (b->type & 0xFF))) { return false; @@ -3134,7 +3180,7 @@ CJSON_PUBLIC(cJSON_bool) cJSON_Compare(const cJSON * const a, const cJSON * cons for (; (a_element != NULL) && (b_element != NULL);) { - if (!cJSON_Compare(a_element, b_element, case_sensitive)) + if (!cJSON_Compare_rec(a_element, b_element, case_sensitive, depth + 1)) { return false; } @@ -3153,40 +3199,96 @@ CJSON_PUBLIC(cJSON_bool) cJSON_Compare(const cJSON * const a, const cJSON * cons case cJSON_Object: { - cJSON *a_element = NULL; - cJSON *b_element = NULL; - cJSON_ArrayForEach(a_element, a) + /* Sort both member lists once and compare pairwise: O(n log n) + * per level instead of the old O(n^2) double scan, so nested + * objects cannot blow up exponentially (CVE-2026-67216). + * The caller's trees are left untouched. */ + cJSON **a_members = NULL; + cJSON **b_members = NULL; + size_t a_count = 0; + size_t b_count = 0; + size_t i = 0; + cJSON *element = NULL; + cJSON_bool equal = false; + + for (element = a->child; element != NULL; element = element->next) { - /* TODO This has O(n^2) runtime, which is horrible! */ - b_element = get_object_item(b, a_element->string, case_sensitive); - if (b_element == NULL) - { - return false; - } - - if (!cJSON_Compare(a_element, b_element, case_sensitive)) - { - return false; - } + a_count++; + } + for (element = b->child; element != NULL; element = element->next) + { + b_count++; + } + if (a_count != b_count) + { + return false; + } + if (a_count == 0) + { + return true; + } + if ((a_count > (size_t)-1 / sizeof(cJSON*)) || (b_count > (size_t)-1 / sizeof(cJSON*))) + { + return false; + } + a_members = (cJSON**)cJSON_malloc(a_count * sizeof(cJSON*)); + b_members = (cJSON**)cJSON_malloc(b_count * sizeof(cJSON*)); + if ((a_members == NULL) || (b_members == NULL)) + { + cJSON_free(a_members); + cJSON_free(b_members); + return false; + } + for (element = a->child, i = 0; element != NULL; element = element->next, i++) + { + a_members[i] = element; + } + for (element = b->child, i = 0; element != NULL; element = element->next, i++) + { + b_members[i] = element; + } + if (case_sensitive) + { + qsort(a_members, a_count, sizeof(cJSON*), compare_member_names_sensitive); + qsort(b_members, b_count, sizeof(cJSON*), compare_member_names_sensitive); + } + else + { + qsort(a_members, a_count, sizeof(cJSON*), compare_member_names_insensitive); + qsort(b_members, b_count, sizeof(cJSON*), compare_member_names_insensitive); } - /* doing this twice, once on a and b to prevent true comparison if a subset of b - * TODO: Do this the proper way, this is just a fix for now */ - cJSON_ArrayForEach(b_element, b) + equal = true; + for (i = 0; i < a_count; i++) { - a_element = get_object_item(a, b_element->string, case_sensitive); - if (a_element == NULL) + const char *a_name = a_members[i]->string; + const char *b_name = b_members[i]->string; + cJSON_bool names_equal = false; + if (a_name == b_name) { - return false; + names_equal = true; } - - if (!cJSON_Compare(b_element, a_element, case_sensitive)) + else if ((a_name != NULL) && (b_name != NULL)) { - return false; + if (case_sensitive) + { + names_equal = (strcmp(a_name, b_name) == 0); + } + else + { + names_equal = (case_insensitive_strcmp((const unsigned char*)a_name, (const unsigned char*)b_name) == 0); + } + } + if (!names_equal || !cJSON_Compare_rec(a_members[i], b_members[i], case_sensitive, depth + 1)) + { + equal = false; + break; } } + cJSON_free(a_members); + cJSON_free(b_members); - return true; + return equal; } default: @@ -3194,6 +3296,11 @@ CJSON_PUBLIC(cJSON_bool) cJSON_Compare(const cJSON * const a, const cJSON * cons } } +CJSON_PUBLIC(cJSON_bool) cJSON_Compare(const cJSON * const a, const cJSON * const b, const cJSON_bool case_sensitive) +{ + return cJSON_Compare_rec(a, b, case_sensitive, 0); +} + CJSON_PUBLIC(void *) cJSON_malloc(size_t size) { return global_hooks.allocate(size); diff --git a/cJSON_Utils.c b/cJSON_Utils.c index 4f3a9ed8d..d28a731f8 100644 --- a/cJSON_Utils.c +++ b/cJSON_Utils.c @@ -108,6 +108,46 @@ static int compare_strings(const unsigned char *string1, const unsigned char *st return tolower(*string1) - tolower(*string2); } +/* qsort comparators for member-pointer arrays (no context pointer, so + * one function per case rule). NULL names sort first. */ +static int compare_json_names_sensitive(const void *a, const void *b) +{ + const unsigned char *sa = (const unsigned char*)(*(cJSON * const *)a)->string; + const unsigned char *sb = (const unsigned char*)(*(cJSON * const *)b)->string; + if (sa == sb) + { + return 0; + } + if (sa == NULL) + { + return -1; + } + if (sb == NULL) + { + return 1; + } + return strcmp((const char*)sa, (const char*)sb); +} + +static int compare_json_names_insensitive(const void *a, const void *b) +{ + const unsigned char *sa = (const unsigned char*)(*(cJSON * const *)a)->string; + const unsigned char *sb = (const unsigned char*)(*(cJSON * const *)b)->string; + if (sa == sb) + { + return 0; + } + if (sa == NULL) + { + return -1; + } + if (sb == NULL) + { + return 1; + } + return compare_strings(sa, sb, false); +} + /* securely comparison of floating-point variables */ static cJSON_bool compare_double(double a, double b) { @@ -601,8 +641,14 @@ static void sort_object(cJSON * const object, const cJSON_bool case_sensitive) object->child = sort_list(object->child, case_sensitive); } -static cJSON_bool compare_json(cJSON *a, cJSON *b, const cJSON_bool case_sensitive) +static cJSON_bool compare_json(cJSON *a, cJSON *b, const cJSON_bool case_sensitive, size_t depth) { + if (depth >= CJSON_NESTING_LIMIT) + { + /* nested past the limit: fail safe (CVE-2026-67216). */ + return false; + } + if ((a == NULL) || (b == NULL) || ((a->type & 0xFF) != (b->type & 0xFF))) { /* mismatched type. */ @@ -635,7 +681,7 @@ static cJSON_bool compare_json(cJSON *a, cJSON *b, const cJSON_bool case_sensiti case cJSON_Array: for ((void)(a = a->child), b = b->child; (a != NULL) && (b != NULL); (void)(a = a->next), b = b->next) { - cJSON_bool identical = compare_json(a, b, case_sensitive); + cJSON_bool identical = compare_json(a, b, case_sensitive, depth + 1); if (!identical) { return false; @@ -653,33 +699,89 @@ static cJSON_bool compare_json(cJSON *a, cJSON *b, const cJSON_bool case_sensiti } case cJSON_Object: - sort_object(a, case_sensitive); - sort_object(b, case_sensitive); - for ((void)(a = a->child), b = b->child; (a != NULL) && (b != NULL); (void)(a = a->next), b = b->next) + { + /* Sort copies of the member lists once and compare pairwise: + * O(n log n) per level instead of sort-then-scan recursion + * with quadratic behavior on nested objects (CVE-2026-67216). + * Unlike the old code, the compared trees are not reordered. */ + cJSON **a_members = NULL; + cJSON **b_members = NULL; + size_t a_count = 0; + size_t b_count = 0; + size_t i = 0; + cJSON *element = NULL; + + for (element = a->child; element != NULL; element = element->next) + { + a_count++; + } + for (element = b->child; element != NULL; element = element->next) + { + b_count++; + } + /* object length mismatch? */ + if (a_count != b_count) + { + return false; + } + if (a_count == 0) + { + return true; + } + if ((a_count > (size_t)-1 / sizeof(cJSON*)) || (b_count > (size_t)-1 / sizeof(cJSON*))) + { + return false; + } + a_members = (cJSON**)cJSON_malloc(a_count * sizeof(cJSON*)); + b_members = (cJSON**)cJSON_malloc(b_count * sizeof(cJSON*)); + if ((a_members == NULL) || (b_members == NULL)) + { + cJSON_free(a_members); + cJSON_free(b_members); + return false; + } + for (element = a->child, i = 0; element != NULL; element = element->next, i++) + { + a_members[i] = element; + } + for (element = b->child, i = 0; element != NULL; element = element->next, i++) + { + b_members[i] = element; + } + if (case_sensitive) + { + qsort(a_members, a_count, sizeof(cJSON*), compare_json_names_sensitive); + qsort(b_members, b_count, sizeof(cJSON*), compare_json_names_sensitive); + } + else + { + qsort(a_members, a_count, sizeof(cJSON*), compare_json_names_insensitive); + qsort(b_members, b_count, sizeof(cJSON*), compare_json_names_insensitive); + } + for (i = 0; i < a_count; i++) { cJSON_bool identical = false; /* compare object keys */ - if (compare_strings((unsigned char*)a->string, (unsigned char*)b->string, case_sensitive)) + if (compare_strings((unsigned char*)a_members[i]->string, (unsigned char*)b_members[i]->string, case_sensitive)) { /* missing member */ - return false; + identical = false; + } + else + { + identical = compare_json(a_members[i], b_members[i], case_sensitive, depth + 1); } - identical = compare_json(a, b, case_sensitive); if (!identical) { + cJSON_free(a_members); + cJSON_free(b_members); return false; } } - - /* object length mismatch (one of both children is not null) */ - if ((a != NULL) || (b != NULL)) - { - return false; - } - else - { - return true; - } + cJSON_free(a_members); + cJSON_free(b_members); + return true; + } default: break; @@ -831,7 +933,7 @@ static int apply_patch(cJSON *object, const cJSON *patch, const cJSON_bool case_ else if (opcode == TEST) { /* compare value: {...} with the given path */ - status = !compare_json(get_item_from_pointer(object, path->valuestring, case_sensitive), get_object_item(patch, "value", case_sensitive), case_sensitive); + status = !compare_json(get_item_from_pointer(object, path->valuestring, case_sensitive), get_object_item(patch, "value", case_sensitive), case_sensitive, 0); goto cleanup; } @@ -1453,7 +1555,7 @@ static cJSON *generate_merge_patch(cJSON * const from, cJSON * const to, const c else { /* object key exists in both objects */ - if (!compare_json(from_child, to_child, case_sensitive)) + if (!compare_json(from_child, to_child, case_sensitive, 0)) { /* not identical --> generate a patch */ cJSON_AddItemToObject(patch, to_child->string, cJSONUtils_GenerateMergePatch(from_child, to_child)); diff --git a/tests/compare_tests.c b/tests/compare_tests.c index 797c7740c..c6fd3f2d0 100644 --- a/tests/compare_tests.c +++ b/tests/compare_tests.c @@ -189,6 +189,62 @@ static void cjson_compare_should_compare_objects(void) false)) } +static void cjson_compare_should_compare_nested_objects(void) +{ + /* nested identical documents compare true regardless of member + * order (exercises the sorted single-pass comparison). */ + TEST_ASSERT_TRUE(compare_from_string( + "{\"b\": {\"d\": 4, \"c\": 3}, \"a\": [1, 2]}", + "{\"a\": [1, 2], \"b\": {\"c\": 3, \"d\": 4}}", + true)); + TEST_ASSERT_TRUE(compare_from_string( + "{\"B\": 1, \"a\": 2}", + "{\"b\": 1, \"A\": 2}", + false)); + /* nested differences are found */ + TEST_ASSERT_FALSE(compare_from_string( + "{\"b\": {\"d\": 4, \"c\": 3}, \"a\": [1, 2]}", + "{\"a\": [1, 2], \"b\": {\"c\": 3, \"d\": 5}}", + true)); + /* different member counts are not equal */ + TEST_ASSERT_FALSE(compare_from_string( + "{\"a\": 1}", + "{\"a\": 1, \"b\": 2}", + true)); + /* empty objects are equal */ + TEST_ASSERT_TRUE(compare_from_string("{}", "{}", true)); +} + +static void cjson_compare_should_fail_safe_past_nesting_limit(void) +{ + /* CVE-2026-67216: over-limit nesting fails safe instead of + * recursing without bound. */ + cJSON *a = cJSON_CreateArray(); + cJSON *b = cJSON_CreateArray(); + cJSON *current_a = a; + cJSON *current_b = b; + int i = 0; + + TEST_ASSERT_NOT_NULL(a); + TEST_ASSERT_NOT_NULL(b); + for (i = 0; i < 1500; i++) + { + cJSON *next_a = cJSON_CreateArray(); + cJSON *next_b = cJSON_CreateArray(); + TEST_ASSERT_NOT_NULL(next_a); + TEST_ASSERT_NOT_NULL(next_b); + cJSON_AddItemToArray(current_a, next_a); + cJSON_AddItemToArray(current_b, next_b); + current_a = next_a; + current_b = next_b; + } + + TEST_ASSERT_FALSE(cJSON_Compare(a, b, true)); + + cJSON_Delete(a); + cJSON_Delete(b); +} + int CJSON_CDECL main(void) { UNITY_BEGIN(); @@ -203,6 +259,8 @@ int CJSON_CDECL main(void) RUN_TEST(cjson_compare_should_compare_raw); RUN_TEST(cjson_compare_should_compare_arrays); RUN_TEST(cjson_compare_should_compare_objects); + RUN_TEST(cjson_compare_should_compare_nested_objects); + RUN_TEST(cjson_compare_should_fail_safe_past_nesting_limit); return UNITY_END(); }