#1081·cJSON

Use-after-free in cJSON_ReplaceItemInObject when the key is the item's own name

Author: FengxiaoxxCreated Sep 17, 2026Updated Sep 17, 2026

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:

c
#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.

diff
--- 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).