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

Open Beginner friendly
#1,081 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
82/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
c
Domain
backend

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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from DaveGamble/cJSON

All issues in DaveGamble/cJSON

Similar issues

More C issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.