Use-after-free in cJSON_ReplaceItemInObject when the key is the item's own name
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 82/100
Research direction
Start with replace_item_in_object in cJSON.c and run the provided same-name replacement reproducer under AddressSanitizer. Check ctest and confirm the reproducer completes without a use-after-free, reports success, and replaces the destination value with 30.
Written by the indexing model from the issue text.
Description
replace_item_in_object() frees replacement->string before it duplicates the string argument. If the caller passes the item's own name as the key, cJSON_strdup() reads the buffer that was just freed. This is easy to run into when moving an item from one object to another under the same name:
#include <stdio.h>
#include <stdlib.h>
#include "cJSON.h"
int main(void)
{
cJSON *dst = cJSON_Parse("{\"timeout\":5}");
cJSON *src = cJSON_Parse("{\"timeout\":30}");
cJSON *item = cJSON_DetachItemFromObject(src, "timeout");
/* move "timeout" from src into dst under the same name */
int ok = cJSON_ReplaceItemInObject(dst, item->string, item);
char *out = cJSON_PrintUnformatted(dst);
printf("ok=%d dst=%s\n", ok, out);
free(out);
if (!ok) cJSON_Delete(item);
cJSON_Delete(src);
cJSON_Delete(dst);
return 0;
}
With -fsanitize=address:
==36383==ERROR: AddressSanitizer: heap-use-after-free on address 0x602000000050 at pc 0x5555555904c6 bp 0x7fffffffd270 sp 0x7fffffffca38
READ of size 2 at 0x602000000050 thread T0
#0 in strlen <null>
#1 in cJSON_strdup cJSON.c:198:14
#2 in replace_item_in_object cJSON.c:2439:34
#3 in main replace_key.c:12:14
0x602000000050 is located 0 bytes inside of 9-byte region [0x602000000050,0x602000000059)
freed by thread T0 here:
#0 in free <null>
#1 in cJSON_free cJSON.c:3204:5
#2 in replace_item_in_object cJSON.c:2437:9
#3 in main replace_key.c:12:14
Without a sanitizer (glibc) it doesn't crash, but the replacement silently doesn't happen: the program prints ok=0 dst={"timeout":5}. With the change below it prints ok=1 dst={"timeout":30}.
This is the same ordering problem that #248 reported for cJSON_AddItemToObject() and that 22a7d04 fixed in add_item_to_object(). The replace path still frees first.
Open PR #859 moves the key copy into cJSON_ReplaceItemViaPointer() and avoids this as a side effect (the example runs clean with it applied to master), but it also changes what cJSON_ReplaceItemViaPointer() does. The change below only fixes the ordering.
Reproduced on v1.7.19 and on current master (6d9f244), x86_64 Linux, clang 15.
Suggested fix, the same approach as 22a7d04: copy the key first, then free the old one, and use the copy for the lookup.
--- a/cJSON.c
+++ b/cJSON.c
@@ -2426,25 +2426,30 @@ CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInArray(cJSON *array, int which, cJSON
static cJSON_bool replace_item_in_object(cJSON *object, const char *string, cJSON *replacement, cJSON_bool case_sensitive)
{
+ char *new_key = NULL;
+
if ((replacement == NULL) || (string == NULL))
{
return false;
}
+ /* copy the key first, string may point to replacement->string */
+ new_key = (char*)cJSON_strdup((const unsigned char*)string, &global_hooks);
+ if (new_key == NULL)
+ {
+ return false;
+ }
+
/* replace the name in the replacement */
if (!(replacement->type & cJSON_StringIsConst) && (replacement->string != NULL))
{
cJSON_free(replacement->string);
}
- replacement->string = (char*)cJSON_strdup((const unsigned char*)string, &global_hooks);
- if (replacement->string == NULL)
- {
- return false;
- }
+ replacement->string = new_key;
replacement->type &= ~cJSON_StringIsConst;
- return cJSON_ReplaceItemViaPointer(object, get_object_item(object, string, case_sensitive), replacement);
+ return cJSON_ReplaceItemViaPointer(object, get_object_item(object, new_key, case_sensitive), replacement);
}
CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObject(cJSON *object, const char *string, cJSON *newitem)
As a side effect, a failed strdup no longer leaves replacement->string set to NULL. With this change the example is clean under ASan and the existing tests pass (ctest, 22/22).
- Dominant language
- C
- Stars
- 13k
- Forks
- 3.5k
- PR merge metrics
- No merged PRs in 30d
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from DaveGamble/cJSON
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
DaveGamble/cJSON#1082 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
DaveGamble/cJSON#1074 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
DaveGamble/cJSON#1071 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
DaveGamble/cJSON#1067 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
DaveGamble/cJSON#1050 · 1 comment ·
All issues in DaveGamble/cJSON
Similar issues
-
level/task module/gcp type/bug
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
-
docs
Difficulty 1/5 Under an hour Newbie friendliness 85/100
-
P3 sonic-vpp
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
sonic-net/sonic-buildimage#29662 ·
-
Difficulty 1/5 Under an hour Newbie friendliness 94/100
spack/spack-packages#6586 ·
-
category:port-update
Difficulty 2/5 1-3 hours Newbie friendliness 72/100